@be-link/cls-logger 1.0.11 → 1.0.13

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.umd.js CHANGED
@@ -182,6 +182,11 @@
182
182
  this.batchTimerDueAt = null;
183
183
  this.initTs = 0;
184
184
  this.startupDelayMs = 0;
185
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
186
+ this.useIdleCallback = false;
187
+ this.idleTimeout = 3000;
188
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
189
+ this.visibilityCleanup = null;
185
190
  // 参考文档:失败缓存 + 重试
186
191
  this.failedCacheKey = 'cls_failed_logs';
187
192
  this.failedCacheMax = 200;
@@ -205,7 +210,7 @@
205
210
  }
206
211
  return 'browser';
207
212
  }
208
- init(options) {
213
+ async init(options) {
209
214
  this.initTs = Date.now();
210
215
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId;
211
216
  const endpoint = options?.tencentCloud?.endpoint ?? options?.endpoint ?? this.endpoint;
@@ -214,7 +219,7 @@
214
219
  if (!topicId) {
215
220
  // eslint-disable-next-line no-console
216
221
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
217
- return;
222
+ return false;
218
223
  }
219
224
  const nextEnvType = options.envType ?? this.detectEnvType();
220
225
  // envType/endpoint/retryTimes 变化时:重置 client(以及可能的 sdk)
@@ -251,15 +256,21 @@
251
256
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
252
257
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
253
258
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
259
+ this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
260
+ this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
261
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
262
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
254
263
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
255
264
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
256
265
  // 预热(避免首条日志触发 import/初始化开销)
257
- void this.getInstance().catch(() => {
258
- // ignore
259
- });
266
+ const sdkReadyPromise = this.getInstance()
267
+ .then(() => true)
268
+ .catch(() => false);
260
269
  if (this.enabled) {
261
270
  // 启动时尝试发送失败缓存
262
271
  this.flushFailed();
272
+ // 添加页面可见性监听(确保页面关闭时数据不丢失)
273
+ this.setupVisibilityListener();
263
274
  // 初始化后立即启动请求监听
264
275
  this.startRequestMonitor(options.requestMonitor);
265
276
  // 初始化后立即启动错误监控/性能监控
@@ -268,6 +279,7 @@
268
279
  // 初始化后立即启动行为埋点(PV/UV/点击)
269
280
  this.startBehaviorMonitor(options.behaviorMonitor);
270
281
  }
282
+ return sdkReadyPromise;
271
283
  }
272
284
  getBaseFields() {
273
285
  let auto = undefined;
@@ -296,6 +308,97 @@
296
308
  return auto;
297
309
  return undefined;
298
310
  }
311
+ /**
312
+ * 设置页面可见性监听
313
+ * - visibilitychange: 页面隐藏时使用 sendBeacon 发送队列
314
+ * - pagehide: 作为移动端 fallback
315
+ * - 子类可覆写此方法以实现平台特定的监听(如小程序的 wx.onAppHide)
316
+ */
317
+ setupVisibilityListener() {
318
+ if (typeof document === 'undefined' || typeof window === 'undefined')
319
+ return;
320
+ // 避免重复监听
321
+ if (this.visibilityCleanup)
322
+ return;
323
+ const handleVisibilityChange = () => {
324
+ if (document.visibilityState === 'hidden') {
325
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
326
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
327
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
328
+ queueMicrotask(() => {
329
+ this.flushBatchSync();
330
+ });
331
+ }
332
+ };
333
+ const handlePageHide = () => {
334
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
335
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
336
+ this.flushBatchSync();
337
+ };
338
+ document.addEventListener('visibilitychange', handleVisibilityChange);
339
+ window.addEventListener('pagehide', handlePageHide);
340
+ this.visibilityCleanup = () => {
341
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
342
+ window.removeEventListener('pagehide', handlePageHide);
343
+ };
344
+ }
345
+ /**
346
+ * 同步发送内存队列(使用 sendBeacon)
347
+ * - 用于页面关闭时确保数据发送
348
+ * - sendBeacon 不可用时降级为缓存到 localStorage
349
+ */
350
+ flushBatchSync() {
351
+ if (this.memoryQueue.length === 0)
352
+ return;
353
+ // 清除定时器
354
+ if (this.batchTimer) {
355
+ try {
356
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
357
+ cancelIdleCallback(this.batchTimer);
358
+ }
359
+ else {
360
+ clearTimeout(this.batchTimer);
361
+ }
362
+ }
363
+ catch {
364
+ // ignore
365
+ }
366
+ this.batchTimer = null;
367
+ }
368
+ this.batchTimerDueAt = null;
369
+ const logs = [...this.memoryQueue];
370
+ this.memoryQueue = [];
371
+ // 优先使用 sendBeacon(页面关闭时可靠发送)
372
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
373
+ try {
374
+ const payload = this.buildSendBeaconPayload(logs);
375
+ const blob = new Blob([payload], { type: 'application/json' });
376
+ const url = `${this.endpoint}/structuredlog?topic_id=${this.topicId}`;
377
+ const success = navigator.sendBeacon(url, blob);
378
+ if (!success) {
379
+ // sendBeacon 返回 false 时,降级缓存
380
+ this.cacheFailedReportLogs(logs);
381
+ }
382
+ }
383
+ catch {
384
+ this.cacheFailedReportLogs(logs);
385
+ }
386
+ }
387
+ else {
388
+ // 不支持 sendBeacon,降级缓存到 localStorage
389
+ this.cacheFailedReportLogs(logs);
390
+ }
391
+ }
392
+ /**
393
+ * 构建 sendBeacon 的 payload
394
+ */
395
+ buildSendBeaconPayload(logs) {
396
+ const logList = logs.map((log) => this.buildReportFields(log));
397
+ return JSON.stringify({
398
+ source: this.source,
399
+ logs: logList,
400
+ });
401
+ }
299
402
  startRequestMonitor(requestMonitor) {
300
403
  if (this.requestMonitorStarted)
301
404
  return;
@@ -541,32 +644,75 @@
541
644
  return;
542
645
  }
