PageSourceSearch

https://xoigac.top/js/tracker.js

js xoigac.top collected 2026-10-10 02:22:03 UTC 11,241 bytes, 333 lines download raw bytes

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.