123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- // 通用 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 分钟无操作视为离开
- debounceTime: 300, // 防抖间隔,默认300毫秒
- }
- 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;
|