543
646
  this.memoryQueue.push(log);
544
- if (this.memoryQueue.length >= this.batchMaxSize) {
647
+ const now = Date.now();
648
+ // 判断是否在启动合并窗口内
649
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
650
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
651
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
652
+ if (this.memoryQueue.length >= effectiveMaxSize) {
545
653
  void this.flushBatch();
546
654
  return;
547
655
  }
548
- const now = Date.now();
549
656
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
550
657
  const desiredDelay = Math.max(0, desiredDueAt - now);
551
658
  if (!this.batchTimer) {
552
659
  this.batchTimerDueAt = desiredDueAt;
660
+ this.scheduleFlush(desiredDelay);
661
+ return;
662
+ }
663
+ // 启动合并窗口内:如果当前 timer 会"更早"触发,则延后到窗口结束,尽量减少多次发送
664
+ if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
665
+ this.cancelScheduledFlush();
666
+ this.batchTimerDueAt = desiredDueAt;
667
+ this.scheduleFlush(desiredDelay);
668
+ }
669
+ }
670
+ /**
671
+ * 调度批量发送
672
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
673
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
674
+ */
675
+ scheduleFlush(desiredDelay) {
676
+ if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
677
+ // 先 setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
678
+ this.batchTimer = setTimeout(() => {
679
+ const idleId = requestIdleCallback(() => {
680
+ this.pendingIdleCallback = null;
681
+ void this.flushBatch();
682
+ }, { timeout: this.idleTimeout });
683
+ this.pendingIdleCallback = idleId;
684
+ }, desiredDelay);
685
+ }
686
+ else {
553
687
  this.batchTimer = setTimeout(() => {
554
688
  void this.flushBatch();
555
689
  }, desiredDelay);
556
- return;
557
690
  }
558
- // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
559
- if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
691
+ }
692
+ /**
693
+ * 取消已调度的批量发送
694
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
695
+ */
696
+ cancelScheduledFlush() {
697
+ // 清理 setTimeout
698
+ if (this.batchTimer) {
560
699
  try {
561
700
  clearTimeout(this.batchTimer);
562
701
  }
563
702
  catch {
564
703
  // ignore
565
704
  }
566
- this.batchTimerDueAt = desiredDueAt;
567
- this.batchTimer = setTimeout(() => {
568
- void this.flushBatch();
569
- }, desiredDelay);
705
+ this.batchTimer = null;
706
+ }
707
+ // 清理可能的 pendingIdleCallback
708
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
709
+ try {
710
+ cancelIdleCallback(this.pendingIdleCallback);
711
+ }
712
+ catch {
713
+ // ignore
714
+ }
715
+ this.pendingIdleCallback = null;
570
716
  }
571
717
  }
