@be-link/cls-logger 1.0.1-beta.17 → 1.0.1-beta.19

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
@@ -180,6 +180,9 @@ class ClsLoggerCore {
180
180
  this.batchTimerDueAt = null;
181
181
  this.initTs = 0;
182
182
  this.startupDelayMs = 0;
183
+ this.useIdleCallback = false;
184
+ this.idleTimeout = 3000;
185
+ this.visibilityCleanup = null;
183
186
  // 参考文档:失败缓存 + 重试
184
187
  this.failedCacheKey = 'cls_failed_logs';
185
188
  this.failedCacheMax = 200;
@@ -249,6 +252,8 @@ class ClsLoggerCore {
249
252
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
250
253
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
251
254
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
255
+ this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
256
+ this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
252
257
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
253
258
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
254
259
  // 预热(避免首条日志触发 import/初始化开销)
@@ -258,6 +263,8 @@ class ClsLoggerCore {
258
263
  if (this.enabled) {
259
264
  // 启动时尝试发送失败缓存
260
265
  this.flushFailed();
266
+ // 添加页面可见性监听(确保页面关闭时数据不丢失)
267
+ this.setupVisibilityListener();
261
268
  // 初始化后立即启动请求监听
262
269
  this.startRequestMonitor(options.requestMonitor);
263
270
  // 初始化后立即启动错误监控/性能监控
@@ -294,6 +301,89 @@ class ClsLoggerCore {
294
301
  return auto;
295
302
  return undefined;
296
303
  }
304
+ /**
305
+ * 设置页面可见性监听
306
+ * - visibilitychange: 页面隐藏时使用 sendBeacon 发送队列
307
+ * - pagehide: 作为移动端 fallback
308
+ */
309
+ setupVisibilityListener() {
310
+ if (typeof document === 'undefined' || typeof window === 'undefined')
311
+ return;
312
+ // 避免重复监听
313
+ if (this.visibilityCleanup)
314
+ return;
315
+ const handleVisibilityChange = () => {
316
+ if (document.visibilityState === 'hidden') {
317
+ this.flushBatchSync();
318
+ }
319
+ };
320
+ const handlePageHide = () => {
321
+ this.flushBatchSync();
322
+ };
323
+ document.addEventListener('visibilitychange', handleVisibilityChange);
324
+ window.addEventListener('pagehide', handlePageHide);
325
+ this.visibilityCleanup = () => {
326
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
327
+ window.removeEventListener('pagehide', handlePageHide);
328
+ };
329
+ }
330
+ /**
331
+ * 同步发送内存队列(使用 sendBeacon)
332
+ * - 用于页面关闭时确保数据发送
333
+ * - sendBeacon 不可用时降级为缓存到 localStorage
334
+ */
335
+ flushBatchSync() {
336
+ if (this.memoryQueue.length === 0)
337
+ return;
338
+ // 清除定时器
339
+ if (this.batchTimer) {
340
+ try {
341
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
342
+ cancelIdleCallback(this.batchTimer);
343
+ }
344
+ else {
345
+ clearTimeout(this.batchTimer);
346
+ }
347
+ }
348
+ catch {
349
+ // ignore
350
+ }
351
+ this.batchTimer = null;
352
+ }
353
+ this.batchTimerDueAt = null;
354
+ const logs = [...this.memoryQueue];
355
+ this.memoryQueue = [];
356
+ // 优先使用 sendBeacon(页面关闭时可靠发送)
357
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
358
+ try {
359
+ const payload = this.buildSendBeaconPayload(logs);
360
+ const blob = new Blob([payload], { type: 'application/json' });
361
+ const url = `${this.endpoint}/structuredlog?topic_id=${this.topicId}`;
362
+ const success = navigator.sendBeacon(url, blob);
363
+ if (!success) {
364
+ // sendBeacon 返回 false 时,降级缓存
365
+ this.cacheFailedReportLogs(logs);
366
+ }
367
+ }
368
+ catch {
369
+ this.cacheFailedReportLogs(logs);
370
+ }
371
+ }
372
+ else {
373
+ // 不支持 sendBeacon,降级缓存到 localStorage
374
+ this.cacheFailedReportLogs(logs);
375
+ }
376
+ }
377
+ /**
378
+ * 构建 sendBeacon 的 payload
379
+ */
380
+ buildSendBeaconPayload(logs) {
381
+ const logList = logs.map((log) => this.buildReportFields(log));
382
+ return JSON.stringify({
383
+ source: this.source,
384
+ logs: logList,
385
+ });
386
+ }
297
387
  startRequestMonitor(requestMonitor) {
298
388
  if (this.requestMonitorStarted)
299
389
  return;
@@ -548,25 +638,55 @@ class ClsLoggerCore {
548
638
  const desiredDelay = Math.max(0, desiredDueAt - now);
549
639
  if (!this.batchTimer) {
550
640
  this.batchTimerDueAt = desiredDueAt;
551
- this.batchTimer = setTimeout(() => {
552
- void this.flushBatch();
553
- }, desiredDelay);
641
+ this.scheduleFlush(desiredDelay);
554
642
  return;
555
643
  }
556
- // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
644
+ // 启动合并窗口内:如果当前 timer 会"更早"触发,则延后到窗口结束,尽量减少多次发送
557
645
  if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
558
- try {
559
- clearTimeout(this.batchTimer);
560
- }
561
- catch {
562
- // ignore
563
- }
646
+ this.cancelScheduledFlush();
564
647
  this.batchTimerDueAt = desiredDueAt;
648
+ this.scheduleFlush(desiredDelay);
649
+ }
650
+ }
651
+ /**
652
+ * 调度批量发送
653
+ * - 支持 requestIdleCallback(浏览器空闲时执行)
654
+ * - 降级为 setTimeout
655
+ */
656
+ scheduleFlush(desiredDelay) {
657
+ if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
658
+ // 使用 requestIdleCallback,设置 timeout 保证最终执行
659
+ const idleId = requestIdleCallback(() => {
660
+ void this.flushBatch();
661
+ }, { timeout: Math.max(desiredDelay, this.idleTimeout) });
662
+ // 存储 idleId 以便清理(类型兼容处理)
663
+ this.batchTimer = idleId;
664
+ }
665
+ else {
565
666
  this.batchTimer = setTimeout(() => {
566
667
  void this.flushBatch();
567
668
  }, desiredDelay);
568
669
  }
569
670
  }
671
+ /**
672
+ * 取消已调度的批量发送
673
+ */
674
+ cancelScheduledFlush() {
675
+ if (!this.batchTimer)
676
+ return;
677
+ try {
678
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
679
+ cancelIdleCallback(this.batchTimer);
680
+ }
681
+ else {
682
+ clearTimeout(this.batchTimer);
683
+ }
684
+ }
685
+ catch {
686
+ // ignore
687
+ }
688
+ this.batchTimer = null;
689
+ }
570
690
  getDesiredBatchFlushDueAt(nowTs) {
571
691
  const start = this.initTs || nowTs;
572
692
  const startupDelay = Number.isFinite(this.startupDelayMs) ? Math.max(0, this.startupDelayMs) : 0;
@@ -577,7 +697,7 @@ class ClsLoggerCore {
577
697
  }
578
698
  return nowTs + this.batchIntervalMs;
579
699
  }
