@be-link/cls-logger 1.0.10 → 1.0.12

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