[JavaScript] 纯文本查看 复制代码
'use strict';
/**
* 前端事件跟踪工具
* 功能:监听页面点击/触摸事件、收集事件数据、向指定接口发送统计信息
* 支持域名过滤、日志管理、请求重试等特性
*/
// ========================= 工具函数库 =========================
const Utils = {
/**
* 字符串解码(还原原混淆中的Base64+URI编码逻辑)
* @param {string} encodedStr - 编码字符串(可能含\x格式或Base64)
* @returns {string} 解码后的可读字符串
*/
decodeString(encodedStr) {
if (!encodedStr) return '';
// 处理\x格式编码
const hexDecoded = encodedStr.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) =>
String.fromCharCode(parseInt(hex, 16))
);
// 处理Base64编码(原混淆中隐性使用的编码方式)
const baseStr = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';
let baseDecoded = '';
let encoded = hexDecoded.replace(/\s+/g, '');
// 补全Base64长度
while (encoded.length % 4) encoded += '=';
for (let i = 0, len = encoded.length; i < len; i += 4) {
const c1 = baseStr.indexOf(encoded);
const c2 = baseStr.indexOf(encoded[i + 1]);
const c3 = baseStr.indexOf(encoded[i + 2]);
const c4 = baseStr.indexOf(encoded[i + 3]);
const bytes = (c1 << 18) | (c2 << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F);
baseDecoded += String.fromCharCode((bytes >> 16) & 0xFF);
if (c3 !== 64) baseDecoded += String.fromCharCode((bytes >> 8) & 0xFF);
if (c4 !== 64) baseDecoded += String.fromCharCode(bytes & 0xFF);
}
// URI解码(处理特殊字符)
return decodeURIComponent(escape(baseDecoded));
},
/**
* 生成唯一ID(用于事件ID、设备ID等)
* @param {number} length - ID长度,默认16位
* @returns {string} 唯一随机字符串
*/
generateUniqueId(length = 16) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';
for (let i = 0; i < length; i++) {
id += chars[Math.floor(Math.random() * chars.length)];
}
return id;
},
/**
* 验证URL是否匹配指定域名正则
* @param {RegExp} domainReg - 域名正则(如 /bdideal\.com|localhost/)
* @param {string} url - 待验证URL,默认当前页面URL
* @returns {boolean} 是否匹配
*/
isMatchDomain(domainReg, url = window.location.href) {
return domainReg.test(url);
},
/**
* 获取当前设备信息(简化版,原混淆中用于生成deviceId)
* @returns {string} 设备标识(基于UA的简化哈希)
*/
getDeviceId() {
const ua = navigator.userAgent;
// 简单哈希计算(避免敏感信息,仅用于标识设备)
let hash = 0;
for (let i = 0; i < ua.length; i++) {
hash = ((hash << 5) - hash) + ua.charCodeAt(i);
hash &= hash; // 转为32位整数
}
return `dev_${Math.abs(hash)}`;
}
};
// ========================= 错误处理类 =========================
class TrackerError extends Error {
/**
* 自定义跟踪错误类(还原原_0xbcf1e9类)
* @param {string} message - 错误信息
* @param {string} code - 错误码
*/
constructor(message, code = 'TRACK_ERROR') {
super(message);
this.name = 'TrackerError';
this.code = code;
this.timestamp = new Date().toISOString();
}
/**
* 格式化错误输出
* @returns {string} 格式化后的错误信息
*/
toString() {
return `[${this.timestamp}] [${this.code}] ${this.message}`;
}
}
// ========================= 核心事件跟踪类 =========================
class EventTracker {
/**
* 构造函数(初始化配置、日志容器、事件监听)
* @param {Object} options - 配置项
* @param {number} options.maxLogs - 最大日志缓存数,默认100
* @param {boolean} options.isDebug - 是否开启调试模式,默认false
* @param {string[]} options.targetClasses - 需跟踪的元素类名列表,默认['btn', 'card']
* @param {string} options.apiUrl - 数据上报接口,默认'/api/event-track'
* @param {number} options.retryMax - 请求重试次数,默认2
*/
constructor(options = {}) {
// 初始化默认配置
this.config = {
maxLogs: options.maxLogs || 100,
isDebug: options.isDebug || false,
targetClasses: options.targetClasses || ['btn', 'card'],
apiUrl: options.apiUrl || '/api/event-track',
retryMax: options.retryMax || 2,
domainReg: options.domainReg || /(bdideal\.com|127\.0\.0\.1|localhost)/ // 默认跟踪域名
};
// 初始化状态变量
this.logs = []; // 日志缓存容器
this.isTracking = false; // 跟踪开关
this.retryCount = 0; // 当前请求重试次数
this.deviceId = Utils.getDeviceId(); // 设备ID
this.sensorId = '-1'; // 传感器ID(原混淆默认值,可扩展)
// 初始化流程
this.init();
}
/**
* 初始化:开启跟踪、绑定事件监听、验证域名
*/
init() {
try {
// 验证当前域名是否在跟踪列表内
if (!Utils.isMatchDomain(this.config.domainReg)) {
if (this.config.isDebug) {
console.warn('当前域名不在跟踪列表内,停止初始化');
}
return;
}
// 开启跟踪状态
this.isTracking = true;
// 绑定页面事件监听
this.bindEvents();
// 调试日志
if (this.config.isDebug) {
console.log('EventTracker 初始化完成', {
deviceId: this.deviceId,
config: this.config
});
}
// 延迟执行初始化后任务(还原原setTimeout逻辑)
setTimeout(() => this.postInit(), 300);
} catch (err) {
throw new TrackerError(`初始化失败: ${err.message}`, 'INIT_FAILED');
}
}
/**
* 初始化后任务(扩展点,可添加额外初始化逻辑)
*/
postInit() {
// 示例:清除过期日志(可根据需求扩展)
this.clearExpiredLogs();
}
/**
* 绑定页面事件(点击+触摸)
*/
bindEvents() {
// 点击事件监听
window.addEventListener('click', (e) => this.handleEvent(e));
// 触摸事件监听(移动端)
window.addEventListener('touchstart', (e) => this.handleEvent(e, 'touch'));
if (this.config.isDebug) {
console.log('事件监听已绑定:click、touchstart');
}
}
/**
* 事件处理核心逻辑:过滤目标元素、收集数据、添加日志
* @param {Event} event - 原生事件对象
* @param {string} eventType - 事件类型(默认取event.type,可手动指定)
*/
handleEvent(event, eventType = '') {
if (!this.isTracking) return;
try {
const target = event.target;
const type = eventType || event.type;
// 过滤:仅跟踪指定类名的元素(可扩展更多过滤规则)
if (!this.isTargetElement(target)) {
if (this.config.isDebug) {
console.debug('非目标元素,跳过跟踪', { target });
}
return;
}
// 收集事件数据
const eventData = this.collectEventData(event, type);
// 添加到日志并尝试上报
this.addLog(eventData);
this.reportLogs(); // 实时上报(可改为批量上报)
} catch (err) {
console.error(new TrackerError(`事件处理失败: ${err.message}`, 'EVENT_HANDLE_FAILED'));
}
}
/**
* 验证元素是否为目标跟踪元素(基于类名)
* @param {HTMLElement} element - 待验证元素
* @returns {boolean} 是否为目标元素
*/
isTargetElement(element) {
if (!(element instanceof HTMLElement)) return false;
// 检查元素是否包含配置中的类名
return this.config.targetClasses.some(cls =>
element.classList.contains(cls)
);
}
/**
* 收集事件数据(核心:整理上报所需的所有字段)
* @param {Event} event - 原生事件对象
* @param {string} eventType - 事件类型(click/touchstart等)
* @returns {Object} 结构化事件数据
*/
collectEventData(event, eventType) {
const target = event.target;
const timestamp = new Date().toISOString();
let clientX = 0, clientY = 0;
// 处理坐标(兼容点击和触摸事件)
if (eventType === 'touch') {
const touch = event.touches[0] || event.changedTouches[0];
clientX = touch.clientX;
clientY = touch.clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
// 结构化数据(与原混淆中的字段对应,补充语义化命名)
return {
id: Utils.generateUniqueId(), // 事件唯一ID
className: target.className || '', // 目标元素类名
tagName: target.tagName.toLowerCase(), // 目标元素标签名
timestamp: timestamp, // 事件时间戳
eventType: eventType, // 事件类型
coordinates: { x: clientX, y: clientY }, // 坐标
clickId: Utils.generateUniqueId(8), // 点击ID(用于关联事件)
sensorId: this.sensorId, // 传感器ID(可扩展硬件信息)
deviceId: this.deviceId, // 设备ID
pageUrl: window.location.href, // 当前页面URL
referrer: document.referrer || '' // 来源页面
};
}
/**
* 添加日志到缓存(自动控制缓存大小)
* @param {Object} log - 单条事件日志
*/
addLog(log) {
// 超出最大缓存数时,删除最早的日志
if (this.logs.length >= this.config.maxLogs) {
this.logs.shift();
}
this.logs.push(log);
// 调试日志
if (this.config.isDebug) {
console.log('添加事件日志', { log, currentLogCount: this.logs.length });
}
}
/**
* 清除过期日志(可扩展时间阈值)
* @param {number} hours - 过期时间(小时),默认24小时
*/
clearExpiredLogs(hours = 24) {
const expireTime = new Date().getTime() - hours * 60 * 60 * 1000;
const beforeCount = this.logs.length;
this.logs = this.logs.filter(log =>
new Date(log.timestamp).getTime() >= expireTime
);
if (this.config.isDebug && this.logs.length !== beforeCount) {
console.log(`清除过期日志:${beforeCount - this.logs.length}条`);
}
}
/**
* 上报日志到接口(支持重试)
*/
async reportLogs() {
if (this.logs.length === 0) return;
if (!this.isTracking) throw new TrackerError('跟踪已停止,无法上报', 'TRACK_STOPPED');
try {
// 构造请求数据
const requestData = {
logs: [...this.logs], // 深拷贝当前日志(避免上报中修改)
deviceInfo: {
deviceId: this.deviceId,
userAgent: navigator.userAgent,
screen: `${window.screen.width}x${window.screen.height}`
},
reportTime: new Date().toISOString()
};
// 发送请求(使用fetch,可替换为axios等)
const response = await fetch(this.config.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tracker-Version': '1.0.0',
'X-Device-Id': this.deviceId
},
body: JSON.stringify(requestData),
credentials: 'include' // 携带Cookie(跨域时需配置CORS)
});
// 处理响应
if (!response.ok) {
throw new TrackerError(`接口返回错误: ${response.statusText}`, 'API_ERROR');
}
const result = await response.json();
if (result.code === 0) {
// 上报成功:清空日志、重置重试次数
this.logs = [];
this.retryCount = 0;
if (this.config.isDebug) {
console.log('日志上报成功', { result });
}
} else {
throw new TrackerError(`业务错误: ${result.message}`, 'BUSINESS_ERROR');
}
} catch (err) {
// 重试逻辑
if (this.retryCount < this.config.retryMax) {
this.retryCount++;
const delay = this.retryCount * 1000; // 指数退避:1s、2s、3s...
setTimeout(() => {
if (this.config.isDebug) {
console.log(`重试上报(第${this.retryCount}次),延迟${delay}ms`);
}
this.reportLogs();
}, delay);
} else {
// 重试次数用尽:抛出错误(可扩展本地存储等降级方案)
this.retryCount = 0;
throw new TrackerError(`上报失败(已重试${this.config.retryMax}次): ${err.message}`, 'RETRY_EXHAUSTED');
}
}
}
/**
* 停止跟踪:关闭开关、移除事件监听、清空日志
*/
stopTracking() {
this.isTracking = false;
// 移除事件监听(需使用具名函数,此处简化处理)
window.removeEventListener('click', (e) => this.handleEvent(e));
window.removeEventListener('touchstart', (e) => this.handleEvent(e, 'touch'));
this.clearLogs();
if (this.config.isDebug) {
console.log('EventTracker 已停止跟踪');
}
}
/**
* 清空所有日志
*/
clearLogs() {
this.logs = [];
if (this.config.isDebug) {
console.log('所有日志已清空');
}
}
/**
* 获取当前日志列表(调试用)
* @returns {Object[]} 日志列表
*/
getLogs() {
return [...this.logs]; // 返回拷贝,避免外部修改
}
}
// ========================= 初始化实例 =========================
/**
* 全局初始化:
* 1. 挂载到window.eventTracker,方便外部调用
* 2. 可根据需求修改配置(如apiUrl、domainReg等)
*/
(function initGlobalTracker() {
// 避免重复初始化
if (window.eventTracker) {
console.warn('EventTracker 已存在,无需重复初始化');
return;
}
// 初始化全局实例(可根据业务修改配置)
const globalTracker = new EventTracker({
maxLogs: 200,
isDebug: false, // 生产环境建议关闭
targetClasses: ['btn', 'card', 'nav-item', 'list-item'], // 需跟踪的元素类名
apiUrl: '/api/v1/event-track', // 实际项目替换为真实上报接口
retryMax: 3,
domainReg: /(bdideal\.com|127\.0\.0\.1|localhost|your-domain\.com)/ // 替换为你的业务域名
});
// 挂载到window,支持外部调用(如:window.eventTracker.stopTracking())
window.eventTracker = globalTracker;
// 调试提示
if (globalTracker.config.isDebug) {
console.log('全局EventTracker实例已挂载到 window.eventTracker');
}
})();