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