|
@@ -11,7 +11,7 @@ window.JSBridge = {
|
|
// Android WebView
|
|
// Android WebView
|
|
if (window.Android && typeof window.Android.getUserId === 'function') {
|
|
if (window.Android && typeof window.Android.getUserId === 'function') {
|
|
callback(window.Android.getUserId());
|
|
callback(window.Android.getUserId());
|
|
- return;
|
|
|
|
|
|
+ return ;
|
|
}
|
|
}
|
|
// iOS WebView
|
|
// iOS WebView
|
|
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getUserId) {
|
|
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.getUserId) {
|
|
@@ -32,28 +32,173 @@ window.JSBridge = {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+// 工具函数
|
|
|
|
+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];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
-let userId = '';
|
|
|
|
-JSBridge.getUserId(function (id) {
|
|
|
|
- userId = id;
|
|
|
|
-});
|
|
|
|
-class Tracker {
|
|
|
|
|
|
+ // 浏览器类型判断
|
|
|
|
+ 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';
|
|
|
|
+ }
|
|
|
|
|
|
- constructor(options = {}) {
|
|
|
|
- this.baseUrl = options.baseUrl || '';
|
|
|
|
|
|
+ // 特殊浏览器环境检测
|
|
|
|
+ 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.timer = null;
|
|
|
|
+ this.userId = 'user'; // 用户ID
|
|
this.startTime = 0;
|
|
this.startTime = 0;
|
|
- this.heartbeatInterval = options.heartbeatInterval || 30000; // 默认30秒发送一次心跳
|
|
|
|
- this.visitCookieTimeout = options.visitCookieTimeout || 1; // 默认访问次数cookie有效期1分钟
|
|
|
|
- this.maxVisitCount = options.maxVisitCount || 5; // 默认最大访问次数5次
|
|
|
|
- this.blockCookieTimeout = options.blockCookieTimeout || 180; // 默认封锁cookie有效期180分钟
|
|
|
|
- this.myEventSend = options.myEventSend || (() => {}); // 是否开启自定义事件上报
|
|
|
|
- this.beforeDestroy = options.beforeDestroy || (() => {}); // 销毁前的回调函数
|
|
|
|
- this.crashTime = options.crashTime || 3; // 闪退阈值
|
|
|
|
- this.idleTimeout = options.idleTimeout || 300000; // 默认 5 分钟无操作视为离开
|
|
|
|
|
|
+ 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 分钟无操作视为离开
|
|
|
|
+
|
|
|
|
+ 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() {
|
|
|
|
|
|
+ init(options = {}) {
|
|
|
|
+ if (this.hasInitialized) {
|
|
|
|
+ console.warn('埋点已初始化,请勿重复调用');
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ this.initialize(options);
|
|
const visitCookieName = 'userVisitCount';
|
|
const visitCookieName = 'userVisitCount';
|
|
const blockCookieName = 'userBlocked';
|
|
const blockCookieName = 'userBlocked';
|
|
const isBlocked = getCookie(blockCookieName);
|
|
const isBlocked = getCookie(blockCookieName);
|
|
@@ -61,10 +206,12 @@ class Tracker {
|
|
this.startTime = Date.now();
|
|
this.startTime = Date.now();
|
|
this.bindEvents();
|
|
this.bindEvents();
|
|
this.startHeartbeat();
|
|
this.startHeartbeat();
|
|
|
|
+ console.log(`埋点初始化成功,用户ID: ${this.userId}`);
|
|
|
|
+ this.hasInitialized = true;
|
|
}
|
|
}
|
|
if (isBlocked) {
|
|
if (isBlocked) {
|
|
// restoreBlock();
|
|
// restoreBlock();
|
|
- return;
|
|
|
|
|
|
+ return `用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`;
|
|
}
|
|
}
|
|
|
|
|
|
let visitCount = getCookie(visitCookieName);
|
|
let visitCount = getCookie(visitCookieName);
|
|
@@ -74,6 +221,7 @@ class Tracker {
|
|
setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
|
|
setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
|
|
deleteCookie(visitCookieName);
|
|
deleteCookie(visitCookieName);
|
|
// restoreBlock();
|
|
// restoreBlock();
|
|
|
|
+ console.warn(`用户访问次数超过限制(${this.maxVisitCount}次),已封锁 ${this.blockCookieTimeout} 分钟`);
|
|
} else {
|
|
} else {
|
|
visitCount += 1;
|
|
visitCount += 1;
|
|
setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
|
|
setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
|
|
@@ -89,19 +237,17 @@ class Tracker {
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
|
window.addEventListener('click', this.handleClickEvent);
|
|
window.addEventListener('click', this.handleClickEvent);
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
- window.addEventListener('mousemove', this.resetIdleTimer);
|
|
|
|
- window.addEventListener('keydown', this.resetIdleTimer);
|
|
|
|
|
|
+ window.addEventListener('mousemove', this.resetIdleTimer.bind(this));
|
|
|
|
+ window.addEventListener('keydown', this.resetIdleTimer.bind(this));
|
|
this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
|
|
this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
|
|
}
|
|
}
|
|
|
|
|
|
startHeartbeat() {
|
|
startHeartbeat() {
|
|
this.timer = setInterval(() => {
|
|
this.timer = setInterval(() => {
|
|
- console.log(userId);
|
|
|
|
-
|
|
|
|
this.sendData({
|
|
this.sendData({
|
|
eventType: 'heartbeat',
|
|
eventType: 'heartbeat',
|
|
duration: Math.round((Date.now() - this.startTime) / 1000),
|
|
duration: Math.round((Date.now() - this.startTime) / 1000),
|
|
- }, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
|
|
|
|
|
|
+ });
|
|
}, this.heartbeatInterval);
|
|
}, this.heartbeatInterval);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -137,7 +283,7 @@ class Tracker {
|
|
if (navigator.sendBeacon) {
|
|
if (navigator.sendBeacon) {
|
|
navigator.sendBeacon(this.baseUrl, payload);
|
|
navigator.sendBeacon(this.baseUrl, payload);
|
|
} else {
|
|
} else {
|
|
- this.sendData(payload, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
|
|
|
|
|
|
+ this.sendData(payload);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
@@ -151,7 +297,7 @@ class Tracker {
|
|
text: target.innerText || target.textContent || '',
|
|
text: target.innerText || target.textContent || '',
|
|
timestamp: new Date().toISOString(),
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
};
|
|
- this.sendData(trackInfo, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
|
|
|
|
|
|
+ this.sendData(trackInfo);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
@@ -176,11 +322,16 @@ class Tracker {
|
|
}
|
|
}
|
|
sendData(data, headers = {}) {
|
|
sendData(data, headers = {}) {
|
|
this.queueIdleTask(() => {
|
|
this.queueIdleTask(() => {
|
|
- console.log('上报埋点数据:', data);
|
|
|
|
|
|
+ const params = {
|
|
|
|
+ ...data,
|
|
|
|
+ ...UtmTracker.get()
|
|
|
|
+ };
|
|
|
|
+ params.browser = params.browser.browser || 'Unknown';
|
|
|
|
+ console.log('上报埋点数据:', params);
|
|
fetch(`${this.baseUrl}`, {
|
|
fetch(`${this.baseUrl}`, {
|
|
method: 'POST',
|
|
method: 'POST',
|
|
- headers: { 'Content-Type': 'application/json', ...headers },
|
|
|
|
- body: JSON.stringify(data)
|
|
|
|
|
|
+ headers: this.headers,
|
|
|
|
+ body: JSON.stringify(params)
|
|
}).catch(err => {
|
|
}).catch(err => {
|
|
console.error('埋点失败:', err);
|
|
console.error('埋点失败:', err);
|
|
});
|
|
});
|
|
@@ -200,7 +351,9 @@ class Tracker {
|
|
this.sendData({
|
|
this.sendData({
|
|
eventType: 'tracker_destroyed',
|
|
eventType: 'tracker_destroyed',
|
|
duration: Math.round((Date.now() - this.startTime) / 1000),
|
|
duration: Math.round((Date.now() - this.startTime) / 1000),
|
|
- }, { heartbeatSeconds: this.heartbeatInterval, userId: userId });
|
|
|
|
|
|
+ });
|
|
|
|
+ this.hasInitialized = false;
|
|
|
|
+ return '销毁埋点成功'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|