580
- info(message, data = {}) {
700
+ info(message, data = {}, options) {
581
701
  let msg = '';
582
702
  let extra = {};
583
703
  if (message instanceof Error) {
@@ -593,9 +713,18 @@ class ClsLoggerCore {
593
713
  extra = data;
594
714
  }
595
715
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'info');
596
- this.report({ type: 'info', data: payload, timestamp: Date.now() });
716
+ const log = { type: 'info', data: payload, timestamp: Date.now() };
717
+ // info 默认走批量队列,支持 immediate 选项立即发送
718
+ if (options?.immediate) {
719
+ void this.sendReportLogs([log]).catch(() => {
720
+ this.cacheFailedReportLogs([log]);
721
+ });
722
+ }
723
+ else {
724
+ this.report(log);
725
+ }
597
726
  }
598
- warn(message, data = {}) {
727
+ warn(message, data = {}, options) {
599
728
  let msg = '';
600
729
  let extra = {};
601
730
  if (message instanceof Error) {
@@ -611,9 +740,18 @@ class ClsLoggerCore {
611
740
  extra = data;
612
741
  }
613
742
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'warn');
614
- this.report({ type: 'warn', data: payload, timestamp: Date.now() });
743
+ const log = { type: 'warn', data: payload, timestamp: Date.now() };
744
+ // warn 默认走批量队列,支持 immediate 选项立即发送
745
+ if (options?.immediate) {
746
+ void this.sendReportLogs([log]).catch(() => {
747
+ this.cacheFailedReportLogs([log]);
748
+ });
749
+ }
750
+ else {
751
+ this.report(log);
752
+ }
615
753
  }
616
- error(message, data = {}) {
754
+ error(message, data = {}, options) {
617
755
  let msg = '';
618
756
  let extra = {};
619
757
  if (message instanceof Error) {
@@ -629,7 +767,17 @@ class ClsLoggerCore {
629
767
  extra = data;
630
768
  }
631
769
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'error');
632
- this.report({ type: 'error', data: payload, timestamp: Date.now() });
770
+ const log = { type: 'error', data: payload, timestamp: Date.now() };
771
+ // error 默认即时上报,除非显式指定 immediate: false
772
+ const immediate = options?.immediate ?? true;
773
+ if (immediate) {
774
+ void this.sendReportLogs([log]).catch(() => {
775
+ this.cacheFailedReportLogs([log]);
776
+ });
777
+ }
778
+ else {
779
+ this.report(log);
780
+ }
633
781
  }
634
782
  track(trackType, data = {}) {
635
783
  if (!trackType)
@@ -644,10 +792,7 @@ class ClsLoggerCore {
644
792
  * 立即发送内存队列
645
793
  */
646
794
  async flushBatch() {
647
- if (this.batchTimer) {
648
- clearTimeout(this.batchTimer);
649
- this.batchTimer = null;
650
- }
795
+ this.cancelScheduledFlush();
651
796
  this.batchTimerDueAt = null;
652
797
  if (this.memoryQueue.length === 0)
653
798
  return;
@@ -1292,6 +1437,13 @@ function installRequestMonitor(report, opts = {}) {
1292
1437
  const DEFAULT_MAX_TEXT$1 = 4000;
1293
1438
  const DEFAULT_DEDUPE_WINDOW_MS$1 = 3000;
1294
1439
  const DEFAULT_DEDUPE_MAX_KEYS$1 = 200;
1440
+ /** 默认忽略的无意义错误信息 */
1441
+ const DEFAULT_IGNORE_MESSAGES$1 = [
1442
+ 'Script error.',
1443
+ 'Script error',
1444
+ /^ResizeObserver loop/,
1445
+ 'Permission was denied',
1446
+ ];
1295
1447
  function truncate$3(s, maxLen) {
1296
1448
  if (!s)
1297
1449
  return s;
@@ -1304,6 +1456,17 @@ function sampleHit$3(sampleRate) {
1304
1456
  return false;
1305
1457
  return Math.random() < sampleRate;
1306
1458
  }
1459
+ /** 检查消息是否应该被忽略 */
1460
+ function shouldIgnoreMessage$1(message, ignorePatterns) {
1461
+ if (!message || ignorePatterns.length === 0)
1462
+ return false;
1463
+ return ignorePatterns.some((pattern) => {
1464
+ if (typeof pattern === 'string') {
1465
+ return message === pattern || message.includes(pattern);
1466
+ }
1467
+ return pattern.test(message);
1468
+ });
1469
+ }
1307
1470
  function getPagePath$1() {
1308
1471
  try {
1309
1472
  if (typeof window === 'undefined')
@@ -1399,6 +1562,9 @@ function installBrowserErrorMonitor(report, options) {
1399
1562
  };
1400
1563
  if (event && typeof event === 'object' && 'message' in event) {
1401
1564
  payload.message = truncate$3(String(event.message ?? ''), options.maxTextLength);
1565
+ // 检查是否应该忽略此错误
1566
+ if (shouldIgnoreMessage$1(String(event.message ?? ''), options.ignoreMessages))
1567
+ return;
1402
1568
  payload.filename = truncate$3(String(event.filename ?? ''), 500);
1403
1569
  payload.lineno = typeof event.lineno === 'number' ? event.lineno : undefined;
1404
1570
  payload.colno = typeof event.colno === 'number' ? event.colno : undefined;
@@ -1437,6 +1603,9 @@ function installBrowserErrorMonitor(report, options) {
1437
1603
  return;
1438
1604
  const reason = event?.reason;
1439
1605
  const e = normalizeErrorLike$1(reason, options.maxTextLength);
1606
+ // 检查是否应该忽略此错误
1607
+ if (shouldIgnoreMessage$1(e.message, options.ignoreMessages))
1608
+ return;
1440
1609
  const payload = {
1441
1610
  pagePath: getPagePath$1(),
1442
1611
  source: 'unhandledrejection',
@@ -1466,6 +1635,7 @@ function installWebErrorMonitor(report, opts = {}) {
1466
1635
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT$1,
1467
1636
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS$1,
1468
1637
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS$1,
1638
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES$1,
1469
1639
  };
1470
1640
  installBrowserErrorMonitor(report, options);
1471
1641
  }
@@ -1473,6 +1643,13 @@ function installWebErrorMonitor(report, opts = {}) {
1473
1643
  const DEFAULT_MAX_TEXT = 4000;
1474
1644
  const DEFAULT_DEDUPE_WINDOW_MS = 3000;
1475
1645
  const DEFAULT_DEDUPE_MAX_KEYS = 200;
1646
+ /** 默认忽略的无意义错误信息 */
1647
+ const DEFAULT_IGNORE_MESSAGES = [
1648
+ 'Script error.',
1649
+ 'Script error',
1650
+ /^ResizeObserver loop/,
1651
+ 'Permission was denied',
1652
+ ];
1476
1653
  function truncate$2(s, maxLen) {
1477
1654
  if (!s)
1478
1655
  return s;
@@ -1485,6 +1662,17 @@ function sampleHit$2(sampleRate) {
1485
1662
  return false;
1486
1663
  return Math.random() < sampleRate;
1487
1664
  }
1665
+ /** 检查消息是否应该被忽略 */
1666
+ function shouldIgnoreMessage(message, ignorePatterns) {
1667
+ if (!message || ignorePatterns.length === 0)
1668
+ return false;
1669
+ return ignorePatterns.some((pattern) => {
1670
+ if (typeof pattern === 'string') {
1671
+ return message === pattern || message.includes(pattern);
1672
+ }
1673
+ return pattern.test(message);
1674
+ });
1675
+ }
1488
1676
  function getMpPagePath() {
1489
1677
  try {
1490
1678
  const pages = globalThis.getCurrentPages?.();
@@ -1579,6 +1767,9 @@ function installMiniProgramErrorMonitor(report, options) {
1579
1767
  if (!sampleHit$2(options.sampleRate))
1580
1768
  return;
1581
1769
  const e = normalizeErrorLike(msg, options.maxTextLength);
1770
+ // 检查是否应该忽略此错误
1771
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1772
+ return;
1582
1773
  const payload = {
1583
1774
  pagePath: getMpPagePath(),
1584
1775
  source: 'wx.onError',
@@ -1606,6 +1797,9 @@ function installMiniProgramErrorMonitor(report, options) {
1606
1797
  if (!sampleHit$2(options.sampleRate))
1607
1798
  return;
1608
1799
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
1800
+ // 检查是否应该忽略此错误
1801
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1802
+ return;
1609
1803
  const payload = {
1610
1804
  pagePath: getMpPagePath(),
1611
1805
  source: 'wx.onUnhandledRejection',
@@ -1637,15 +1831,18 @@ function installMiniProgramErrorMonitor(report, options) {
1637
1831
  try {
1638
1832
  if (sampleHit$2(options.sampleRate)) {
1639
1833
  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);
1834
+ // 检查是否应该忽略此错误
1835
+ if (!shouldIgnoreMessage(e.message, options.ignoreMessages)) {
1836
+ const payload = {
1837
+ pagePath: getMpPagePath(),
1838
+ source: 'App.onError',
1839
+ message: e.message,
1840
+ errorName: e.name,
1841
+ stack: e.stack,
1842
+ };
1843
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
1844
+ report(options.reportType, payload);
1845
+ }
1649
1846
  }
1650
1847
  }
1651
1848
  catch {
@@ -1661,15 +1858,18 @@ function installMiniProgramErrorMonitor(report, options) {
1661
1858
  if (sampleHit$2(options.sampleRate)) {
1662
1859
  const reason = args?.[0]?.reason ?? args?.[0];
1663
1860
  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);
1861
+ // 检查是否应该忽略此错误
1862
+ if (!shouldIgnoreMessage(e.message, options.ignoreMessages)) {
1863
+ const payload = {
1864
+ pagePath: getMpPagePath(),
1865
+ source: 'App.onUnhandledRejection',
1866
+ message: e.message,
1867
+ errorName: e.name,
1868
+ stack: e.stack,
1869
+ };
1870
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
1871
+ report(options.reportType, payload);
1872
+ }
1673
1873
  }
1674
1874
  }
1675
1875
  catch {
@@ -1700,6 +1900,7 @@ function installMiniErrorMonitor(report, opts = {}) {
1700
1900
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
1701
1901
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
1702
1902
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
1903
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES,
1703
1904
  };
1704
1905
  installMiniProgramErrorMonitor(report, options);
1705
1906
  }