// 通用 JSBridge window.JSBridge = { getOS: function () { const ua = navigator.userAgent || navigator.vendor || window.opera; if (/android/i.test(ua)) return 'Android'; if (/iPad|iPhone|iPod/.test(ua) && !window.MSStream) return 'iOS'; return 'Web'; }, getUserId: function (callback) { // Android WebView if (window.Android && typeof window.Android.getUserId === 'function') { callback(window.Android.getUserId()); return ; } // iOS WebView if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getUserId) { window.webkit.messageHandlers.getUserId.postMessage(null); window.onUserIdReceived = callback; // iOS原生需回调window.onUserIdReceived(userId) return; } // Web环境(用 FingerprintJS 生成指纹) if (window.FingerprintJS) { FingerprintJS.load().then(fp => { fp.get().then(result => { callback(result.visitorId); }); }); } else { callback('unsupported'); } } }; // 工具函数 function getUrlParam(name) { var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)'); var r = window.location.search.substr(1).match(reg); if (r != null) return decodeURIComponent(r[2]); return null; } function getCookie(name) { const matches = document.cookie.match(new RegExp( `(?:^|; )${name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1')}=([^;]*)` )); return matches ? decodeURIComponent(matches[1]) : undefined; } function setCookie(name, value, minutes) { let expires = ""; if (minutes) { const date = new Date(); date.setTime(date.getTime() + (minutes * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`; } function deleteCookie(name) { document.cookie = `${name}=; Max-Age=-99999999; path=/`; } // 获取浏览器信息 function getBrowserInfo() { var ua = navigator.userAgent; var browser = 'Unknown'; var isMobile = /Mobile|Android|iPhone|iPad|iPod|HarmonyOS|HMS/i.test(ua); var osType = 'Unknown'; var osVersion = 'Unknown'; // 系统类型和版本号判断 if (/iPhone|iPad|iPod/i.test(ua)) { osType = 'iOS'; var iosVersionMatch = ua.match(/OS (\d+)[_.](\d+)?([_.](\d+))?/i); if (iosVersionMatch) { osVersion = iosVersionMatch[1]; if (iosVersionMatch[2]) osVersion += '.' + iosVersionMatch[2]; if (iosVersionMatch[4]) osVersion += '.' + iosVersionMatch[4]; } } else if (/Android/i.test(ua)) { osType = 'Android'; var androidVersionMatch = ua.match(/Android ([\d.]+)/i); if (androidVersionMatch) { osVersion = androidVersionMatch[1]; } } else if (/HarmonyOS|HMS/i.test(ua)) { osType = 'HarmonyOS'; var harmonyVersionMatch = ua.match(/HarmonyOS[\s/]?([\d.]+)/i) || ua.match(/HMSCore[\s/]?([\d.]+)/i); if (harmonyVersionMatch) { osVersion = harmonyVersionMatch[1]; } } // 浏览器类型判断 if (/Edg/i.test(ua)) { browser = 'Edge'; } else if (/HuaweiBrowser/i.test(ua) || /HMS/i.test(ua)) { browser = 'HuaweiBrowser'; } else if (/Chrome/i.test(ua)) { browser = 'Chrome'; } else if (/Firefox/i.test(ua)) { browser = 'Firefox'; } else if (/Safari/i.test(ua) && !/Chrome/i.test(ua) && !/Edg/i.test(ua)) { browser = 'Safari'; } else if (/MSIE|Trident/i.test(ua)) { browser = 'IE'; } // 特殊浏览器环境检测 if (/MicroMessenger/i.test(ua)) { browser = 'WeChat'; } else if (/QQBrowser/i.test(ua)) { browser = 'QQBrowser'; } else if (/UCBrowser/i.test(ua)) { browser = 'UCBrowser'; } else if (/Telegram/i.test(ua)) { browser = 'Telegram'; } return { isMobile: isMobile, browser: browser, userAgent: ua, osType: osType, osVersion: osVersion }; } // 主类 function UtmTracker() {} // 获取UTM参数 UtmTracker.prototype.getParams = function() { return { utm_source: getUrlParam('utm_source') || '', utm_medium: getUrlParam('utm_medium') || '', utm_campaign: getUrlParam('utm_campaign') || '', utm_term: getUrlParam('utm_term') || '', utm_content: getUrlParam('utm_content') || '', referrer: document.referrer || '', browser: getBrowserInfo(), timestamp: new Date().toISOString(), url: window.location.href }; }; // 静态方法:快速获取(无需实例化) UtmTracker.get = function() { return new UtmTracker().getParams(); }; class Tracker { initCofig = { baseUrl: '', heartbeatInterval: 30000, // 心跳间隔,默认30秒 visitCookieTimeout: 1, // 访问次数cookie有效期1分钟 maxVisitCount: 5, // 最大访问次数5次 blockCookieTimeout: 180, // 封锁cookie有效期180分钟 myEventSend: () => {}, // 自定义事件上报函数 beforeDestroy: () => {}, // 销毁前的回调函数 crashTime: 3, // 闪退阈值(几秒内关闭) idleTimeout: 300000, // 默认 5 分钟无操作视为离开 } hasInitialized = false; // 是否已初始化 initialize(options) { this.initCofig = { ...this.initCofig, ...options, } this.baseUrl = options.baseUrl || this.initCofig.baseUrl || 'https://your-default-tracking-api.com'; // 默认上报地址 this.timer = null; this.userId = 'user'; // 用户ID this.startTime = 0; this.heartbeatInterval = options.heartbeatInterval || this.initCofig.heartbeatInterval; // 默认30秒发送一次心跳 this.visitCookieTimeout = options.visitCookieTimeout || this.initCofig.visitCookieTimeout; // 默认访问次数cookie有效期1分钟 this.maxVisitCount = options.maxVisitCount || this.initCofig.maxVisitCount; // 默认最大访问次数5次 this.blockCookieTimeout = options.blockCookieTimeout || this.initCofig.blockCookieTimeout; // 默认封锁cookie有效期180分钟 this.myEventSend = options.myEventSend || this.initCofig.myEventSend; // 是否开启自定义事件上报 this.beforeDestroy = options.beforeDestroy || this.initCofig.beforeDestroy; // 销毁前的回调函数 this.crashTime = options.crashTime || this.initCofig.crashTime; // 闪退阈值(几秒内关闭页面被认为是闪退) this.idleTimeout = options.idleTimeout || this.initCofig.idleTimeout; // 默认 5 分钟无操作视为离开 this.sendDataDebounceTimers = {}; // 【2024-06-09】存储每种事件类型的定时器,用于事件级防抖 this.debounceTime = options.debounceTime || this.debounceTime || 300; // 【2024-06-09】防抖间隔支持初始化参数传入 window.JSBridge.getUserId((id) => { this.userId = id; }); this.headers = { 'Content-Type': 'application/json', heartbeatSeconds: this.heartbeatInterval, userId: this.userId, }; options.headers && Object.assign(this.headers, options.headers); } constructor(options = {}) { this.initialize(options); } init(options = {}) { if (this.hasInitialized) { console.warn('埋点已初始化,请勿重复调用'); return } this.initialize(options); const visitCookieName = 'userVisitCount'; const blockCookieName = 'userBlocked'; const isBlocked = getCookie(blockCookieName); const doneFN = () => { this.startTime = Date.now(); this.bindEvents(); this.startHeartbeat(); console.log(`埋点初始化成功,用户ID: ${this.userId}`); this.hasInitialized = true; } if (isBlocked) { // restoreBlock(); return `用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`; } let visitCount = getCookie(visitCookieName); if (visitCount) { visitCount = parseInt(visitCount, 10); if (visitCount >= this.maxVisitCount) { setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟 deleteCookie(visitCookieName); // restoreBlock(); console.warn(`用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`); } else { visitCount += 1; setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟 doneFN(); } } else { setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1 doneFN(); } } bindEvents() { window.addEventListener('beforeunload', this.handleBeforeUnload); window.addEventListener('click', this.handleClickEvent); document.addEventListener('visibilitychange', this.handleVisibilityChange); window.addEventListener('mousemove', this.resetIdleTimer.bind(this)); window.addEventListener('keydown', this.resetIdleTimer.bind(this)); this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报 } startHeartbeat() { this.timer = setInterval(() => { this.sendData({ eventType: 'heartbeat', duration: Math.round((Date.now() - this.startTime) / 1000), }); }, this.heartbeatInterval); } // 页面初始化调用,手动在不同框架的生命周期中调用 markContentLoaded() { this.sendData({ eventType: 'content_loaded', loadTime: Math.round((Date.now() - this.startTime) / 1000), }); } // 用户无操作时发送不活跃事件 resetIdleTimer() { if (this.idleTimer) { clearTimeout(this.idleTimer); } this.idleTimer = setTimeout(() => { this.sendData({ eventType: 'user_inactive', duration: Math.round((Date.now() - this.startTime) / 1000), }); }, this.idleTimeout); } handleBeforeUnload = (e) => { const duration = Math.round((Date.now() - this.startTime) / 1000); const data = { eventType: 'page_close', duration, exitType: duration > this.crashTime ? 'crash' : 'normal', }; const payload = JSON.stringify(data); if (navigator.sendBeacon) { navigator.sendBeacon(this.baseUrl, payload); } else { this.sendData(payload); } }; handleClickEvent = (event) => { const target = event.target; if (target.matches('[data-track]')) { const trackInfo = { eventType: target.getAttribute('event-type') || 'button_click', elementId: target.id || '无ID', elementClass: target.className || '无class', text: target.innerText || target.textContent || '', timestamp: new Date().toISOString(), }; this.sendData(trackInfo); } }; handleVisibilityChange = () => { if (document.hidden) { // 用户切换到其他 tab this.sendData({ eventType: 'tab_inactive' }); } else { // 用户回到当前 tab this.sendData({ eventType: 'tab_active' }); } }; queueIdleTask(task, timeout = 1000) { if (typeof requestIdleCallback === 'function') { requestIdleCallback(() => { task(); }, { timeout }); } else { // 兜底方案:使用 setTimeout 模拟 setTimeout(task, 0); } } sendData(data, headers = {}) { const eventType = data && data.eventType; // 【2024-06-09】只对有 eventType 且不是系统事件的做防抖,系统事件立即上报 const systemEvents = [ 'heartbeat', 'tab_active', 'tab_inactive', 'page_close', 'content_loaded', 'user_inactive', 'tracker_destroyed' ]; if (eventType && !systemEvents.includes(eventType)) { // 【2024-06-09】每种事件类型单独防抖,互不影响 if (this.sendDataDebounceTimers[eventType]) { clearTimeout(this.sendDataDebounceTimers[eventType]); } this.sendDataDebounceTimers[eventType] = setTimeout(() => { this._sendDataNow(data, headers); this.sendDataDebounceTimers[eventType] = null; }, this.debounceTime); } else { this._sendDataNow(data, headers); } } _sendDataNow(data, headers = {}) { this.queueIdleTask(() => { const UTMP = UtmTracker.get(); const params = { ...data, ...UTMP, ...UTMP.browser, }; console.log('上报埋点数据:', params); fetch(`${this.baseUrl}`, { method: 'POST', headers: this.headers, body: JSON.stringify(params) }).catch(err => { console.error('埋点失败:', err); }); }); } destroy() { window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('click', this.handleClickEvent); document.removeEventListener('visibilitychange', this.handleVisibilityChange); window.removeEventListener('mousemove', this.resetIdleTimer); window.removeEventListener('keydown', this.resetIdleTimer); this.beforeDestroy(); if (this.timer) { clearInterval(this.timer); } this.sendData({ eventType: 'tracker_destroyed', duration: Math.round((Date.now() - this.startTime) / 1000), }); this.hasInitialized = false; return '销毁埋点成功' } } // 使用示例: // const tracker = new Tracker({ baseUrl: 'https://your-tracking-api.com' }); // tracker.init(); // 暴露给全局或者作为模块导出 window.Tracker = Tracker;