UNPKG

data-tracker-uni

Version:

非通用埋点追踪器,仅适用于 uni-app 的app前端项目。

527 lines (488 loc) 15.6 kB
import CryptoJS from "crypto-js"; // 埋点 class Tracker { /** * @param {Object} options * @param {Object} options.uni - uni 实例(由外部传入) * @param {string} options.serverUrl - 服务端地址 * @param {string} options.method - 上报方法 * @param {string} options.appSecret - appSecret * @param {Object} options.pagesJson - pages.json 配置 * @param {string} [options.appId] - 应用ID * @param {number} [batchDelay] - 批量上报延迟时间 */ constructor({ uni, serverUrl, method, appSecret, pagesJson, appId = "1001", batchDelay = 10000, }) { // 参数验证 if (!uni) throw new Error("必须传入 uni 实例"); if (!serverUrl || typeof serverUrl !== "string") throw new Error("必须传入有效的 serverUrl"); if (!method || typeof method !== "string") throw new Error("必须传入有效的 method"); if (!appSecret || typeof appSecret !== "string") throw new Error("必须传入有效的 appSecret"); if (!pagesJson || typeof pagesJson !== "object") throw new Error("必须传入有效的 pagesJson"); if (typeof appId !== "string") throw new Error("appId 必须是字符串类型"); this.uni = uni; // 配置项 this.config = { serverUrl: serverUrl.trim(), appId: appId.trim(), method: method.trim(), appSecret: appSecret.trim(), }; // 设备信息 this.deviceInfo = null; this.net = null; this.lat = 0; this.lng = 0; // 用户信息 this.user = null; // 埋点事件队列 this.eventQueue = []; // 批量发送定时器 this.batchTimer = null; // 批量发送延迟(ms) this.batchDelay = batchDelay; // 默认延迟10秒发送 // 添加存储监听 this.addStorageListener(); // 获取 pages.json里的TabBar 配置 this.tabBarList = pagesJson?.tabBar?.list; } // 创建标准化错误 createError(method, message) { throw new Error(`[Tracker] ${method}: ${message}`); } // 埋点系统 初始化 async init() { await Promise.all([ this.getDeviceInfo(), this.getUserInfo(), this.getlocation(), this.getNetType(), ]); console.log( "[Track] 埋点系统初始化完成", this.deviceInfo, this.net ); } getNetType() { this.uni.getNetworkType({ success: (res) => { console.log("[Track] 获取网络类型成功", res); // 可能的返回值: wifi/2g/3g/4g/5g/ethernet/unknown/none this.net = res.networkType; }, fail: (err) => { console.error("[Track] 获取网络类型失败:", err); this.net = "unknown"; }, }); } async getlocation() { // 获取用户位置 this.uni.getLocation({ type: "gcj02", geocode: true, success: (res) => { console.log("[Tracker] 获取定位getlocation res", res); this.lat = res.latitude; this.lng = res.longitude; }, fail: (err) => { console.log("[Tracker] 获取定位失败 err", err); this.lat = 0; this.lng = 0; }, }); } async getDeviceInfo() { const systemInfo = await this.uni.getSystemInfoSync(); console.log("[Tracker] 获取设备信息", systemInfo); let pt = systemInfo.uniPlatform || ""; let uuid = ""; console.log("[Tracker] 获取设备信息pt", pt); try { // 安全检查plus API是否可用 // #ifdef APP-PLUS if (pt === "app") { if (systemInfo.platform == "ios") { uuid = plus.device.uuid; } else if (systemInfo.platform == "android") { // 优先使用 OAID uuid = systemInfo.oaid && systemInfo.oaid.trim() !== "" ? systemInfo.oaid : plus.device.uuid; } } else { console.warn('[Tracker] plus API 不可用,无法获取设备唯一标识'); uuid = systemInfo.deviceId || ''; console.log("[Tracker] 获取设备信息uuid", uuid); } // #endif // console.log("[Tracker] 获取设备信息", systemInfo); this.deviceInfo = { deviceId: uuid || systemInfo.deviceId, osName: systemInfo.osName, brand: systemInfo.brand || "", md: systemInfo.model || "", pt, v: systemInfo.appVersion || "1.0.0", }; console.log("[Tracker] 设备信息获取成功:", this.deviceInfo); } catch (e) { console.error("[Tracker] getDeviceInfo获取设备信息失败:", e); this.deviceInfo = { deviceId: "", osName: "", brand: "", md: "", pt: "", v: "1.0.0", }; // this.createError("getDeviceInfo", e.message); } } async getUserInfo() { // 获取用户信息 const token = this.uni.getStorageSync("token"); // 如果有token, 从存储中获取用户信息 if (token) { const userInfo = this.uni.getStorageSync("userInfo") || {}; this.user = userInfo.mobile || ""; } else { // console.log("[Tracker] 用户未登录"); this.user = ""; } // console.log("[Tracker] 用户信息获取成功:", this.user); } // 添加本地存储监听用户信息 addStorageListener() { this.uni.$on("storageChange", (key) => { console.log("[Tracker] 监听到本地存储用户信息变化:", key); if (key === "token" || key === "userInfo") { this.getUserInfo(); } }); } // 获取当前页面完整路径 getCurrentFullPath() { const pages = getCurrentPages(); if (!pages.length) return ""; const currentPage = pages[pages.length - 1]; // 优先使用$page?.fullPath提供的完整路径(如果存在) if (currentPage.$page?.fullPath) { return currentPage.$page.fullPath; } // 手动构建 const route = currentPage.route; const options = currentPage.options || {}; // 构建查询字符串 const queryString = Object.keys(options) .filter((key) => !route.includes(`:${key}`)) // 排除路径参数 .map((key) => `${key}=${encodeURIComponent(options[key])}`) .join("&"); // 替换路径参数 let pathWithParams = `/${route}`; Object.keys(options).forEach((key) => { if (route.includes(`:${key}`)) { pathWithParams = pathWithParams.replace(`:${key}`, options[key]); } }); return `${pathWithParams}${queryString ? "?" + queryString : ""}`; } // 获取当前页面的路由 getCurrentPageRoute() { const pages = getCurrentPages(); if (!pages.length) return ""; const currentPage = pages[pages.length - 1]; const route = currentPage.route; return route; } // 获取tabBar的name getTabBarName() { let name = ""; const currentPageRoute = this.getCurrentPageRoute(); if (this.tabBarList && currentPageRoute) { // 查找匹配的 TabBar 项 const currentTab = this.tabBarList.find((item) => { // 注意:tabBar配置中的pagePath通常不带前面的'/' return item.pagePath === currentPageRoute; }); if (currentTab) { // console.log("当前TabBar路由:", currentTab.pagePath); // console.log("当前TabBar文本:", currentTab.text); name = currentTab.text; } else { // console.log("当前页面不是TabBar页面"); name = ""; } } return name; } // 清除定时器 clearBatchTimer() { if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } } // 销毁实例 destroy() { this.clearBatchTimer(); this.deviceInfo = null; this.net = null; this.lat = 0; this.lng = 0; this.user = null; this.eventQueue = []; } // ================ 核心埋点方法 ================ /** * 页面访问统计(包含访问用户统计) * @param {string} menuName 菜单名称 如 热门 */ trackPageView(menuName = "") { // 页面浏览埋点 const eventData = { data_type: "11", url: this.getCurrentFullPath() || "", ttc: this.getTabBarName() || "", ttct: menuName, prop: {} }; this.sendEvent({ data: eventData }); } /** * 视频点击统计 * @param {string} clickType 点击类型 * @param {string} menuName 菜单名称 如 热门 * @param {object} data 视频数据 */ trackVideoClick(clickType = "v_click", menuName = "", data) { if (!clickType) this.createError("trackVideoClick", "点击类型为必填"); if (!menuName || typeof menuName !== 'string') { this.createError("trackVideoClick", "必须传入有效的 menuName"); } if (!data || typeof data !== 'object') { this.createError("trackVideoClick", "视频数据为必填且必须是对象"); } const eventData = { data_type: "101", url: this.getCurrentFullPath() || "", ttc: this.getTabBarName() || "", ttct: menuName, prop: { e_n: clickType, e_p: data, } }; this.sendEvent({ data: eventData }); } // 搜索事件 trackSearch(keyword) { if (!keyword || typeof keyword !== 'string' || !keyword.trim()) { this.createError("trackSearch", "必须传入有效的搜索关键词"); } const eventData = { data_type: "101", url: this.getCurrentFullPath() || "", ttc: "", ttct: "", prop: { e_n: "search", e_p: keyword, // 搜索词 } }; this.sendEvent({ data: eventData }); } // 视频发布 trackVideoPublish(menuName = "", videoData) { if (!videoData || typeof videoData !== 'object') { this.createError("trackVideoPublish", "必须传入有效的视频数据videoData"); } const eventData = { data_type: "101", url: this.getCurrentFullPath() || "", ttc: "", ttct: menuName, prop: { e_n: "pub_r", e_p: videoData, } }; this.sendEvent({ data: eventData }); } // 用户注册 trackUserRegister(data) { // console.log("[Tracker]用户注册", data); if (!data || typeof data !== 'object') { this.createError("trackUserRegister", "必须传入有效的用户data"); } const eventData = { data_type: "101", url: this.getCurrentFullPath() || "", ttc: "", ttct: "", prop: { e_n: "register", e_p: data, } }; this.sendEvent({ data: eventData }); } // 应用启动 trackAppStart() { // console.log("[Tracker]应用启动"); const eventData = { data_type: "1", url: this.getCurrentFullPath() || "", ttc: this.getTabBarName() || "", ttct: "", prop: { net: this.net, md: this.deviceInfo.md, brand: this.deviceInfo.brand, lat: this.lat, lng: this.lng, } }; this.sendEvent({ data: eventData }); } // ================ 数据发送 ================ // 生成请求ID generateRequestId() { return Date.now().toString(); } // 生成HmacSHA256签名 generateSign(signStr) { try { const signature = CryptoJS.HmacSHA256(signStr, this.config.appSecret); // 转换为Base64编码 const base64Signature = CryptoJS.enc.Base64.stringify(signature); if (!base64Signature) { throw new Error("[Tracker] 签名生成失败"); } return base64Signature; } catch (error) { console.error("[Tracker] 生成签名失败:", error); throw error; } } // 构建请求参数 buildRequestParams(events) { try { // 检查设备信息是否已初始化 if (!this.deviceInfo) { throw new Error("[Tracker] 设备信息未初始化,请先调用 init() 方法"); } // console.log('[Tracker] 开始构建请求参数...'); const timestamp = Date.now(); console.log("[Tracker] ++++++++创建时间:", timestamp); // 业务内容参数数组 const contentItems = events.map((event) => ({ timestamp, user: this.user, deviceId: this.deviceInfo.deviceId || "", osName: this.deviceInfo.osName || "", pt: this.deviceInfo.pt || "", v: this.deviceInfo.v || "1.0.0", ...(event.data || {}), })); // console.log('[Tracker] 业务内容参数:', JSON.stringify(contentItems)); // 构建请求参数 const params = { api_request_id: this.generateRequestId(), app_id: this.config.appId, timestamp, content: JSON.stringify(contentItems), method: this.config.method, }; // console.log('[Tracker] 基础请求参数:', JSON.stringify(params)); // 生成签名 const sortedKeys = Object.keys(params).sort(); let signStr = sortedKeys .filter((key) => key !== "sign") // 过滤掉sign参数 .map((key) => `${key}=${params[key]}`) .join("&"); params.sign = this.generateSign(signStr); return params; } catch (error) { console.error("[Tracker] 构建请求参数失败:", error); throw error; } } // 发送埋点数据 - 添加到队列 sendEvent(event) { this.eventQueue.push(event); this.scheduleBatchSend(); } // 调度批量发送 scheduleBatchSend() { if (!this.batchTimer) { this.batchTimer = setTimeout(() => { this.sendBatchEvents(); this.batchTimer = null; }, this.batchDelay); } } // 批量发送事件 sendBatchEvents() { if (this.eventQueue.length === 0) return; // 取出当前队列中的所有事件 const events = [...this.eventQueue]; this.eventQueue = []; try { const params = this.buildRequestParams(events); this.sendSingleEvent(params); } catch (error) { console.error("[Tracker] 构建埋点参数失败:", error); // 构建参数失败时,将事件重新加入队列 this.eventQueue = [...this.eventQueue, ...events]; } } // 发送单个事件请求 sendSingleEvent(params) { try { if (!params || typeof params !== "object") { throw new Error("请求参数无效"); } this.uni.request({ url: this.config.serverUrl, method: "POST", data: params, header: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, timeout: 10000, // 10秒超时 success: (res) => { console.log("[Tracker] 请求发送成功,响应:", res); if (!res.data?.status) { console.error("[Tracker] 请求返回status:false:", res); } }, fail: (err) => { console.error("[Tracker] 请求发送失败:", err); // 可以在这里添加重试逻辑 }, complete: () => { console.log("[Tracker] ====== 埋点请求完成 ======", Date.now()); }, }); } catch (error) { console.error("[Tracker] 发送请求时发生错误:", error); } } } export default Tracker;