|
@@ -9,43 +9,48 @@ class Tracker {
|
|
this.blockCookieTimeout = options.blockCookieTimeout || 180; // 默认封锁cookie有效期180分钟
|
|
this.blockCookieTimeout = options.blockCookieTimeout || 180; // 默认封锁cookie有效期180分钟
|
|
this.myEventSend = options.myEventSend || (() => {}); // 是否开启自定义事件上报
|
|
this.myEventSend = options.myEventSend || (() => {}); // 是否开启自定义事件上报
|
|
this.beforeDestroy = options.beforeDestroy || (() => {}); // 销毁前的回调函数
|
|
this.beforeDestroy = options.beforeDestroy || (() => {}); // 销毁前的回调函数
|
|
|
|
+ this.crashTime = options.crashTime || 3; // 闪退阈值
|
|
|
|
+ this.idleTimeout = options.idleTimeout || 300000; // 默认 5 分钟无操作视为离开
|
|
}
|
|
}
|
|
|
|
|
|
-init() {
|
|
|
|
- const visitCookieName = 'userVisitCount';
|
|
|
|
- const blockCookieName = 'userBlocked';
|
|
|
|
- const isBlocked = getCookie(blockCookieName);
|
|
|
|
- const doneFN = () => {
|
|
|
|
- this.startTime = Date.now();
|
|
|
|
- this.bindEvents();
|
|
|
|
- this.startHeartbeat();
|
|
|
|
- }
|
|
|
|
- if (isBlocked) {
|
|
|
|
- // restoreBlock();
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- let visitCount = getCookie(visitCookieName);
|
|
|
|
- if (visitCount) {
|
|
|
|
- visitCount = parseInt(visitCount, 10);
|
|
|
|
- if (visitCount >= this.maxVisitCount) {
|
|
|
|
- setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
|
|
|
|
- deleteCookie(visitCookieName);
|
|
|
|
|
|
+ init() {
|
|
|
|
+ const visitCookieName = 'userVisitCount';
|
|
|
|
+ const blockCookieName = 'userBlocked';
|
|
|
|
+ const isBlocked = getCookie(blockCookieName);
|
|
|
|
+ const doneFN = () => {
|
|
|
|
+ this.startTime = Date.now();
|
|
|
|
+ this.bindEvents();
|
|
|
|
+ this.startHeartbeat();
|
|
|
|
+ }
|
|
|
|
+ if (isBlocked) {
|
|
// restoreBlock();
|
|
// restoreBlock();
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let visitCount = getCookie(visitCookieName);
|
|
|
|
+ if (visitCount) {
|
|
|
|
+ visitCount = parseInt(visitCount, 10);
|
|
|
|
+ if (visitCount >= this.maxVisitCount) {
|
|
|
|
+ setCookie(blockCookieName, 'true', this.blockCookieTimeout); // 封锁用户 180 分钟
|
|
|
|
+ deleteCookie(visitCookieName);
|
|
|
|
+ // restoreBlock();
|
|
|
|
+ } else {
|
|
|
|
+ visitCount += 1;
|
|
|
|
+ setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
|
|
|
|
+ doneFN();
|
|
|
|
+ }
|
|
} else {
|
|
} else {
|
|
- visitCount += 1;
|
|
|
|
- setCookie(visitCookieName, visitCount, this.visitCookieTimeout); // 每次访问 +1,有效期 1 分钟
|
|
|
|
|
|
+ setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1
|
|
doneFN();
|
|
doneFN();
|
|
}
|
|
}
|
|
- } else {
|
|
|
|
- setCookie(visitCookieName, '1', this.visitCookieTimeout); // 首次访问,设置访问次数为 1
|
|
|
|
- doneFN();
|
|
|
|
}
|
|
}
|
|
-}
|
|
|
|
|
|
|
|
bindEvents() {
|
|
bindEvents() {
|
|
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);
|
|
|
|
+ window.addEventListener('mousemove', this.resetIdleTimer);
|
|
|
|
+ window.addEventListener('keydown', this.resetIdleTimer);
|
|
this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
|
|
this.myEventSend(this.sendData.bind(this)); // 绑定自定义事件上报
|
|
}
|
|
}
|
|
|
|
|
|
@@ -58,10 +63,33 @@ init() {
|
|
}, this.heartbeatInterval);
|
|
}, 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) => {
|
|
handleBeforeUnload = (e) => {
|
|
|
|
+ const duration = Math.round((Date.now() - this.startTime) / 1000);
|
|
const data = {
|
|
const data = {
|
|
eventType: 'page_close',
|
|
eventType: 'page_close',
|
|
- duration: Math.round((Date.now() - this.startTime) / 1000),
|
|
|
|
|
|
+ duration,
|
|
|
|
+ exitType: duration > this.crashTime ? 'crash' : 'normal',
|
|
};
|
|
};
|
|
const payload = JSON.stringify(data);
|
|
const payload = JSON.stringify(data);
|
|
if (navigator.sendBeacon) {
|
|
if (navigator.sendBeacon) {
|
|
@@ -85,14 +113,44 @@ init() {
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ 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 = {}) {
|
|
sendData(data, headers = {}) {
|
|
- console.log('上报埋点数据:', data);
|
|
|
|
- fetch(`${this.baseUrl}`, { method: 'POST', headers: { 'Content-Type': 'application/json' ,...headers}, body: JSON.stringify(data) });
|
|
|
|
|
|
+ this.queueIdleTask(() => {
|
|
|
|
+ console.log('上报埋点数据:', data);
|
|
|
|
+ fetch(`${this.baseUrl}`, {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ headers: { 'Content-Type': 'application/json', ...headers },
|
|
|
|
+ body: JSON.stringify(data)
|
|
|
|
+ }).catch(err => {
|
|
|
|
+ console.error('埋点失败:', err);
|
|
|
|
+ });
|
|
|
|
+ });
|
|
}
|
|
}
|
|
|
|
|
|
destroy() {
|
|
destroy() {
|
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
|
window.removeEventListener('click', this.handleClickEvent);
|
|
window.removeEventListener('click', this.handleClickEvent);
|
|
|
|
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
|
|
+ window.removeEventListener('mousemove', this.resetIdleTimer);
|
|
|
|
+ window.removeEventListener('keydown', this.resetIdleTimer);
|
|
this.beforeDestroy();
|
|
this.beforeDestroy();
|
|
if (this.timer) {
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
clearInterval(this.timer);
|