data-tracker-uni
Version:
非通用埋点追踪器,仅适用于 uni-app 的app前端项目。
527 lines (488 loc) • 15.6 kB
JavaScript
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;