572
718
  getDesiredBatchFlushDueAt(nowTs) {
@@ -579,7 +725,7 @@
579
725
  }
580
726
  return nowTs + this.batchIntervalMs;
581
727
  }
582
- info(message, data = {}) {
728
+ info(message, data = {}, options) {
583
729
  let msg = '';
584
730
  let extra = {};
585
731
  if (message instanceof Error) {
@@ -595,9 +741,18 @@
595
741
  extra = data;
596
742
  }
597
743
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'info');
598
- this.report({ type: 'info', data: payload, timestamp: Date.now() });
744
+ const log = { type: 'info', data: payload, timestamp: Date.now() };
745
+ // info 默认走批量队列,支持 immediate 选项立即发送
746
+ if (options?.immediate) {
747
+ void this.sendReportLogs([log]).catch(() => {
748
+ this.cacheFailedReportLogs([log]);
749
+ });
750
+ }
751
+ else {
752
+ this.report(log);
753
+ }
599
754
  }
600
- warn(message, data = {}) {
755
+ warn(message, data = {}, options) {
601
756
  let msg = '';
602
757
  let extra = {};
603
758
  if (message instanceof Error) {
@@ -613,9 +768,18 @@
613
768
  extra = data;
614
769
  }
615
770
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'warn');
616
- this.report({ type: 'warn', data: payload, timestamp: Date.now() });
771
+ const log = { type: 'warn', data: payload, timestamp: Date.now() };
772
+ // warn 默认走批量队列,支持 immediate 选项立即发送
773
+ if (options?.immediate) {
774
+ void this.sendReportLogs([log]).catch(() => {
775
+ this.cacheFailedReportLogs([log]);
776
+ });
777
+ }
778
+ else {
779
+ this.report(log);
780
+ }
617
781
  }
618
- error(message, data = {}) {
782
+ error(message, data = {}, options) {
619
783
  let msg = '';
620
784
  let extra = {};
621
785
  if (message instanceof Error) {
@@ -631,7 +795,17 @@
631
795
  extra = data;
632
796
  }
633
797
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'error');
634
- this.report({ type: 'error', data: payload, timestamp: Date.now() });
798
+ const log = { type: 'error', data: payload, timestamp: Date.now() };
799
+ // error 默认即时上报,除非显式指定 immediate: false
800
+ const immediate = options?.immediate ?? true;
801
+ if (immediate) {
802
+ void this.sendReportLogs([log]).catch(() => {
803
+ this.cacheFailedReportLogs([log]);
804
+ });
805
+ }
806
+ else {
807
+ this.report(log);
808
+ }
635
809
  }
