1(function () { 2 'use strict'; 3 4 if (typeof window.sharedSocket === 'undefined') return; 5 6 var socket = window.sharedSocket; 7 var sessionId = null; 8 var startTime = Date.now(); 9 var maxScroll = 0; 10 var clickBuffer = []; 11 var heartbeatInterval = null; 12 var clickFlushInterval = null; 13 var scrollTimeout = null; 14 15 // --- PAGE VIEW --- 16 function trackPageView() { 17 var sourceID = typeof getOrCreateSourceID === 'function' ? getOrCreateSourceID() : localStorage.getItem('sourceID'); 18 var payload = { 19 sourceID: sourceID, 20 url: window.location.pathname + window.location.search, 21 title: document.title, 22 referrer: document.referrer || null, 23 viewportW: window.innerWidth, 24 viewportH: window.innerHeight 25 }; 26 27 socket.emit('analytics:pageview', payload, function (response) { 28 if (response && response.sessionId) { 29 sessionId = response.sessionId; 30 console.log('[Analytics] Session started:', sessionId); 31 startHeartbeat(); 32 startClickFlush(); 33 } 34 }); 35 } 36 37 // --- HEARTBEAT (toutes les 15s) --- 38 function startHeartbeat() { 39 if (heartbeatInterval) clearInterval(heartbeatInterval); 40 heartbeatInterval = setInterval(function () { 41 if (!sessionId) return; 42 socket.emit('analytics:heartbeat', { 43 sessionId: sessionId, 44 durationMs: Date.now() - startTime, 45 scrollPct: maxScroll 46 }); 47 }, 15000); 48 } 49 50 // --- SCROLL TRACKING (throttle 500ms) --- 51 function getScrollPercent() { 52 var scrollTop = window.pageYOffset || document.documentElement.scrollTop; 53 var docHeight = Math.max( 54 document.body.scrollHeight, 55 document.documentElement.scrollHeight 56 ) - window.innerHeight; 57 if (docHeight <= 0) return 100; 58 return Math.min(Math.round((scrollTop / docHeight) * 100), 100); 59 } 60 61 window.addEventListener('scroll', function () { 62 if (scrollTimeout) return; 63 scrollTimeout = setTimeout(function () { 64 scrollTimeout = null; 65 var pct = getScrollPercent(); 66 if (pct > maxScroll) { 67 maxScroll = pct; 68 if (sessionId) { 69 socket.emit('analytics:scroll', { 70 sessionId: sessionId, 71 scrollPct: maxScroll 72 }); 73 } 74 } 75 }, 500); 76 }, { passive: true }); 77 78 // --- CLICK TRACKING (capture phase = intercepte avant stopPropagation) --- 79 // Remonte au parent significatif (a, button, img, element avec id/onclick) 80 function findMeaningfulTarget(el) { 81 var node = el; 82 while (node && node !== document.body && node !== document.documentElement) { 83 var tag = (node.tagName || '').toLowerCase(); 84 if (tag === 'a' || tag === 'button' || tag === 'input' || tag === 'select' || 85 tag === 'img' || tag === 'video' || node.id || 86 (node.getAttribute && (node.getAttribute('role') === 'button' || node.getAttribute('onclick')))) { 87 return node; 88 } 89 node = node.parentElement; 90 } 91 return el; // fallback: element original 92 } 93 94 document.addEventListener('click', function (e) { 95 if (!sessionId) return; 96 97 var meaningful = findMeaningfulTarget(e.target); 98 99 // Centre de l'element (pas la souris) = position stable et coherente 100 var rect = meaningful.getBoundingClientRect(); 101 var scrollX = window.pageXOffset || 0; 102 var scrollY = window.pageYOffset || 0; 103 var pageW = document.documentElement.scrollWidth || window.innerWidth; 104 var pageH = document.documentElement.scrollHeight || window.innerHeight; 105 106 var centerX = rect.left + rect.width / 2 + scrollX; 107 var centerY = rect.top + rect.height / 2 + scrollY; 108 109 var clickData = { 110 xPct: pageW > 0 ? parseFloat(((centerX / pageW) * 100).toFixed(1)) : 0, 111 yPct: pageH > 0 ? parseFloat(((centerY / pageH) * 100).toFixed(1)) : 0, 112 tag: (meaningful.tagName || '').toLowerCase(), 113 id: meaningful.id || '', 114 cls: (meaningful.className && typeof meaningful.className === 'string') ? meaningful.className.substring(0, 255) : '', 115 text: (meaningful.innerText || meaningful.textContent || '').trim().substring(0, 50) 116 }; 117 118 clickBuffer.push(clickData); 119 120 // Si clic sur un lien, flush immediatement (navigation imminente) 121 if (meaningful.tagName === 'A' || (meaningful.closest && meaningful.closest('a[href]'))) { 122 flushClicks(); 123 } 124 }, true); // <-- capture: true 125 126 // Flush clics toutes les 2s 127 function startClickFlush() { 128 if (clickFlushInterval) clearInterval(clickFlushInterval);
129 clickFlushInterval = setInterval(flushClicks, 2000); 130 } 131 132 function flushClicks() { 133 if (!sessionId || clickBuffer.length === 0) return; 134 var batch = clickBuffer.splice(0, clickBuffer.length); 135 socket.emit('analytics:click-batch', { 136 sessionId: sessionId, 137 clicks: batch 138 }); 139 } 140 141 // --- CLEANUP au changement de page --- 142 window.addEventListener('beforeunload', function () { 143 flushClicks(); 144 if (sessionId) { 145 socket.emit('analytics:heartbeat', { 146 sessionId: sessionId, 147 durationMs: Date.now() - startTime, 148 scrollPct: maxScroll 149 }); 150 } 151 }); 152 153 // --- INIT --- 154 if (socket.connected) { 155 trackPageView(); 156 } else { 157 socket.on('connect', function onAnalyticsConnect() { 158 socket.off('connect', onAnalyticsConnect); 159 trackPageView(); 160 }); 161 } 162})();
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.