1/* Snapiom Analytics â beacon first-party (sortant uniquement). 2 Mesure : temps actif, scroll max, source, device, clics internes/sortants, quick-back. 3 Envoi d'UN résumé par page vue via sendBeacon (text/plain â pas de préflight CORS). */ 4(function () { 5 if (!window.SnapiomAN || !SnapiomAN.pk) return; 6 var cfg = SnapiomAN; 7 var startTs = Date.now(); 8 var activeMs = 0, lastTick = Date.now(); 9 var visible = (document.visibilityState === 'visible'); 10 var maxScroll = 0, internalClicks = 0, outboundClicks = 0, sent = false; 11 12 // --- Core Web Vitals (RUM) : mesures RÃELLES du chargement côté visiteur --- 13 // LCP = quand le plus gros élément (souvent l'image de recette) s'affiche ; 14 // CLS = stabilité visuelle ; INP = latence d'interaction ; FCP/TTFB = début. 15 // Collectés via PerformanceObserver (buffered) puis envoyés dans le MÃME beacon. 16 var vitals = { lcp: null, cls: null, inp: null, fcp: null, ttfb: null }; 17 try { 18 var nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; 19 if (nav && nav.responseStart) vitals.ttfb = Math.round(nav.responseStart); 20 } catch (e) {} 21 function vObserve(type, cb) { 22 try { var po = new PerformanceObserver(function (l) { cb(l.getEntries()); }); po.observe({ type: type, buffered: true }); return po; } 23 catch (e) { return null; } 24 } 25 vObserve('paint', function (es) { es.forEach(function (en) { if (en.name === 'first-contentful-paint') vitals.fcp = Math.round(en.startTime); }); }); 26 vObserve('largest-contentful-paint', function (es) { var last = es[es.length - 1]; if (last) vitals.lcp = Math.round(last.startTime); }); 27 var clsMax = 0, clsSess = 0, clsEntries = []; 28 vObserve('layout-shift', function (es) { 29 es.forEach(function (en) { 30 if (en.hadRecentInput) return; 31 var first = clsEntries[0], last = clsEntries[clsEntries.length - 1]; 32 if (clsSess && en.startTime - last.startTime < 1000 && en.startTime - first.startTime < 5000) { clsSess += en.value; clsEntries.push(en); } 33 else { clsSess = en.value; clsEntries = [en]; } 34 if (clsSess > clsMax) { clsMax = clsSess; vitals.cls = Math.round(clsMax * 1000) / 1000; } 35 }); 36 }); 37 var inpMax = 0; 38 try { 39 new PerformanceObserver(function (l) { l.getEntries().forEach(function (en) { if (en.duration > inpMax) { inpMax = en.duration; vitals.inp = Math.round(inpMax); } }); }) 40 .observe({ type: 'event', buffered: true, durationThreshold: 40 }); 41 } catch (e) {} 42 43 function tick() { 44 var now = Date.now(); 45 if (visible) activeMs += now - lastTick; 46 lastTick = now; 47 } 48 var iv = setInterval(tick, 4000); 49 50 document.addEventListener('visibilitychange', function () { 51 tick(); 52 visible = (document.visibilityState === 'visible'); 53 lastTick = Date.now(); 54 if (document.visibilityState === 'hidden') send(); 55 }); 56 57 function onScroll() { 58 var h = document.documentElement; 59 var scrollable = (h.scrollHeight - h.clientHeight); 60 var top = (h.scrollTop || window.pageYOffset || 0); 61 var pct = scrollable > 0 ? Math.round((top / scrollable) * 100) : 100; 62 if (pct > maxScroll) maxScroll = Math.max(0, Math.min(100, pct)); 63 } 64 window.addEventListener('scroll', onScroll, { passive: true }); 65 onScroll(); 66 67 document.addEventListener('click', function (e) { 68 var a = e.target && e.target.closest ? e.target.closest('a[href]') : null; 69 if (!a) return; 70 var href = a.getAttribute('href') || ''; 71 if (href.indexOf('http') !== 0) { internalClicks++; return; } // relatif/ancre = interne 72 try { 73 var host = new URL(href, location.href).hostname.replace(/^www\./, ''); 74 if (host === cfg.host) internalClicks++; else outboundClicks++; 75 } catch (err) {} 76 }, true); 77 78 function classifySource() { 79 try { 80 var utm = (new URLSearchParams(location.search).get('utm_source') || '').toLowerCase(); 81 if (utm) { 82 if (utm.indexOf('pinterest') >= 0) return 'pinterest'; 83 if (utm.indexOf('google') >= 0) return 'google'; 84 if (['facebook', 'instagram', 'twitter', 'tiktok'].some(function (s) { return utm.indexOf(s) >= 0; })) return 'social'; 85 return 'referral'; 86 } 87 var ref = document.referrer; 88 if (!ref) return 'direct'; 89 var rh = new URL(ref).hostname.replace(/^www\./, ''); 90 if (rh === cfg.host) return 'direct'; 91 if (rh.indexOf('pinterest.') >= 0 || rh.indexOf('pin.it') >= 0) return 'pinterest'; 92 if (rh.indexOf('google.') >= 0) return 'google'; 93 if (['facebook.com', 'instagram.com', 't.co', 'twitter.com', 'x.com', 'tiktok.com'].some(function (s) { return rh.indexOf(s) >= 0; })) return 'social'; 94 return 'referral'; 95 } catch (e) { return 'direct'; } 96 } 97 98 function refererHost() { 99 try { return document.referrer ? new URL(document.referrer).hostname.replace(/^www\./, '') : ''; } catch (e) { return ''; } 100 } 101 102 // UTM d'attribution (ex: pin Pinterest â utm_content = id du pin). Présents seulement 103 // sur la page d'atterrissage â on les persiste en sessionStorage pour les rattacher aux 104 // pages vues suivantes de la même session (attribution first-touch).
105 function utmParams() { 106 var keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']; 107 var out = {}; 108 try { 109 var qs = new URLSearchParams(location.search); 110 var hasFresh = false; 111 keys.forEach(function (k) { if (qs.get(k)) hasFresh = true; }); 112 if (hasFresh) { 113 keys.forEach(function (k) { 114 var val = (qs.get(k) || '').slice(0, 190); 115 if (val) out[k] = val; 116 }); 117 try { sessionStorage.setItem('snap_utm', JSON.stringify(out)); } catch (e) {} 118 return out; 119 } 120 // Pas d'UTM dans l'URL courante â réutilise ceux de l'atterrissage (même session). 121 var stored = sessionStorage.getItem('snap_utm'); 122 if (stored) { var p = JSON.parse(stored); if (p && typeof p === 'object') return p; } 123 } catch (e) {} 124 return out; 125 } 126 127 function device() { 128 var w = window.innerWidth || screen.width || 0; 129 if (w && w < 768) return 'mobile'; 130 if (w && w < 1024) return 'tablet'; 131 return 'desktop'; 132 } 133 134 function visitorHash() { 135 try { 136 var today = new Date().toISOString().slice(0, 10); 137 var v = localStorage.getItem('snap_vh'); 138 if (!v || v.slice(0, 10) !== today) { 139 v = today + '.' + Math.random().toString(36).slice(2, 12); 140 localStorage.setItem('snap_vh', v); 141 } 142 return v.slice(0, 40); 143 } catch (e) { return ''; } 144 } 145 146 function send() { 147 if (sent) return; 148 tick(); 149 var activeSeconds = Math.round(activeMs / 1000); 150 var quickback = (activeSeconds < 8 && maxScroll < 25) ? 1 : 0; 151 var utm = utmParams(); 152 var payload = { 153 public_key: cfg.pk, 154 events: [{ 155 post_id: cfg.postId, 156 path: location.pathname, 157 referer_host: refererHost(), 158 source: classifySource(), 159 utm_source: utm.utm_source, 160 utm_medium: utm.utm_medium, 161 utm_campaign: utm.utm_campaign, 162 utm_content: utm.utm_content, 163 utm_term: utm.utm_term, 164 device: device(), 165 scroll_pct: maxScroll, 166 active_seconds: activeSeconds, 167 internal_clicks: internalClicks, 168 outbound_clicks: outboundClicks, 169 is_quickback: quickback, 170 visitor_hash: visitorHash(), 171 lcp_ms: vitals.lcp, 172 cls: vitals.cls, 173 inp_ms: vitals.inp, 174 fcp_ms: vitals.fcp, 175 ttfb_ms: vitals.ttfb 176 }] 177 }; 178 sent = true; 179 clearInterval(iv); 180 try { 181 var blob = new Blob([JSON.stringify(payload)], { type: 'text/plain' }); 182 if (navigator.sendBeacon) navigator.sendBeacon(cfg.url, blob); 183 else fetch(cfg.url, { method: 'POST', body: JSON.stringify(payload), keepalive: true, headers: { 'Content-Type': 'text/plain' } }); 184 } catch (e) {} 185 } 186 187 window.addEventListener('pagehide', send); 188})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.