trackersdk2 1.0.0 → 1.0.2

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