636
810
  track(trackType, data = {}) {
637
811
  if (!trackType)
@@ -646,10 +820,7 @@
646
820
  * 立即发送内存队列
647
821
  */
648
822
  async flushBatch() {
649
- if (this.batchTimer) {
650
- clearTimeout(this.batchTimer);
651
- this.batchTimer = null;
652
- }
823
+ this.cancelScheduledFlush();
653
824
  this.batchTimerDueAt = null;
654
825
  if (this.memoryQueue.length === 0)
655
826
  return;
@@ -755,7 +926,14 @@
755
926
  // 先清空,再尝试发送
756
927
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
757
928
  this.memoryQueue.unshift(...logs);
758
- void this.flushBatch();
929
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
930
+ if (!this.batchTimer) {
931
+ const now = Date.now();
932
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
933
+ const desiredDelay = Math.max(0, desiredDueAt - now);
934
+ this.batchTimerDueAt = desiredDueAt;
935
+ this.scheduleFlush(desiredDelay);
936
+ }
759
937
  }
760
938
  /**
761
939
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
@@ -1294,6 +1472,13 @@
1294
1472
  const DEFAULT_MAX_TEXT$1 = 4000;
1295
1473
  const DEFAULT_DEDUPE_WINDOW_MS$1 = 3000;
1296
1474
  const DEFAULT_DEDUPE_MAX_KEYS$1 = 200;
1475
+ /** 默认忽略的无意义错误信息 */
1476
+ const DEFAULT_IGNORE_MESSAGES$1 = [
1477
+ 'Script error.',
1478
+ 'Script error',
1479
+ /^ResizeObserver loop/,
1480
+ 'Permission was denied',
1481
+ ];
1297
1482
  function truncate$3(s, maxLen) {
1298
1483
  if (!s)
1299
1484
  return s;
@@ -1306,11 +1491,22 @@
1306
1491
  return false;
1307
1492
  return Math.random() < sampleRate;
1308
1493
  }
1494
+ /** 检查消息是否应该被忽略 */
1495
+ function shouldIgnoreMessage$1(message, ignorePatterns) {
1496
+ if (!message || ignorePatterns.length === 0)
1497
+ return false;
1498
+ return ignorePatterns.some((pattern) => {
1499
+ if (typeof pattern === 'string') {
1500
+ return message === pattern || message.includes(pattern);
1501
+ }
1502
+ return pattern.test(message);
1503
+ });
1504
+ }
1309
1505
  function getPagePath$1() {
1310
1506
  try {
1311
1507
  if (typeof window === 'undefined')
1312
1508
  return '';
1313
- return window.location?.pathname ?? '';
1509
+ return window.location?.href ?? '';
1314
1510
  }
1315
1511
  catch {
1316
1512
  return '';
@@ -1401,6 +1597,9 @@
1401
1597
  };
1402
1598
  if (event && typeof event === 'object' && 'message' in event) {
1403
1599
  payload.message = truncate$3(String(event.message ?? ''), options.maxTextLength);
1600
+ // 检查是否应该忽略此错误
1601
+ if (shouldIgnoreMessage$1(String(event.message ?? ''), options.ignoreMessages))
1602
+ return;
1404
1603
  payload.filename = truncate$3(String(event.filename ?? ''), 500);
1405
1604
  payload.lineno = typeof event.lineno === 'number' ? event.lineno : undefined;
1406
1605
  payload.colno = typeof event.colno === 'number' ? event.colno : undefined;
@@ -1439,6 +1638,9 @@
1439
1638
  return;
1440
1639
  const reason = event?.reason;
1441
1640
  const e = normalizeErrorLike$1(reason, options.maxTextLength);
1641
+ // 检查是否应该忽略此错误
1642
+ if (shouldIgnoreMessage$1(e.message, options.ignoreMessages))
1643
+ return;
1442
1644
  const payload = {
1443
1645
  pagePath: getPagePath$1(),
1444
1646
  source: 'unhandledrejection',
@@ -1468,6 +1670,7 @@
1468
1670
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT$1,
1469
1671
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS$1,
1470
1672
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS$1,
1673
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES$1,
1471
1674
  };
1472
1675
  installBrowserErrorMonitor(report, options);
1473
1676
  }
@@ -1475,6 +1678,13 @@
1475
1678
  const DEFAULT_MAX_TEXT = 4000;
1476
1679
  const DEFAULT_DEDUPE_WINDOW_MS = 3000;
1477
1680
  const DEFAULT_DEDUPE_MAX_KEYS = 200;
1681
+ /** 默认忽略的无意义错误信息 */
1682
+ const DEFAULT_IGNORE_MESSAGES = [
1683
+ 'Script error.',
1684
+ 'Script error',
1685
+ /^ResizeObserver loop/,
1686
+ 'Permission was denied',
1687
+ ];
1478
1688
  function truncate$2(s, maxLen) {
1479
1689
  if (!s)
1480
1690
  return s;
@@ -1487,12 +1697,33 @@
1487
1697
  return false;
1488
1698
  return Math.random() < sampleRate;
1489
1699
  }
1700
+ /** 检查消息是否应该被忽略 */
1701
+ function shouldIgnoreMessage(message, ignorePatterns) {
1702
+ if (!message || ignorePatterns.length === 0)
1703
+ return false;
1704
+ return ignorePatterns.some((pattern) => {
1705
+ if (typeof pattern === 'string') {
1706
+ return message === pattern || message.includes(pattern);
1707
+ }
1708
+ return pattern.test(message);
1709
+ });
1710
+ }
1490
1711
  function getMpPagePath() {
1491
1712
  try {
1492
1713
  const pages = globalThis.getCurrentPages?.();
1493
1714
  if (Array.isArray(pages) && pages.length > 0) {
1494
1715
  const page = pages[pages.length - 1];
1495
- return page.route || page.__route__ || '';
1716
+ const route = page.route || page.__route__;
1717
+ if (typeof route === 'string') {
1718
+ let path = route.startsWith('/') ? route : `/${route}`;
1719
+ const options = page.options || {};
1720
+ const keys = Object.keys(options);
1721
+ if (keys.length > 0) {
1722
+ const qs = keys.map((k) => `${k}=${options[k]}`).join('&');
1723
+ path = `${path}?${qs}`;
1724
+ }
1725
+ return path;
1726
+ }
1496
1727
  }
1497
1728
  return '';
1498
1729
  }
@@ -1581,6 +1812,9 @@
1581
1812
  if (!sampleHit$2(options.sampleRate))
1582
1813
  return;
1583
1814
  const e = normalizeErrorLike(msg, options.maxTextLength);
1815
+ // 检查是否应该忽略此错误
1816
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1817
+ return;
1584
1818
  const payload = {
1585
1819
  pagePath: getMpPagePath(),
1586
1820
  source: 'wx.onError',
@@ -1608,6 +1842,9 @@
1608
1842
  if (!sampleHit$2(options.sampleRate))
1609
1843
  return;
1610
1844
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
1845
+ // 检查是否应该忽略此错误
1846
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1847
+ return;
1611
1848
  const payload = {
1612
1849
  pagePath: getMpPagePath(),
1613
1850
  source: 'wx.onUnhandledRejection',
@@ -1639,15 +1876,18 @@
1639
1876
  try {
1640
1877
  if (sampleHit$2(options.sampleRate)) {
1641
1878
  const e = normalizeErrorLike(args?.[0], options.maxTextLength);
1642
- const payload = {
1643
- pagePath: getMpPagePath(),
1644
- source: 'App.onError',
1645
- message: e.message,
1646
- errorName: e.name,
1647
- stack: e.stack,
1648
- };
1649
- if (shouldReport(buildErrorKey(options.reportType, payload)))
1650
- report(options.reportType, payload);
1879
+ // 检查是否应该忽略此错误
1880
+ if (!shouldIgnoreMessage(e.message, options.ignoreMessages)) {
1881
+ const payload = {
1882
+ pagePath: getMpPagePath(),
1883
+ source: 'App.onError',
1884
+ message: e.message,
1885
+ errorName: e.name,
1886
+ stack: e.stack,
1887
+ };
1888
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
1889
+ report(options.reportType, payload);
1890
+ }
1651
1891
  }
1652
1892
  }
1653
1893
  catch {
@@ -1663,15 +1903,18 @@
1663
1903
  if (sampleHit$2(options.sampleRate)) {
1664
1904
  const reason = args?.[0]?.reason ?? args?.[0];
1665
1905
  const e = normalizeErrorLike(reason, options.maxTextLength);
1666
- const payload = {
1667
- pagePath: getMpPagePath(),
1668
- source: 'App.onUnhandledRejection',
1669
- message: e.message,
1670
- errorName: e.name,
1671
- stack: e.stack,
1672
- };
1673
- if (shouldReport(buildErrorKey(options.reportType, payload)))
1674
- report(options.reportType, payload);
1906
+ // 检查是否应该忽略此错误
1907
+ if (!shouldIgnoreMessage(e.message, options.ignoreMessages)) {
1908
+ const payload = {
1909
+ pagePath: getMpPagePath(),
1910
+ source: 'App.onUnhandledRejection',
1911
+ message: e.message,
1912
+ errorName: e.name,
1913
+ stack: e.stack,
1914
+ };
1915
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
1916
+ report(options.reportType, payload);
1917
+ }
1675
1918
  }
1676
1919
  }
1677
1920
  catch {
@@ -1702,6 +1945,7 @@
1702
1945
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
1703
1946
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
1704
1947
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
1948
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES,
1705
1949
  };
1706
1950
  installMiniProgramErrorMonitor(report, options);
1707
1951
  }
@@ -1747,7 +1991,7 @@
1747
1991
  try {
1748
1992
  if (typeof window === 'undefined')
1749
1993
  return '';
1750
- return window.location?.pathname ?? '';
1994
+ return window.location?.href ?? '';
1751
1995
  }
1752
1996
  catch {
1753
1997
  return '';
@@ -1919,9 +2163,25 @@
1919
2163
  if (!name || shouldIgnoreUrl(name, ignoreUrls))
1920
2164
  continue;
1921
2165
  const initiatorType = String(entry?.initiatorType ?? '');
1922
- // 对齐文档:关注 fetch/xhr/img/script(同时允许 css/other 但不强制)
2166
+ // 关注 fetch/xhr/img/script(同时允许 css/other 但不强制)
1923
2167
  if (!['xmlhttprequest', 'fetch', 'img', 'script', 'css'].includes(initiatorType))
1924
2168
  continue;
2169
+ // 时序分解(便于定位慢在哪个阶段)
2170
+ const domainLookupStart = entry.domainLookupStart ?? 0;
2171
+ const domainLookupEnd = entry.domainLookupEnd ?? 0;
2172
+ const connectStart = entry.connectStart ?? 0;
2173
+ const connectEnd = entry.connectEnd ?? 0;
2174
+ const requestStart = entry.requestStart ?? 0;
2175
+ const responseStart = entry.responseStart ?? 0;
2176
+ const responseEnd = entry.responseEnd ?? 0;
2177
+ const dns = domainLookupEnd - domainLookupStart;
2178
+ const tcp = connectEnd - connectStart;
2179
+ const ttfb = responseStart - requestStart;
2180
+ const download = responseEnd - responseStart;
2181
+ // 缓存检测:transferSize=0 且 decodedBodySize>0 表示缓存命中
2182
+ const transferSize = entry.transferSize ?? 0;
2183
+ const decodedBodySize = entry.decodedBodySize ?? 0;
2184
+ const cached = transferSize === 0 && decodedBodySize > 0;
1925
2185
  const payload = {
1926
2186
  pagePath: getPagePath(),
1927
2187
  metric: 'resource',
@@ -1929,16 +2189,26 @@
1929
2189
  url: truncate$1(name, options.maxTextLength),
1930
2190
  startTime: typeof entry?.startTime === 'number' ? entry.startTime : undefined,
1931
2191
  duration: typeof entry?.duration === 'number' ? entry.duration : undefined,
2192
+ // 时序分解(跨域资源可能为 0)
2193
+ dns: dns > 0 ? Math.round(dns) : undefined,
2194
+ tcp: tcp > 0 ? Math.round(tcp) : undefined,
2195
+ ttfb: ttfb > 0 ? Math.round(ttfb) : undefined,
2196
+ download: download > 0 ? Math.round(download) : undefined,
2197
+ // 缓存标记
2198
+ cached,
1932
2199
  };
1933
- // 兼容字段(部分浏览器支持)
1934
- if (typeof entry?.transferSize === 'number')
1935
- payload.transferSize = entry.transferSize;
1936
- if (typeof entry?.encodedBodySize === 'number')
2200
+ // 尺寸字段(跨域资源可能为 0)
2201
+ if (transferSize > 0)
2202
+ payload.transferSize = transferSize;
2203
+ if (typeof entry?.encodedBodySize === 'number' && entry.encodedBodySize > 0) {
1937
2204
  payload.encodedBodySize = entry.encodedBodySize;
1938
- if (typeof entry?.decodedBodySize === 'number')
1939
- payload.decodedBodySize = entry.decodedBodySize;
1940
- if (typeof entry?.nextHopProtocol === 'string')
2205
+ }
2206
+ if (decodedBodySize > 0)
2207
+ payload.decodedBodySize = decodedBodySize;
2208
+ // 协议和状态
2209
+ if (typeof entry?.nextHopProtocol === 'string' && entry.nextHopProtocol) {
1941
2210
  payload.nextHopProtocol = entry.nextHopProtocol;
2211
+ }
1942
2212
  if (typeof entry?.responseStatus === 'number')
1943
2213
  payload.status = entry.responseStatus;
1944
2214
  report(options.reportType, payload);
@@ -2115,7 +2385,7 @@
2115
2385
  function getWebPagePath() {
2116
2386
  if (typeof window === 'undefined')
2117
2387
  return '';
2118
- return window.location?.pathname || '';
2388
+ return window.location?.href || '';
2119
2389
  }
2120
2390
  function buildCommonUvFields$1(uvId, uvMeta, isFirstVisit) {
2121
2391
  return {
@@ -2367,7 +2637,16 @@
2367
2637
  const pages = typeof g.getCurrentPages === 'function' ? g.getCurrentPages() : [];
2368
2638
  const last = Array.isArray(pages) ? pages[pages.length - 1] : undefined;
2369
2639
  const route = (last?.route || last?.__route__);
2370
- return typeof route === 'string' ? route : '';
2640
+ if (typeof route !== 'string')
2641
+ return '';
2642
+ let path = route.startsWith('/') ? route : `/${route}`;
2643
+ const options = last?.options || {};
2644
+ const keys = Object.keys(options);
2645
+ if (keys.length > 0) {
2646
+ const qs = keys.map((k) => `${k}=${options[k]}`).join('&');
2647
+ path = `${path}?${qs}`;
2648
+ }
2649
+ return path;
2371
2650
  }
2372
2651
  catch {
2373
2652
  return '';
@@ -2715,7 +2994,7 @@
2715
2994
  if (options.includeNetwork) {
2716
2995
  try {
2717
2996
  const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
2718
- if (conn && isPlainObject(conn)) {
2997
+ if (conn && typeof conn === 'object') {
2719
2998
  if (typeof conn.effectiveType === 'string')
2720
2999
  out.netEffectiveType = conn.effectiveType;
2721
3000
  if (typeof conn.downlink === 'number')
@@ -2730,6 +3009,43 @@
2730
3009
  // ignore
2731
3010
  }
2732
3011
  }
3012
+ if (options.includeNetworkType) {
3013
+ try {
3014
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
3015
+ if (conn && typeof conn === 'object') {
3016
+ const type = conn.type;
3017
+ const eff = conn.effectiveType;
3018
+ let val = '';
3019
+ if (type === 'wifi' || type === 'ethernet') {
3020
+ val = 'WIFI';
3021
+ }
3022
+ else if (type === 'cellular' || type === 'wimax') {
3023
+ if (eff === 'slow-2g' || eff === '2g')
3024
+ val = '2G';
3025
+ else if (eff === '3g')
3026
+ val = '3G';
3027
+ else if (eff === '4g')
3028
+ val = '4G';
3029
+ else
3030
+ val = '4G';
3031
+ }
3032
+ else {
3033
+ // type 未知或不支持,降级使用 effectiveType
3034
+ if (eff === 'slow-2g' || eff === '2g')
3035
+ val = '2G';
3036
+ else if (eff === '3g')
3037
+ val = '3G';
3038
+ else if (eff === '4g')
3039
+ val = '4G';
3040
+ }
3041
+ if (val)
3042
+ out.networkType = val;
3043
+ }
3044
+ }
3045
+ catch {
3046
+ // ignore
3047
+ }
3048
+ }
2733
3049
  return out;
2734
3050
  }
2735
3051
  function createWebDeviceInfoBaseFields(opts) {
@@ -2759,7 +3075,7 @@
2759
3075
  try {
2760
3076
  const g = globalThis;
2761
3077
  const extra = g[globalKey];
2762
- if (extra && isPlainObject(extra))
3078
+ if (extra && typeof extra === 'object')
2763
3079
  return { ...base, ...extra };
2764
3080
  }
2765
3081
  catch {
@@ -2796,7 +3112,7 @@
2796
3112
  try {
2797
3113
  if (typeof wxAny.getNetworkTypeSync === 'function') {
2798
3114
  const n = wxAny.getNetworkTypeSync();
2799
- out.networkType = n?.networkType ? String(n.networkType) : '';
3115
+ out.networkType = n?.networkType ? String(n.networkType).toUpperCase() : '';
2800
3116
  }
2801
3117
  else if (typeof wxAny.getNetworkType === 'function') {
2802
3118
  // 异步更新:先不阻塞初始化
@@ -2806,7 +3122,9 @@
2806
3122
  const g = globalThis;
2807
3123
  if (!g.__beLinkClsLoggerDeviceInfo__)
2808
3124
  g.__beLinkClsLoggerDeviceInfo__ = {};
2809
- g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType ? String(res.networkType) : '';
3125
+ g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType
3126
+ ? String(res.networkType).toUpperCase()
3127
+ : '';
2810
3128
  }
2811
3129
  catch {
2812
3130
  // ignore
@@ -2825,7 +3143,7 @@
2825
3143
  const g = globalThis;
2826
3144
  if (!g.__beLinkClsLoggerDeviceInfo__)
2827
3145
  g.__beLinkClsLoggerDeviceInfo__ = {};
2828
- g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType ? String(res.networkType) : '';
3146
+ g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType ? String(res.networkType).toUpperCase() : '';
2829
3147
  }
2830
3148
  catch {
2831
3149
  // ignore
@@ -2866,7 +3184,7 @@
2866
3184
  try {
2867
3185
  const g = globalThis;
2868
3186
  const extra = g[globalKey];
2869
- if (extra && isPlainObject(extra))
3187
+ if (extra && typeof extra === 'object')
2870
3188
  return { ...base, ...extra };
2871
3189
  }
2872
3190
  catch {