1/** 2 * Visitor Tracking Script v2.0 3 * ç¨äºæ¶éè®¿å®¢æ°æ®å¹¶åéå°å端 4 * 使ç¨é¡µé¢å¯è§æ§API精确ç»è®¡å¨çº¿æ¶é´ 5 */ 6(function() { 7 'use strict'; 8 9 const TRACKER_VERSION = '2.0.0'; 10 const API_BASE = '/api/track'; 11 const HEARTBEAT_INTERVAL = 30000; // 30ç§å¿è·³ 12 const SESSION_KEY = 'visitor_session_id'; 13 const VISITOR_KEY = 'visitor_id'; 14 15 // çæå¯ä¸è®¿å®¢ID 16 function generateVisitorId() { 17 const stored = localStorage.getItem(VISITOR_KEY); 18 if (stored) return stored; 19 20 const id = 'v_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9); 21 localStorage.setItem(VISITOR_KEY, id); 22 return id; 23 } 24 25 // çæä¼è¯IDï¼æ¯å¤©éç½®ï¼ 26 function getSessionId() { 27 const today = new Date().toDateString(); 28 const stored = sessionStorage.getItem(SESSION_KEY); 29 const storedDate = sessionStorage.getItem(SESSION_KEY + '_date'); 30 31 if (stored && storedDate === today) return stored; 32 33 const id = 's_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9); 34 sessionStorage.setItem(SESSION_KEY, id); 35 sessionStorage.setItem(SESSION_KEY + '_date', today); 36 return id; 37 } 38 39 // æ£æµè®¾å¤ç±»å 40 function getDeviceType() { 41 const ua = navigator.userAgent; 42 if (/iPad|Tablet|PlayBook/i.test(ua)) return 'Tablet'; 43 if (/Mobile|Android|iPhone|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua)) return 'Mobile'; 44 return 'PC'; 45 } 46 47 // æ£æµæä½ç³»ç» 48 function getOS() { 49 const ua = navigator.userAgent; 50 if (/Windows NT 10/i.test(ua)) return 'Windows 10'; 51 if (/Windows NT 6.3/i.test(ua)) return 'Windows 8.1'; 52 if (/Windows NT 6.2/i.test(ua)) return 'Windows 8'; 53 if (/Windows NT 6.1/i.test(ua)) return 'Windows 7'; 54 if (/Windows/i.test(ua)) return 'Windows'; 55 if (/Mac OS X (\d+[._]\d+)/i.test(ua)) return 'macOS ' + RegExp.$1.replace('_', '.'); 56 if (/Android (\d+\.?\d*)/i.test(ua)) return 'Android ' + RegExp.$1; 57 if (/iPhone OS (\d+[._]\d+)/i.test(ua)) return 'iOS ' + RegExp.$1.replace('_', '.'); 58 if (/iPad/i.test(ua)) return 'iPadOS'; 59 if (/Linux/i.test(ua)) return 'Linux'; 60 return 'Unknown'; 61 } 62 63 // æ£æµæµè§å¨ 64 function getBrowser() { 65 const ua = navigator.userAgent; 66 if (/Edg\//i.test(ua)) return 'Edge'; 67 if (/Chrome/i.test(ua) && !/Chromium|Edge/i.test(ua)) return 'Chrome'; 68 if (/Firefox/i.test(ua)) return 'Firefox'; 69 if (/Safari/i.test(ua) && !/Chrome/i.test(ua)) return 'Safari'; 70 if (/MSIE|Trident/i.test(ua)) return 'IE'; 71 if (/Opera|OPR/i.test(ua)) return 'Opera'; 72 return 'Unknown'; 73 } 74 75 // è·å页é¢ç±»å 76 function getPageType() { 77 const path = window.location.pathname; 78 if (path === '/' || path === '/index') return 'home'; 79 if (/^\/match\/\d+/i.test(path)) return 'live'; 80 if (/^\/news|article/i.test(path)) return 'article'; 81 if (/^\/league/i.test(path)) return 'league'; 82 if (/^\/team/i.test(path)) return 'team'; 83 if (/^\/admin/i.test(path)) return 'admin'; 84 return 'other'; 85 } 86 87 // è·åå¼ç¨IDï¼æ¯èµIDææç« IDï¼ 88 function getReferenceId() { 89 const path = window.location.pathname; 90 const matchId = path.match(/\/match\/(\d+)/i); 91 if (matchId) return matchId[1]; 92 const articleId = path.match(/\/(?:news|article)\/(\d+|[\w-]+)/i); 93 if (articleId) return articleId[1]; 94 return null; 95 } 96 97 // åéè¿½è¸ªæ°æ® 98 async function sendTrack(action, data = {}) { 99 try { 100 const payload = { 101 action: action, 102 visitorId: generateVisitorId(), 103 sessionId: getSessionId(), 104 timestamp: new Date().toISOString(), 105 ...data 106 }; 107 108 const response = await fetch(API_BASE, { 109 method: 'POST', 110 headers: { 111 'Content-Type': 'application/json', 112 }, 113 body: JSON.stringify(payload), 114 keepalive: true 115 }); 116 117 return await response.json(); 118 } catch (error) { 119 console.warn('[Tracker] Failed to send:', error); 120 return null; 121 } 122 } 123 124 // 页é¢è®¿é®è¿½è¸ª 125 function trackPageView() { 126 const isNewVisitor = !localStorage.getItem('returning_visitor'); 127 if (!isNewVisitor) { 128 localStorage.setItem('returning_visitor', 'true'); 129 } 130 131 sendTrack('pageview', { 132 pagePath: window.location.pathname, 133 pageType: getPageType(), 134 referenceId: getReferenceId(), 135 referrer: document.referrer, 136 deviceType: getDeviceType(), 137 os: getOS(), 138 browser: getBrowser(), 139 screenWidth: window.screen.width, 140 screenHeight: window.screen.height, 141 language: navigator.language, 142 isNewVisitor: !localStorage.getItem('returning_visitor_marked') 143 }); 144 145 // æ 记为è访客 146 localStorage.setItem('returning_visitor_marked', 'true'); 147 } 148 149 // ============== 精确å¨çº¿æ¶é´ç»è®¡ ============== 150 // åºäºé¡µé¢å¯è§æ§API (Page Visibility API) 151 // è¡ä¸æ åï¼åªç»è®¡é¡µé¢å¯è§æ¶çæ´»è·æ¶é´ 152 153 let heartbeatTimer = null; 154 let pageStartTime = Date.now(); 155 let visibleStartTime = Date.now(); 156 let totalVisibleTime = 0; // 累计å¯è§æ¶é´ï¼æ¯«ç§ï¼ 157 let isPageVisible = !document.hidden; 158 let lastHeartbeatTime = Date.now(); 159 160 // æ´æ°å¯è§æ¶é´ 161 function updateVisibleTime() { 162 if (isPageVisible) { 163 const now = Date.now(); 164 totalVisibleTime += (now - visibleStartTime);
165 visibleStartTime = now; 166 } 167 } 168 169 // è·åæ»çæ´»è·æ¶é´ï¼ç§ï¼ 170 function getActiveDuration() { 171 updateVisibleTime(); 172 return Math.floor(totalVisibleTime / 1000); 173 } 174 175 // è·åæ»ç页é¢åçæ¶é´ï¼ç§ï¼- å æ¬ä¸å¯è§æ¶é´ 176 function getTotalDuration() { 177 return Math.floor((Date.now() - pageStartTime) / 1000); 178 } 179 180 function startHeartbeat() { 181 if (heartbeatTimer) return; 182 183 const sendHeartbeat = () => { 184 const now = Date.now(); 185 const timeSinceLastHeartbeat = Math.floor((now - lastHeartbeatTime) / 1000); 186 lastHeartbeatTime = now; 187 188 sendTrack('heartbeat', { 189 pagePath: window.location.pathname, 190 pageType: getPageType(), 191 referenceId: getReferenceId(), 192 duration: getTotalDuration(), // æ»åçæ¶é´ 193 activeDuration: getActiveDuration(), // æ´»è·æ¶é´ï¼å¯è§æ¶é´ï¼ 194 heartbeatInterval: timeSinceLastHeartbeat, 195 status: document.hidden ? 'inactive' : 'active' 196 }); 197 }; 198 199 // ç«å³åé䏿¬¡ 200 sendHeartbeat(); 201 202 // 宿åé 203 heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); 204 } 205 206 function stopHeartbeat() { 207 if (heartbeatTimer) { 208 clearInterval(heartbeatTimer); 209 heartbeatTimer = null; 210 } 211 } 212 213 // 页é¢å¯è§æ§ååå¤ç 214 function handleVisibilityChange() { 215 const wasVisible = isPageVisible; 216 isPageVisible = !document.hidden; 217 218 if (wasVisible && !isPageVisible) { 219 // 页é¢ä»å¯è§å为ä¸å¯è§ - 累计ä¹åçå¯è§æ¶é´ 220 updateVisibleTime(); 221 222 sendTrack('heartbeat', { 223 pagePath: window.location.pathname, 224 pageType: getPageType(), 225 referenceId: getReferenceId(), 226 duration: getTotalDuration(), 227 activeDuration: getActiveDuration(), 228 status: 'inactive', 229 event: 'visibility_hidden' 230 }); 231 } else if (!wasVisible && isPageVisible) { 232 // 页é¢ä»ä¸å¯è§å为å¯è§ - éç½®å¯è§å¼å§æ¶é´ 233 visibleStartTime = Date.now(); 234 235 sendTrack('heartbeat', { 236 pagePath: window.location.pathname, 237 pageType: getPageType(), 238 referenceId: getReferenceId(), 239 duration: getTotalDuration(), 240 activeDuration: getActiveDuration(), 241 status: 'active', 242 event: 'visibility_visible' 243 }); 244 } 245 } 246 247 // 页é¢ç¦»å¼è¿½è¸ª 248 function trackPageLeave() { 249 updateVisibleTime(); 250 251 sendTrack('leave', { 252 pagePath: window.location.pathname, 253 pageType: getPageType(), 254 referenceId: getReferenceId(), 255 duration: getTotalDuration(), 256 activeDuration: getActiveDuration() 257 }); 258 259 stopHeartbeat(); 260 } 261 262 // ç¹å»äºä»¶è¿½è¸ª 263 function trackClick(event) { 264 const target = event.target.closest('a, button, [data-track]'); 265 if (!target) return; 266 267 const trackData = target.dataset.track; 268 const href = target.href || ''; 269 const text = target.innerText?.substring(0, 50) || ''; 270 271 sendTrack('click', { 272 clickType: trackData || (target.tagName.toLowerCase()), 273 clickTarget: href || text, 274 pagePath: window.location.pathname 275 }); 276 } 277 278 // åå§å 279 function init() { 280 // 追踪页é¢è®¿é® 281 trackPageView(); 282 283 // å¯å¨å¿è·³ 284 startHeartbeat(); 285 286 // çå¬é¡µé¢å¯è§æ§åå - æ ¸å¿åè½ 287 document.addEventListener('visibilitychange', handleVisibilityChange); 288 289 // çå¬é¡µé¢ç¦ç¹åå - è¾ å©æ£æµ 290 window.addEventListener('focus', () => { 291 if (!isPageVisible) { 292 isPageVisible = true; 293 visibleStartTime = Date.now(); 294 } 295 }); 296 297 window.addEventListener('blur', () => { 298 if (isPageVisible) { 299 updateVisibleTime(); 300 isPageVisible = false;
301 } 302 }); 303 304 // çå¬é¡µé¢ç¦»å¼ 305 window.addEventListener('beforeunload', trackPageLeave); 306 window.addEventListener('pagehide', trackPageLeave); 307 308 // çå¬ç¹å»ï¼å¯éï¼ç¨äºæ´è¯¦ç»çè¡ä¸ºåæï¼ 309 // document.addEventListener('click', trackClick, { passive: true }); 310 311 console.log('[Tracker] Initialized v' + TRACKER_VERSION + ' with visibility tracking'); 312 } 313 314 // ç¡®ä¿DOMå è½½å®æ 315 if (document.readyState === 'loading') { 316 document.addEventListener('DOMContentLoaded', init); 317 } else { 318 init(); 319 } 320 321 // æ´é²å ¬å ±API 322 window.SiteTracker = { 323 trackEvent: function(eventName, data) { 324 sendTrack('event', { eventName, ...data }); 325 }, 326 trackClick: function(target, type) { 327 sendTrack('click', { clickTarget: target, clickType: type }); 328 }, 329 getActiveDuration: getActiveDuration, 330 getTotalDuration: getTotalDuration 331 }; 332 333})();
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.