trackersdk2 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/TrackerSDK.d.ts +123 -0
- package/dist/index.cjs.js +588 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.esm.js +582 -0
- package/dist/index.js +592 -0
- package/dist/types.d.ts +21 -0
- package/package.json +32 -0
- package/rollup.config.js +43 -0
- package/src/core/TrackerSDK.ts +561 -0
- package/src/core/index.ts +16 -0
- package/src/core/types.ts +29 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import type { TrackerConfig, TrackEvent, TagNames } from "./types";
|
|
2
|
+
import { onFCP, onLCP, onCLS, onTTFB } from "web-vitals";
|
|
3
|
+
|
|
4
|
+
export class TrackerSDK {
|
|
5
|
+
private config: Required<TrackerConfig>;
|
|
6
|
+
private autoTrackClickHandler: null | ((e: MouseEvent) => void) = null;
|
|
7
|
+
// 事件队列
|
|
8
|
+
private eventQueue: TrackEvent[] = [];
|
|
9
|
+
// 批量上报定时器
|
|
10
|
+
private batchTimer: null | number = null;
|
|
11
|
+
// 页面卸载前是否已发送数据标识
|
|
12
|
+
private hasSentBeforeUnload: boolean = false;
|
|
13
|
+
// #region 构造函数 & 初始化
|
|
14
|
+
constructor() {
|
|
15
|
+
this.config = {
|
|
16
|
+
appId: "",
|
|
17
|
+
serverUrl: "",
|
|
18
|
+
userId: "",
|
|
19
|
+
debug: true,
|
|
20
|
+
enableAutoClickTagNameList: [],
|
|
21
|
+
autoTrack: false,
|
|
22
|
+
batchSize: 10,
|
|
23
|
+
batchInterval: 5000,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
init(config: TrackerConfig): void {
|
|
27
|
+
if (!window || typeof window !== "object") {
|
|
28
|
+
console.warn("TrackerSDK 仅支持浏览器环境,当前环境已跳过初始化");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!config.serverUrl) {
|
|
32
|
+
throw new Error("serverUrl is required");
|
|
33
|
+
}
|
|
34
|
+
if (!config.appId) {
|
|
35
|
+
throw new Error("appId is required");
|
|
36
|
+
}
|
|
37
|
+
this.config = {
|
|
38
|
+
...this.config,
|
|
39
|
+
...config,
|
|
40
|
+
};
|
|
41
|
+
this.setupErrorTracking();
|
|
42
|
+
this.setupPerformanceTracking();
|
|
43
|
+
this.trackUV();
|
|
44
|
+
// 监听页面卸载
|
|
45
|
+
this.setupPageUnloadHandler();
|
|
46
|
+
// 重试之前失败的批量上报
|
|
47
|
+
this.retryFailedBatches();
|
|
48
|
+
}
|
|
49
|
+
// #endregion
|
|
50
|
+
// #region 埋点api工具方法
|
|
51
|
+
/**
|
|
52
|
+
* @description 重试批量上报之前上报失败的数据
|
|
53
|
+
* @returns Promise<void>
|
|
54
|
+
*/
|
|
55
|
+
private async retryFailedBatches(): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
const key = "tracker_failed_batches";
|
|
58
|
+
const batchesStr = localStorage.getItem(key);
|
|
59
|
+
if (!batchesStr) return;
|
|
60
|
+
const batches: any[] = JSON.parse(batchesStr);
|
|
61
|
+
const remainingBatches: any[] = [];
|
|
62
|
+
for (const batch of batches) {
|
|
63
|
+
try {
|
|
64
|
+
await this.fallbackSendBatch(batch);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
remainingBatches.push(batch);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (remainingBatches.length) {
|
|
70
|
+
// 只保留3批
|
|
71
|
+
const toSave = remainingBatches.splice(-3);
|
|
72
|
+
localStorage.setItem(key, JSON.stringify(toSave));
|
|
73
|
+
} else {
|
|
74
|
+
localStorage.removeItem(key);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.warn("Failed to retry batches:", err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* @description 监听页面卸载事件,确保数据上报
|
|
82
|
+
* @returns void
|
|
83
|
+
*/
|
|
84
|
+
private setupPageUnloadHandler(): void {
|
|
85
|
+
const flushAndSend = () => {
|
|
86
|
+
if (this.hasSentBeforeUnload || this.eventQueue.length == 0) return;
|
|
87
|
+
// 清除定时器,防止内存泄漏
|
|
88
|
+
if (this.batchTimer) {
|
|
89
|
+
clearTimeout(this.batchTimer);
|
|
90
|
+
this.batchTimer = null;
|
|
91
|
+
}
|
|
92
|
+
// 标记为已上报
|
|
93
|
+
this.hasSentBeforeUnload = true;
|
|
94
|
+
const payload = { events: [...this.eventQueue] };
|
|
95
|
+
// 清空队列
|
|
96
|
+
this.eventQueue = [];
|
|
97
|
+
// this.sendBatch(payload.events);
|
|
98
|
+
if (navigator.sendBeacon) {
|
|
99
|
+
const blob = new Blob([JSON.stringify(payload)], {
|
|
100
|
+
type: "application/json",
|
|
101
|
+
});
|
|
102
|
+
if (!navigator.sendBeacon(this.config.serverUrl, blob)) {
|
|
103
|
+
this.fallbackSendBatch(payload);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
this.fallbackSendBatch(payload);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
// 页面级别卸载事件
|
|
110
|
+
// beforeunload safari不支持
|
|
111
|
+
window.addEventListener("beforeunload", flushAndSend);
|
|
112
|
+
// pagehide 兜底
|
|
113
|
+
window.addEventListener("pagehide", flushAndSend);
|
|
114
|
+
// 可选:页面最小化、切换页签时触发
|
|
115
|
+
// window.addEventListener("visibilitychange", () => {
|
|
116
|
+
// if (document.hidden) flushAndSend();
|
|
117
|
+
// });
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* @description 获取设备id
|
|
121
|
+
*/
|
|
122
|
+
private getDeviceId(): string {
|
|
123
|
+
let id = localStorage.getItem("tracker_device_id");
|
|
124
|
+
if (!id) {
|
|
125
|
+
id =
|
|
126
|
+
"device_" +
|
|
127
|
+
Date.now() +
|
|
128
|
+
"_" +
|
|
129
|
+
Math.random().toString(32).substring(2, 10);
|
|
130
|
+
localStorage.setItem("tracker_device_id", id);
|
|
131
|
+
}
|
|
132
|
+
return id;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* @description navigator.sendBeacon 和 fetch 双通道实现数据上报
|
|
136
|
+
* @param data 上报的负载
|
|
137
|
+
*/
|
|
138
|
+
private send(data: TrackEvent): void {
|
|
139
|
+
const payload = {
|
|
140
|
+
...data,
|
|
141
|
+
userId: this.config.userId || this.getDeviceId(),
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
url: window.location.href,
|
|
144
|
+
title: document.title,
|
|
145
|
+
userAgent: navigator.userAgent,
|
|
146
|
+
appId: this.config.appId,
|
|
147
|
+
};
|
|
148
|
+
// 错误事件立即上报
|
|
149
|
+
if (data.eventType == "error") return this.sendImmediately(payload);
|
|
150
|
+
this.enqueue(payload);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* @description 入队列
|
|
154
|
+
* @param payload 上报的负载
|
|
155
|
+
*/
|
|
156
|
+
private enqueue(payload: any): void {
|
|
157
|
+
this.eventQueue.push(payload);
|
|
158
|
+
// 达到数量阈值,立即上报
|
|
159
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
160
|
+
return this.flush();
|
|
161
|
+
}
|
|
162
|
+
// 启动定时器
|
|
163
|
+
if (!this.batchTimer) {
|
|
164
|
+
this.batchTimer = setTimeout(() => {
|
|
165
|
+
this.flush();
|
|
166
|
+
}, this.config.batchInterval);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* @description 刷新队列,批量上报
|
|
171
|
+
* @return void
|
|
172
|
+
*/
|
|
173
|
+
private flush(): void {
|
|
174
|
+
if (this.eventQueue.length === 0) return;
|
|
175
|
+
const batchPayload = [...this.eventQueue];
|
|
176
|
+
this.eventQueue = [];
|
|
177
|
+
if (this.batchTimer) {
|
|
178
|
+
clearTimeout(this.batchTimer);
|
|
179
|
+
this.batchTimer = null;
|
|
180
|
+
}
|
|
181
|
+
this.sendBatch(batchPayload);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* @description navigator.sendBeacon单条错误上报失败的fetch回退方案
|
|
185
|
+
* @param payload any
|
|
186
|
+
* */
|
|
187
|
+
private async fallbackSend(payload: any): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
await fetch(this.config.serverUrl, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(payload),
|
|
195
|
+
keepalive: true,
|
|
196
|
+
// credentials: "include",
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (this.config.debug) {
|
|
200
|
+
console.error("Single event fallback send failed:", error);
|
|
201
|
+
}
|
|
202
|
+
this.saveFailedBatch({ events: [payload] });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* @description 错误事件立即上报
|
|
207
|
+
* @param data 上报的负载
|
|
208
|
+
* @returns void
|
|
209
|
+
*/
|
|
210
|
+
sendImmediately(payload: TrackEvent): void {
|
|
211
|
+
// if (navigator.sendBeacon) {
|
|
212
|
+
// const blob = new Blob([JSON.stringify(payload)], {
|
|
213
|
+
// type: "application/json",
|
|
214
|
+
// });
|
|
215
|
+
// if (!navigator.sendBeacon(this.config.serverUrl, blob)) {
|
|
216
|
+
// this.fallbackSend(payload);
|
|
217
|
+
// }
|
|
218
|
+
// } else {
|
|
219
|
+
// this.fallbackSend(payload);
|
|
220
|
+
// }
|
|
221
|
+
/* 改用 fetch api */
|
|
222
|
+
this.fallbackSend(payload);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* @description 批量上报
|
|
226
|
+
* @param events 上报的事件数组
|
|
227
|
+
* @returns Promise<void>
|
|
228
|
+
*/
|
|
229
|
+
private async sendBatch(events: any[]): Promise<void> {
|
|
230
|
+
const payload = { events };
|
|
231
|
+
// if (navigator.sendBeacon) {
|
|
232
|
+
// const blob = new Blob([JSON.stringify(payload)], {
|
|
233
|
+
// type: "application/json",
|
|
234
|
+
// });
|
|
235
|
+
// if (!navigator.sendBeacon(this.config.serverUrl, blob)) {
|
|
236
|
+
// await this.fallbackSendBatch(payload);
|
|
237
|
+
// }
|
|
238
|
+
// } else {
|
|
239
|
+
// await this.fallbackSendBatch(payload);
|
|
240
|
+
// }
|
|
241
|
+
/* 改用 fetch api */
|
|
242
|
+
await this.fallbackSendBatch(payload);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* @description 批量上报的fetch回退方案
|
|
246
|
+
* @param payload 上报的负载
|
|
247
|
+
* @returns Promise<void>
|
|
248
|
+
*/
|
|
249
|
+
private async fallbackSendBatch(payload: any): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
await fetch(this.config.serverUrl, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: {
|
|
254
|
+
"Content-Type": "application/json",
|
|
255
|
+
},
|
|
256
|
+
body: JSON.stringify(payload),
|
|
257
|
+
keepalive: true,
|
|
258
|
+
// credentials: "include",
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (this.config.debug) {
|
|
262
|
+
console.error("[fallbackSendBatch] fecth批量上报失败:", error);
|
|
263
|
+
}
|
|
264
|
+
// 可选:存入 localStorage 用于下次重试
|
|
265
|
+
this.saveFailedBatch(payload);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
private saveFailedBatch(payload: any): void {
|
|
269
|
+
try {
|
|
270
|
+
const key = "tracker_failed_batches";
|
|
271
|
+
const existing = localStorage.getItem(key);
|
|
272
|
+
const batches = existing ? JSON.parse(existing) : [];
|
|
273
|
+
batches.push(payload);
|
|
274
|
+
if (batches.length > 3) batches.shift(); // 限制最多3批
|
|
275
|
+
localStorage.setItem(key, JSON.stringify(batches));
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// localStorage 可能满或禁用,静默忽略
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* @description 自定义点击事件
|
|
282
|
+
*/
|
|
283
|
+
track(event: string, properties: Record<string, any> = {}): void {
|
|
284
|
+
this.send({
|
|
285
|
+
eventType: "custom",
|
|
286
|
+
event,
|
|
287
|
+
properties,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* @description 用于自动点击埋点
|
|
292
|
+
*
|
|
293
|
+
* @param element 点击的DOM元素
|
|
294
|
+
*/
|
|
295
|
+
trackClick(element: Element): void {
|
|
296
|
+
const tagName = element.tagName.toLowerCase();
|
|
297
|
+
const id = element.id || "";
|
|
298
|
+
const classes = Array.from(element.classList).join(" ");
|
|
299
|
+
const text = ["button", "a"].includes(tagName)
|
|
300
|
+
? (element as HTMLElement).innerText?.trim().slice(0, 20)
|
|
301
|
+
: "";
|
|
302
|
+
if (this.config.enableAutoClickTagNameList.includes(tagName)) {
|
|
303
|
+
this.send({
|
|
304
|
+
eventType: "click",
|
|
305
|
+
event: "auto_click",
|
|
306
|
+
properties: {
|
|
307
|
+
tagName,
|
|
308
|
+
id,
|
|
309
|
+
classes,
|
|
310
|
+
text,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
if (this.config.debug) {
|
|
315
|
+
console.log("点击元素不触发自动埋点");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// #endregion
|
|
320
|
+
// #region pv & uv
|
|
321
|
+
/**
|
|
322
|
+
* @description 自动pv
|
|
323
|
+
*
|
|
324
|
+
* @param path pv的页面路径
|
|
325
|
+
* @param properties 上报数据
|
|
326
|
+
*/
|
|
327
|
+
trackPV(path: string, properties: Record<string, any> = {}): void {
|
|
328
|
+
this.send({
|
|
329
|
+
eventType: "pv",
|
|
330
|
+
properties: {
|
|
331
|
+
path,
|
|
332
|
+
referrer: document.referrer,
|
|
333
|
+
...properties,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* @description 统计uv(每天每个设备只上报一次)
|
|
339
|
+
*
|
|
340
|
+
*/
|
|
341
|
+
trackUV(): void {
|
|
342
|
+
const deviceId = this.getDeviceId();
|
|
343
|
+
const now = new Date().getTime();
|
|
344
|
+
const beijingTime = new Date(now + 8 * 3600 * 1000);
|
|
345
|
+
const today = new Date(beijingTime).toISOString().split("T")[0];
|
|
346
|
+
const uvKey = `tracker_uv_${today}`;
|
|
347
|
+
if (!localStorage.getItem(uvKey)) {
|
|
348
|
+
this.send({
|
|
349
|
+
eventType: "uv",
|
|
350
|
+
properties: {
|
|
351
|
+
deviceId,
|
|
352
|
+
date: today,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
localStorage.setItem(uvKey, "1");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// #endregion
|
|
359
|
+
// #region 错误监控
|
|
360
|
+
/** @description 初始化错误事件监听
|
|
361
|
+
*
|
|
362
|
+
*/
|
|
363
|
+
setupErrorTracking() {
|
|
364
|
+
// 全局 JS 错误
|
|
365
|
+
window.addEventListener(
|
|
366
|
+
"error",
|
|
367
|
+
({ message, filename, lineno, colno, error }) => {
|
|
368
|
+
console.log("🚀 ~ '全局 JS 错误捕获了'");
|
|
369
|
+
this.send({
|
|
370
|
+
eventType: "error",
|
|
371
|
+
event: "js_error",
|
|
372
|
+
properties: {
|
|
373
|
+
message,
|
|
374
|
+
filename,
|
|
375
|
+
lineno,
|
|
376
|
+
colno,
|
|
377
|
+
stack: error?.stack || "no stack",
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
// 资源加载错误
|
|
383
|
+
window.addEventListener(
|
|
384
|
+
"error",
|
|
385
|
+
(e: any) => {
|
|
386
|
+
if (e.target?.localName) {
|
|
387
|
+
let url = e.target.src || e.target.href || "";
|
|
388
|
+
url = url.trim();
|
|
389
|
+
const tag = e.target;
|
|
390
|
+
const originalSrc = tag.getAttribute("src");
|
|
391
|
+
const originalHref = tag.getAttribute("href");
|
|
392
|
+
// 如果原始值是空字符串(空 src 会被解析成当前页)、null、undefined,则是伪错误
|
|
393
|
+
if (
|
|
394
|
+
(originalSrc != null && originalSrc.trim() === "") ||
|
|
395
|
+
(originalHref != null && originalHref.trim() === "")
|
|
396
|
+
) {
|
|
397
|
+
return false; // 过滤掉
|
|
398
|
+
}
|
|
399
|
+
if (url == "") return false;
|
|
400
|
+
try {
|
|
401
|
+
// 验证是否是合法 URL
|
|
402
|
+
new URL(url);
|
|
403
|
+
console.log("🚀 ~ '资源加载错误捕获了'");
|
|
404
|
+
this.send({
|
|
405
|
+
eventType: "error",
|
|
406
|
+
event: "resource_load_error",
|
|
407
|
+
properties: {
|
|
408
|
+
resourceUrl: url,
|
|
409
|
+
tageName: e.target.localName,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
true
|
|
418
|
+
);
|
|
419
|
+
// Promise unhandled rejection
|
|
420
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
421
|
+
"Promise unhandled rejection捕获了";
|
|
422
|
+
console.log("🚀 ~ 'Promise unhandled rejection捕获了'");
|
|
423
|
+
this.send({
|
|
424
|
+
eventType: "error",
|
|
425
|
+
event: "promise_unhandled_rejection",
|
|
426
|
+
properties: {
|
|
427
|
+
reason: String(event.reason),
|
|
428
|
+
stack: event.reason?.stack || "no stack",
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
// 阻止打印报错信息
|
|
432
|
+
event.preventDefault();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* @description vue3 全局错误处理器
|
|
437
|
+
*/
|
|
438
|
+
setupVueErrorHandlder(app: any) {
|
|
439
|
+
app.config.errorHandler = (err: Error, instance: any, info: string) => {
|
|
440
|
+
console.log("🚀 ~ vue3 全局错误处理器捕获了");
|
|
441
|
+
// setTimeout(() => {
|
|
442
|
+
// throw err;
|
|
443
|
+
// }, 0);
|
|
444
|
+
this.send({
|
|
445
|
+
eventType: "error",
|
|
446
|
+
event: "vue_component_error",
|
|
447
|
+
properties: {
|
|
448
|
+
message: err.message,
|
|
449
|
+
stack: err.stack || "no stack",
|
|
450
|
+
componentInfo: info,
|
|
451
|
+
componentName: instance?.$options?.name || "unknown",
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// #endregion
|
|
457
|
+
// #region 性能监控
|
|
458
|
+
private setupPerformanceTracking(): void {
|
|
459
|
+
// if (!("performance" in window)) return;
|
|
460
|
+
// const perf: any = performance.getEntriesByType("navigation")[0];
|
|
461
|
+
// if (!perf) return;
|
|
462
|
+
// this.send({
|
|
463
|
+
// eventType: "performance",
|
|
464
|
+
// event: "page_performance",
|
|
465
|
+
// properties: {
|
|
466
|
+
// dns: perf.domainLookupEnd - perf.domainLookupStart,
|
|
467
|
+
// tcpAndTls: perf.connectEnd - perf.connectStart,
|
|
468
|
+
// domReady: perf.domContentLoadedEventEnd - perf.fetchStart,
|
|
469
|
+
// load: perf.loadEventEnd - perf.fetchStart,
|
|
470
|
+
// },
|
|
471
|
+
// });
|
|
472
|
+
this.observeFCP();
|
|
473
|
+
this.observeLCP();
|
|
474
|
+
this.observeCLS();
|
|
475
|
+
this.observeTTFB();
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* @description 监控FCP
|
|
479
|
+
* @returns void
|
|
480
|
+
*/
|
|
481
|
+
private observeFCP(): void {
|
|
482
|
+
onFCP((metric: any) => {
|
|
483
|
+
this.send({
|
|
484
|
+
eventType: "performance",
|
|
485
|
+
event: "first_contentful_paint",
|
|
486
|
+
properties: metric,
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* @description 监控LCP
|
|
492
|
+
* @returns void
|
|
493
|
+
*/
|
|
494
|
+
private observeLCP(): void {
|
|
495
|
+
onLCP((metric: any) => {
|
|
496
|
+
this.send({
|
|
497
|
+
eventType: "performance",
|
|
498
|
+
event: "largest_contentful_paint",
|
|
499
|
+
properties: metric,
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* @description 监控CLS
|
|
505
|
+
* @returns void
|
|
506
|
+
*/
|
|
507
|
+
private observeCLS(): void {
|
|
508
|
+
onCLS((metric: any) => {
|
|
509
|
+
this.send({
|
|
510
|
+
eventType: "performance",
|
|
511
|
+
event: "cumulative_layout_shift",
|
|
512
|
+
properties: {
|
|
513
|
+
...metric,
|
|
514
|
+
entries: undefined, // 去掉 entries,数据量可能很庞大
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* @description 监控TTFB
|
|
521
|
+
* @returns void
|
|
522
|
+
*/
|
|
523
|
+
private observeTTFB(): void {
|
|
524
|
+
onTTFB((metric: any) => {
|
|
525
|
+
this.send({
|
|
526
|
+
eventType: "performance",
|
|
527
|
+
event: "time_to_first_byte",
|
|
528
|
+
properties: metric,
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
// #endregion
|
|
533
|
+
// #region 全埋点
|
|
534
|
+
/**
|
|
535
|
+
* @description 全埋点(谨慎使用,存在性能问题)
|
|
536
|
+
*/
|
|
537
|
+
enableAutoTracking() {
|
|
538
|
+
// 防止重复绑定
|
|
539
|
+
if (this.autoTrackClickHandler) return;
|
|
540
|
+
this.autoTrackClickHandler = (e) => {
|
|
541
|
+
if (e.target instanceof Element) {
|
|
542
|
+
this.trackClick(e.target);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
document.documentElement.addEventListener(
|
|
546
|
+
"click",
|
|
547
|
+
this.autoTrackClickHandler
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* @description: 设置可触发自动埋点的DOM元素标签名称白名单
|
|
552
|
+
*/
|
|
553
|
+
setEnableAutoClickTagNameList(tagNameList: Array<TagNames>) {
|
|
554
|
+
if (this.config.autoTrack) {
|
|
555
|
+
this.config.enableAutoClickTagNameList = tagNameList;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// #endregion
|
|
559
|
+
}
|
|
560
|
+
// 单例导出
|
|
561
|
+
export const tracker = new TrackerSDK();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { tracker } from "./TrackerSDK";
|
|
2
|
+
import type { TrackerConfig } from "./types";
|
|
3
|
+
export default {
|
|
4
|
+
install(app: any, options: TrackerConfig) {
|
|
5
|
+
tracker.init(options);
|
|
6
|
+
app.provide("tracker", tracker);
|
|
7
|
+
// 自动化埋点,存在 performance
|
|
8
|
+
if (options.autoTrack) {
|
|
9
|
+
tracker.enableAutoTracking();
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export function useTracker() {
|
|
14
|
+
return tracker;
|
|
15
|
+
}
|
|
16
|
+
export { tracker };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type TagNames = string;
|
|
2
|
+
export interface TrackerConfig {
|
|
3
|
+
serverUrl: string;
|
|
4
|
+
appId: string;
|
|
5
|
+
userId?: string;
|
|
6
|
+
autoTrack?: boolean;
|
|
7
|
+
enableAutoClickTagNameList?: Array<TagNames>;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
batchSize?: number;
|
|
10
|
+
batchInterval?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TrackProperties {
|
|
14
|
+
[propName: string]: string | number | boolean | null | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type TrackerEventType =
|
|
18
|
+
| "custom"
|
|
19
|
+
| "click"
|
|
20
|
+
| "pv"
|
|
21
|
+
| "uv"
|
|
22
|
+
| "error"
|
|
23
|
+
| "performance";
|
|
24
|
+
|
|
25
|
+
export interface TrackEvent {
|
|
26
|
+
eventType: TrackerEventType;
|
|
27
|
+
properties?: TrackProperties;
|
|
28
|
+
event?: string;
|
|
29
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2018",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["DOM", "ES2020"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|