@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.esm.js CHANGED
@@ -176,6 +176,11 @@ class ClsLoggerCore {
176
176
  this.batchTimerDueAt = null;
177
177
  this.initTs = 0;
178
178
  this.startupDelayMs = 0;
179
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
180
+ this.useIdleCallback = false;
181
+ this.idleTimeout = 3000;
182
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
183
+ this.visibilityCleanup = null;
179
184
  // 参考文档:失败缓存 + 重试
180
185
  this.failedCacheKey = 'cls_failed_logs';
181
186
  this.failedCacheMax = 200;
@@ -199,7 +204,7 @@ class ClsLoggerCore {
199
204
  }
200
205
  return 'browser';
201
206
  }
202
- init(options) {
207
+ async init(options) {
203
208
  this.initTs = Date.now();
204
209
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId;
205
210
  const endpoint = options?.tencentCloud?.endpoint ?? options?.endpoint ?? this.endpoint;
@@ -208,7 +213,7 @@ class ClsLoggerCore {
208
213
  if (!topicId) {
209
214
  // eslint-disable-next-line no-console
210
215
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
211
- return;
216
+ return false;
212
217
  }
213
218
  const nextEnvType = options.envType ?? this.detectEnvType();
214
219
  // envType/endpoint/retryTimes 变化时:重置 client(以及可能的 sdk)
@@ -245,15 +250,21 @@ class ClsLoggerCore {
245
250
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
246
251
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
247
252
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
253
+ this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
254
+ this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
255
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
256
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
248
257
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
249
258
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
250
259
  // 预热(避免首条日志触发 import/初始化开销)
251
- void this.getInstance().catch(() => {
252
- // ignore
253
- });
260
+ const sdkReadyPromise = this.getInstance()
261
+ .then(() => true)
262
+ .catch(() => false);
254
263
  if (this.enabled) {
255
264
  // 启动时尝试发送失败缓存
256
265
  this.flushFailed();
266
+ // 添加页面可见性监听(确保页面关闭时数据不丢失)
267
+ this.setupVisibilityListener();
257
268
  // 初始化后立即启动请求监听
258
269
  this.startRequestMonitor(options.requestMonitor);
259
270
  // 初始化后立即启动错误监控/性能监控
@@ -262,6 +273,7 @@ class ClsLoggerCore {
262
273
  // 初始化后立即启动行为埋点(PV/UV/点击)
263
274
  this.startBehaviorMonitor(options.behaviorMonitor);
264
275
  }
276
+ return sdkReadyPromise;
265
277
  }
266
278
  getBaseFields() {
267
279
  let auto = undefined;
@@ -290,6 +302,97 @@ class ClsLoggerCore {
290
302
  return auto;
291
303
  return undefined;
292
304
  }
305
+ /**
306
+ * 设置页面可见性监听
307
+ * - visibilitychange: 页面隐藏时使用 sendBeacon 发送队列
308
+ * - pagehide: 作为移动端 fallback
309
+ * - 子类可覆写此方法以实现平台特定的监听(如小程序的 wx.onAppHide)
310
+ */
311
+ setupVisibilityListener() {
312
+ if (typeof document === 'undefined' || typeof window === 'undefined')
313
+ return;
314
+ // 避免重复监听
315
+ if (this.visibilityCleanup)
316
+ return;
317
+ const handleVisibilityChange = () => {
318
+ if (document.visibilityState === 'hidden') {
319
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
320
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
321
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
322
+ queueMicrotask(() => {
323
+ this.flushBatchSync();
324
+ });
325
+ }
326
+ };
327
+ const handlePageHide = () => {
328
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
329
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
330
+ this.flushBatchSync();
331
+ };
332
+ document.addEventListener('visibilitychange', handleVisibilityChange);
333
+ window.addEventListener('pagehide', handlePageHide);
334
+ this.visibilityCleanup = () => {
335
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
336
+ window.removeEventListener('pagehide', handlePageHide);
337
+ };
338
+ }
339
+ /**
340
+ * 同步发送内存队列(使用 sendBeacon)
341
+ * - 用于页面关闭时确保数据发送
342
+ * - sendBeacon 不可用时降级为缓存到 localStorage
343
+ */
344
+ flushBatchSync() {
345
+ if (this.memoryQueue.length === 0)
346
+ return;
347
+ // 清除定时器
348
+ if (this.batchTimer) {
349
+ try {
350
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
351
+ cancelIdleCallback(this.batchTimer);
352
+ }
353
+ else {
354
+ clearTimeout(this.batchTimer);
355
+ }
356
+ }
357
+ catch {
358
+ // ignore
359
+ }
360
+ this.batchTimer = null;
361
+ }
362
+ this.batchTimerDueAt = null;
363
+ const logs = [...this.memoryQueue];
364
+ this.memoryQueue = [];
365
+ // 优先使用 sendBeacon(页面关闭时可靠发送)
366
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
367
+ try {
368
+ const payload = this.buildSendBeaconPayload(logs);
369
+ const blob = new Blob([payload], { type: 'application/json' });
370
+ const url = `${this.endpoint}/structuredlog?topic_id=${this.topicId}`;
371
+ const success = navigator.sendBeacon(url, blob);
372
+ if (!success) {
373
+ // sendBeacon 返回 false 时,降级缓存
374
+ this.cacheFailedReportLogs(logs);
375
+ }
376
+ }
377
+ catch {
378
+ this.cacheFailedReportLogs(logs);
379
+ }
380
+ }
381
+ else {
382
+ // 不支持 sendBeacon,降级缓存到 localStorage
383
+ this.cacheFailedReportLogs(logs);
384
+ }
385
+ }
386
+ /**
387
+ * 构建 sendBeacon 的 payload
388
+ */
389
+ buildSendBeaconPayload(logs) {
390
+ const logList = logs.map((log) => this.buildReportFields(log));
391
+ return JSON.stringify({
392
+ source: this.source,
393
+ logs: logList,
394
+ });
395
+ }
293
396
  startRequestMonitor(requestMonitor) {
294
397
  if (this.requestMonitorStarted)
295
398
  return;
@@ -535,32 +638,75 @@ class ClsLoggerCore {
535
638
  return;
536
639
  }
537
640
  this.memoryQueue.push(log);
538
- if (this.memoryQueue.length >= this.batchMaxSize) {
641
+ const now = Date.now();
642
+ // 判断是否在启动合并窗口内
643
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
644
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
645
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
646
+ if (this.memoryQueue.length >= effectiveMaxSize) {
539
647
  void this.flushBatch();
540
648
  return;
541
649
  }
542
- const now = Date.now();
543
650
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
544
651
  const desiredDelay = Math.max(0, desiredDueAt - now);
545
652
  if (!this.batchTimer) {
546
653
  this.batchTimerDueAt = desiredDueAt;
654
+ this.scheduleFlush(desiredDelay);
655
+ return;
656
+ }
657
+ // 启动合并窗口内:如果当前 timer 会"更早"触发,则延后到窗口结束,尽量减少多次发送
658
+ if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
659
+ this.cancelScheduledFlush();
660
+ this.batchTimerDueAt = desiredDueAt;
661
+ this.scheduleFlush(desiredDelay);
662
+ }
663
+ }
664
+ /**
665
+ * 调度批量发送
666
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
667
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
668
+ */
669
+ scheduleFlush(desiredDelay) {
670
+ if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
671
+ // 先 setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
672
+ this.batchTimer = setTimeout(() => {
673
+ const idleId = requestIdleCallback(() => {
674
+ this.pendingIdleCallback = null;
675
+ void this.flushBatch();
676
+ }, { timeout: this.idleTimeout });
677
+ this.pendingIdleCallback = idleId;
678
+ }, desiredDelay);
679
+ }
680
+ else {
547
681
  this.batchTimer = setTimeout(() => {
548
682
  void this.flushBatch();
549
683
  }, desiredDelay);
550
- return;
551
684
  }
552
- // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
553
- if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
685
+ }
686
+ /**
687
+ * 取消已调度的批量发送
688
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
689
+ */
690
+ cancelScheduledFlush() {
691
+ // 清理 setTimeout
692
+ if (this.batchTimer) {
554
693
  try {
555
694
  clearTimeout(this.batchTimer);
556
695
  }
557
696
  catch {
558
697
  // ignore
559
698
  }
560
- this.batchTimerDueAt = desiredDueAt;
561
- this.batchTimer = setTimeout(() => {
562
- void this.flushBatch();
563
- }, desiredDelay);
699
+ this.batchTimer = null;
700
+ }
701
+ // 清理可能的 pendingIdleCallback
702
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
703
+ try {
704
+ cancelIdleCallback(this.pendingIdleCallback);
705
+ }
706
+ catch {
707
+ // ignore
708
+ }
709
+ this.pendingIdleCallback = null;
564
710
  }
565
711
  }
566
712
  getDesiredBatchFlushDueAt(nowTs) {
@@ -573,7 +719,7 @@ class ClsLoggerCore {
573
719
  }
574
720
  return nowTs + this.batchIntervalMs;
575
721
  }
576
- info(message, data = {}) {
722
+ info(message, data = {}, options) {
577
723
  let msg = '';
578
724
  let extra = {};
579
725
  if (message instanceof Error) {
@@ -589,9 +735,18 @@ class ClsLoggerCore {
589
735
  extra = data;
590
736
  }
591
737
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'info');
592
- this.report({ type: 'info', data: payload, timestamp: Date.now() });
738
+ const log = { type: 'info', data: payload, timestamp: Date.now() };
739
+ // info 默认走批量队列,支持 immediate 选项立即发送
740
+ if (options?.immediate) {
741
+ void this.sendReportLogs([log]).catch(() => {
742
+ this.cacheFailedReportLogs([log]);
743
+ });
744
+ }
745
+ else {
746
+ this.report(log);
747
+ }
593
748
  }
594
- warn(message, data = {}) {
749
+ warn(message, data = {}, options) {
595
750
  let msg = '';
596
751
  let extra = {};
597
752
  if (message instanceof Error) {
@@ -607,9 +762,18 @@ class ClsLoggerCore {
607
762
  extra = data;
608
763
  }
609
764
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'warn');
610
- this.report({ type: 'warn', data: payload, timestamp: Date.now() });
765
+ const log = { type: 'warn', data: payload, timestamp: Date.now() };
766
+ // warn 默认走批量队列,支持 immediate 选项立即发送
767
+ if (options?.immediate) {
768
+ void this.sendReportLogs([log]).catch(() => {
769
+ this.cacheFailedReportLogs([log]);
770
+ });
771
+ }
772
+ else {
773
+ this.report(log);
774
+ }
611
775
  }
612
- error(message, data = {}) {
776
+ error(message, data = {}, options) {
613
777
  let msg = '';
614
778
  let extra = {};
615
779
  if (message instanceof Error) {
@@ -625,7 +789,17 @@ class ClsLoggerCore {
625
789
  extra = data;
626
790
  }
627
791
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'error');
628
- this.report({ type: 'error', data: payload, timestamp: Date.now() });
792
+ const log = { type: 'error', data: payload, timestamp: Date.now() };
793
+ // error 默认即时上报,除非显式指定 immediate: false
794
+ const immediate = options?.immediate ?? true;
795
+ if (immediate) {
796
+ void this.sendReportLogs([log]).catch(() => {
797
+ this.cacheFailedReportLogs([log]);
798
+ });
799
+ }
800
+ else {
801
+ this.report(log);
802
+ }
629
803
  }
630
804
  track(trackType, data = {}) {
631
805
  if (!trackType)
@@ -640,10 +814,7 @@ class ClsLoggerCore {
640
814
  * 立即发送内存队列
641
815
  */
642
816
  async flushBatch() {
643
- if (this.batchTimer) {
644
- clearTimeout(this.batchTimer);
645
- this.batchTimer = null;
646
- }
817
+ this.cancelScheduledFlush();
647
818
  this.batchTimerDueAt = null;
648
819
  if (this.memoryQueue.length === 0)
649
820
  return;
@@ -749,7 +920,14 @@ class ClsLoggerCore {
749
920
  // 先清空,再尝试发送
750
921
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
751
922
  this.memoryQueue.unshift(...logs);
752
- void this.flushBatch();
923
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
924
+ if (!this.batchTimer) {
925
+ const now = Date.now();
926
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
927
+ const desiredDelay = Math.max(0, desiredDueAt - now);
928
+ this.batchTimerDueAt = desiredDueAt;
929
+ this.scheduleFlush(desiredDelay);
930
+ }
753
931
  }
754
932
  /**
755
933
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
@@ -1288,6 +1466,13 @@ function installRequestMonitor(report, opts = {}) {
1288
1466
  const DEFAULT_MAX_TEXT$1 = 4000;
1289
1467
  const DEFAULT_DEDUPE_WINDOW_MS$1 = 3000;
1290
1468
  const DEFAULT_DEDUPE_MAX_KEYS$1 = 200;
1469
+ /** 默认忽略的无意义错误信息 */
1470
+ const DEFAULT_IGNORE_MESSAGES$1 = [
1471
+ 'Script error.',
1472
+ 'Script error',
1473
+ /^ResizeObserver loop/,
1474
+ 'Permission was denied',
1475
+ ];
1291
1476
  function truncate$3(s, maxLen) {
1292
1477
  if (!s)
1293
1478
  return s;
@@ -1300,11 +1485,22 @@ function sampleHit$3(sampleRate) {
1300
1485
  return false;
1301
1486
  return Math.random() < sampleRate;
1302
1487
  }
1488
+ /** 检查消息是否应该被忽略 */
1489
+ function shouldIgnoreMessage$1(message, ignorePatterns) {
1490
+ if (!message || ignorePatterns.length === 0)
1491
+ return false;
1492
+ return ignorePatterns.some((pattern) => {
1493
+ if (typeof pattern === 'string') {
1494
+ return message === pattern || message.includes(pattern);
1495
+ }
1496
+ return pattern.test(message);
1497
+ });
1498
+ }
1303
1499
  function getPagePath$1() {
1304
1500
  try {
1305
1501
  if (typeof window === 'undefined')
1306
1502
  return '';
1307
- return window.location?.pathname ?? '';
1503
+ return window.location?.href ?? '';
1308
1504
  }
1309
1505
  catch {
1310
1506
  return '';
@@ -1395,6 +1591,9 @@ function installBrowserErrorMonitor(report, options) {
1395
1591
  };
1396
1592
  if (event && typeof event === 'object' && 'message' in event) {
1397
1593
  payload.message = truncate$3(String(event.message ?? ''), options.maxTextLength);
1594
+ // 检查是否应该忽略此错误
1595
+ if (shouldIgnoreMessage$1(String(event.message ?? ''), options.ignoreMessages))
1596
+ return;
1398
1597
  payload.filename = truncate$3(String(event.filename ?? ''), 500);
1399
1598
  payload.lineno = typeof event.lineno === 'number' ? event.lineno : undefined;
1400
1599
  payload.colno = typeof event.colno === 'number' ? event.colno : undefined;
@@ -1433,6 +1632,9 @@ function installBrowserErrorMonitor(report, options) {
1433
1632
  return;
1434
1633
  const reason = event?.reason;
1435
1634
  const e = normalizeErrorLike$1(reason, options.maxTextLength);
1635
+ // 检查是否应该忽略此错误
1636
+ if (shouldIgnoreMessage$1(e.message, options.ignoreMessages))
1637
+ return;
1436
1638
  const payload = {
1437
1639
  pagePath: getPagePath$1(),
1438
1640
  source: 'unhandledrejection',
@@ -1462,6 +1664,7 @@ function installWebErrorMonitor(report, opts = {}) {
1462
1664
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT$1,
1463
1665
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS$1,
1464
1666
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS$1,
1667
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES$1,
1465
1668
  };
1466
1669
  installBrowserErrorMonitor(report, options);
1467
1670
  }
@@ -1469,6 +1672,13 @@ function installWebErrorMonitor(report, opts = {}) {
1469
1672
  const DEFAULT_MAX_TEXT = 4000;
1470
1673
  const DEFAULT_DEDUPE_WINDOW_MS = 3000;
1471
1674
  const DEFAULT_DEDUPE_MAX_KEYS = 200;
1675
+ /** 默认忽略的无意义错误信息 */
1676
+ const DEFAULT_IGNORE_MESSAGES = [
1677
+ 'Script error.',
1678
+ 'Script error',
1679
+ /^ResizeObserver loop/,
1680
+ 'Permission was denied',
1681
+ ];
1472
1682
  function truncate$2(s, maxLen) {
1473
1683
  if (!s)
1474
1684
  return s;
@@ -1481,12 +1691,33 @@ function sampleHit$2(sampleRate) {
1481
1691
  return false;
1482
1692
  return Math.random() < sampleRate;
1483
1693
  }
1694
+ /** 检查消息是否应该被忽略 */
1695
+ function shouldIgnoreMessage(message, ignorePatterns) {
1696
+ if (!message || ignorePatterns.length === 0)
1697
+ return false;
1698
+ return ignorePatterns.some((pattern) => {
1699
+ if (typeof pattern === 'string') {
1700
+ return message === pattern || message.includes(pattern);
1701
+ }
1702
+ return pattern.test(message);
1703
+ });
1704
+ }
1484
1705
  function getMpPagePath() {
1485
1706
  try {
1486
1707
  const pages = globalThis.getCurrentPages?.();
1487
1708
  if (Array.isArray(pages) && pages.length > 0) {
1488
1709
  const page = pages[pages.length - 1];
1489
- return page.route || page.__route__ || '';
1710
+ const route = page.route || page.__route__;
1711
+ if (typeof route === 'string') {
1712
+ let path = route.startsWith('/') ? route : `/${route}`;
1713
+ const options = page.options || {};
1714
+ const keys = Object.keys(options);
1715
+ if (keys.length > 0) {
1716
+ const qs = keys.map((k) => `${k}=${options[k]}`).join('&');
1717
+ path = `${path}?${qs}`;
1718
+ }
1719
+ return path;
1720
+ }
1490
1721
  }
1491
1722
  return '';
1492
1723
  }
@@ -1575,6 +1806,9 @@ function installMiniProgramErrorMonitor(report, options) {
1575
1806
  if (!sampleHit$2(options.sampleRate))
1576
1807
  return;
1577
1808
  const e = normalizeErrorLike(msg, options.maxTextLength);
1809
+ // 检查是否应该忽略此错误
1810
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1811
+ return;
1578
1812
  const payload = {
1579
1813
  pagePath: getMpPagePath(),
1580
1814
  source: 'wx.onError',
@@ -1602,6 +1836,9 @@ function installMiniProgramErrorMonitor(report, options) {
1602
1836
  if (!sampleHit$2(options.sampleRate))
1603
1837
  return;
1604
1838
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
1839
+ // 检查是否应该忽略此错误
1840
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1841
+ return;
1605
1842
  const payload = {
1606
1843
  pagePath: getMpPagePath(),
1607
1844
  source: 'wx.onUnhandledRejection',
@@ -1633,15 +1870,18 @@ function installMiniProgramErrorMonitor(report, options) {
1633
1870
  try {
1634
1871
  if (sampleHit$2(options.sampleRate)) {
1635
1872
  const e = normalizeErrorLike(args?.[0], options.maxTextLength);
1636
- const payload = {
1637
- pagePath: getMpPagePath(),
1638
- source: 'App.onError',
1639
- message: e.message,
1640
- errorName: e.name,
1641
- stack: e.stack,
1642
- };
1643
- if (shouldReport(buildErrorKey(options.reportType, payload)))
1644
- report(options.reportType, payload);
1873
+ // 检查是否应该忽略此错误
1874
+ if (!shouldIgnoreMessage(e.message, options.ignoreMessages)) {
1875
+ const payload = {
1876
+ pagePath: getMpPagePath(),
1877
+ source: 'App.onError',
1878
+ message: e.message,
1879
+ errorName: e.name,
1880
+ stack: e.stack,
1881
+ };
1882
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
1883
+ report(options.reportType, payload);
1884
+ }
1645
1885
  }
1646
1886
  }
1647
1887
  catch {
@@ -1657,15 +1897,18 @@ function installMiniProgramErrorMonitor(report, options) {
1657
1897
  if (sampleHit$2(options.sampleRate)) {
1658
1898
  const reason = args?.[0]?.reason ?? args?.[0];
1659
1899
  const e = normalizeErrorLike(reason, options.maxTextLength);
1660
- const payload = {
1661
- pagePath: getMpPagePath(),
1662
- source: 'App.onUnhandledRejection',
1663
- message: e.message,
1664
- errorName: e.name,
1665
- stack: e.stack,
1666
- };
1667
- if (shouldReport(buildErrorKey(options.reportType, payload)))
1668
- report(options.reportType, payload);
1900
+ // 检查是否应该忽略此错误
1901
+ if (!shouldIgnoreMessage(e.message, options.ignoreMessages)) {
1902
+ const payload = {
1903
+ pagePath: getMpPagePath(),
1904
+ source: 'App.onUnhandledRejection',
1905
+ message: e.message,
1906
+ errorName: e.name,
1907
+ stack: e.stack,
1908
+ };
1909
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
1910
+ report(options.reportType, payload);
1911
+ }
1669
1912
  }
1670
1913
  }
1671
1914
  catch {
@@ -1696,6 +1939,7 @@ function installMiniErrorMonitor(report, opts = {}) {
1696
1939
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
1697
1940
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
1698
1941
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
1942
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES,
1699
1943
  };
1700
1944
  installMiniProgramErrorMonitor(report, options);
1701
1945
  }
@@ -1741,7 +1985,7 @@ function getPagePath() {
1741
1985
  try {
1742
1986
  if (typeof window === 'undefined')
1743
1987
  return '';
1744
- return window.location?.pathname ?? '';
1988
+ return window.location?.href ?? '';
1745
1989
  }
1746
1990
  catch {
1747
1991
  return '';
@@ -1913,9 +2157,25 @@ function installBrowserPerformanceMonitor(report, options) {
1913
2157
  if (!name || shouldIgnoreUrl(name, ignoreUrls))
1914
2158
  continue;
1915
2159
  const initiatorType = String(entry?.initiatorType ?? '');
1916
- // 对齐文档:关注 fetch/xhr/img/script(同时允许 css/other 但不强制)
2160
+ // 关注 fetch/xhr/img/script(同时允许 css/other 但不强制)
1917
2161
  if (!['xmlhttprequest', 'fetch', 'img', 'script', 'css'].includes(initiatorType))
1918
2162
  continue;
2163
+ // 时序分解(便于定位慢在哪个阶段)
2164
+ const domainLookupStart = entry.domainLookupStart ?? 0;
2165
+ const domainLookupEnd = entry.domainLookupEnd ?? 0;
2166
+ const connectStart = entry.connectStart ?? 0;
2167
+ const connectEnd = entry.connectEnd ?? 0;
2168
+ const requestStart = entry.requestStart ?? 0;
2169
+ const responseStart = entry.responseStart ?? 0;
2170
+ const responseEnd = entry.responseEnd ?? 0;
2171
+ const dns = domainLookupEnd - domainLookupStart;
2172
+ const tcp = connectEnd - connectStart;
2173
+ const ttfb = responseStart - requestStart;
2174
+ const download = responseEnd - responseStart;
2175
+ // 缓存检测:transferSize=0 且 decodedBodySize>0 表示缓存命中
2176
+ const transferSize = entry.transferSize ?? 0;
2177
+ const decodedBodySize = entry.decodedBodySize ?? 0;
2178
+ const cached = transferSize === 0 && decodedBodySize > 0;
1919
2179
  const payload = {
1920
2180
  pagePath: getPagePath(),
1921
2181
  metric: 'resource',
@@ -1923,16 +2183,26 @@ function installBrowserPerformanceMonitor(report, options) {
1923
2183
  url: truncate$1(name, options.maxTextLength),
1924
2184
  startTime: typeof entry?.startTime === 'number' ? entry.startTime : undefined,
1925
2185
  duration: typeof entry?.duration === 'number' ? entry.duration : undefined,
2186
+ // 时序分解(跨域资源可能为 0)
2187
+ dns: dns > 0 ? Math.round(dns) : undefined,
2188
+ tcp: tcp > 0 ? Math.round(tcp) : undefined,
2189
+ ttfb: ttfb > 0 ? Math.round(ttfb) : undefined,
2190
+ download: download > 0 ? Math.round(download) : undefined,
2191
+ // 缓存标记
2192
+ cached,
1926
2193
  };
1927
- // 兼容字段(部分浏览器支持)
1928
- if (typeof entry?.transferSize === 'number')
1929
- payload.transferSize = entry.transferSize;
1930
- if (typeof entry?.encodedBodySize === 'number')
2194
+ // 尺寸字段(跨域资源可能为 0)
2195
+ if (transferSize > 0)
2196
+ payload.transferSize = transferSize;
2197
+ if (typeof entry?.encodedBodySize === 'number' && entry.encodedBodySize > 0) {
1931
2198
  payload.encodedBodySize = entry.encodedBodySize;
1932
- if (typeof entry?.decodedBodySize === 'number')
1933
- payload.decodedBodySize = entry.decodedBodySize;
1934
- if (typeof entry?.nextHopProtocol === 'string')
2199
+ }
2200
+ if (decodedBodySize > 0)
2201
+ payload.decodedBodySize = decodedBodySize;
2202
+ // 协议和状态
2203
+ if (typeof entry?.nextHopProtocol === 'string' && entry.nextHopProtocol) {
1935
2204
  payload.nextHopProtocol = entry.nextHopProtocol;
2205
+ }
1936
2206
  if (typeof entry?.responseStatus === 'number')
1937
2207
  payload.status = entry.responseStatus;
1938
2208
  report(options.reportType, payload);
@@ -2109,7 +2379,7 @@ function writeUvMeta$1(key, meta) {
2109
2379
  function getWebPagePath() {
2110
2380
  if (typeof window === 'undefined')
2111
2381
  return '';
2112
- return window.location?.pathname || '';
2382
+ return window.location?.href || '';
2113
2383
  }
2114
2384
  function buildCommonUvFields$1(uvId, uvMeta, isFirstVisit) {
2115
2385
  return {
@@ -2361,7 +2631,16 @@ function getMiniProgramPagePath() {
2361
2631
  const pages = typeof g.getCurrentPages === 'function' ? g.getCurrentPages() : [];
2362
2632
  const last = Array.isArray(pages) ? pages[pages.length - 1] : undefined;
2363
2633
  const route = (last?.route || last?.__route__);
2364
- return typeof route === 'string' ? route : '';
2634
+ if (typeof route !== 'string')
2635
+ return '';
2636
+ let path = route.startsWith('/') ? route : `/${route}`;
2637
+ const options = last?.options || {};
2638
+ const keys = Object.keys(options);
2639
+ if (keys.length > 0) {
2640
+ const qs = keys.map((k) => `${k}=${options[k]}`).join('&');
2641
+ path = `${path}?${qs}`;
2642
+ }
2643
+ return path;
2365
2644
  }
2366
2645
  catch {
2367
2646
  return '';
@@ -2709,7 +2988,7 @@ function getBrowserDeviceInfo(options) {
2709
2988
  if (options.includeNetwork) {
2710
2989
  try {
2711
2990
  const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
2712
- if (conn && isPlainObject(conn)) {
2991
+ if (conn && typeof conn === 'object') {
2713
2992
  if (typeof conn.effectiveType === 'string')
2714
2993
  out.netEffectiveType = conn.effectiveType;
2715
2994
  if (typeof conn.downlink === 'number')
@@ -2724,6 +3003,43 @@ function getBrowserDeviceInfo(options) {
2724
3003
  // ignore
2725
3004
  }
2726
3005
  }
3006
+ if (options.includeNetworkType) {
3007
+ try {
3008
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
3009
+ if (conn && typeof conn === 'object') {
3010
+ const type = conn.type;
3011
+ const eff = conn.effectiveType;
3012
+ let val = '';
3013
+ if (type === 'wifi' || type === 'ethernet') {
3014
+ val = 'WIFI';
3015
+ }
3016
+ else if (type === 'cellular' || type === 'wimax') {
3017
+ if (eff === 'slow-2g' || eff === '2g')
3018
+ val = '2G';
3019
+ else if (eff === '3g')
3020
+ val = '3G';
3021
+ else if (eff === '4g')
3022
+ val = '4G';
3023
+ else
3024
+ val = '4G';
3025
+ }
3026
+ else {
3027
+ // type 未知或不支持,降级使用 effectiveType
3028
+ if (eff === 'slow-2g' || eff === '2g')
3029
+ val = '2G';
3030
+ else if (eff === '3g')
3031
+ val = '3G';
3032
+ else if (eff === '4g')
3033
+ val = '4G';
3034
+ }
3035
+ if (val)
3036
+ out.networkType = val;
3037
+ }
3038
+ }
3039
+ catch {
3040
+ // ignore
3041
+ }
3042
+ }
2727
3043
  return out;
2728
3044
  }
2729
3045
  function createWebDeviceInfoBaseFields(opts) {
@@ -2753,7 +3069,7 @@ function createWebDeviceInfoBaseFields(opts) {
2753
3069
  try {
2754
3070
  const g = globalThis;
2755
3071
  const extra = g[globalKey];
2756
- if (extra && isPlainObject(extra))
3072
+ if (extra && typeof extra === 'object')
2757
3073
  return { ...base, ...extra };
2758
3074
  }
2759
3075
  catch {
@@ -2790,7 +3106,7 @@ function getMiniProgramDeviceInfo(options) {
2790
3106
  try {
2791
3107
  if (typeof wxAny.getNetworkTypeSync === 'function') {
2792
3108
  const n = wxAny.getNetworkTypeSync();
2793
- out.networkType = n?.networkType ? String(n.networkType) : '';
3109
+ out.networkType = n?.networkType ? String(n.networkType).toUpperCase() : '';
2794
3110
  }
2795
3111
  else if (typeof wxAny.getNetworkType === 'function') {
2796
3112
  // 异步更新:先不阻塞初始化
@@ -2800,7 +3116,9 @@ function getMiniProgramDeviceInfo(options) {
2800
3116
  const g = globalThis;
2801
3117
  if (!g.__beLinkClsLoggerDeviceInfo__)
2802
3118
  g.__beLinkClsLoggerDeviceInfo__ = {};
2803
- g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType ? String(res.networkType) : '';
3119
+ g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType
3120
+ ? String(res.networkType).toUpperCase()
3121
+ : '';
2804
3122
  }
2805
3123
  catch {
2806
3124
  // ignore
@@ -2819,7 +3137,7 @@ function getMiniProgramDeviceInfo(options) {
2819
3137
  const g = globalThis;
2820
3138
  if (!g.__beLinkClsLoggerDeviceInfo__)
2821
3139
  g.__beLinkClsLoggerDeviceInfo__ = {};
2822
- g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType ? String(res.networkType) : '';
3140
+ g.__beLinkClsLoggerDeviceInfo__.networkType = res?.networkType ? String(res.networkType).toUpperCase() : '';
2823
3141
  }
2824
3142
  catch {
2825
3143
  // ignore
@@ -2860,7 +3178,7 @@ function createMiniDeviceInfoBaseFields(opts) {
2860
3178
  try {
2861
3179
  const g = globalThis;
2862
3180
  const extra = g[globalKey];
2863
- if (extra && isPlainObject(extra))
3181
+ if (extra && typeof extra === 'object')
2864
3182
  return { ...base, ...extra };
2865
3183
  }
2866
3184
  catch {