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

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,9 @@
182
182
  this.batchTimerDueAt = null;
183
183
  this.initTs = 0;
184
184
  this.startupDelayMs = 0;
185
+ this.useIdleCallback = false;
186
+ this.idleTimeout = 3000;
187
+ this.visibilityCleanup = null;
185
188
  // 参考文档:失败缓存 + 重试
186
189
  this.failedCacheKey = 'cls_failed_logs';
187
190
  this.failedCacheMax = 200;
@@ -251,6 +254,8 @@
251
254
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
252
255
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
253
256
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
257
+ this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
258
+ this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
254
259
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
255
260
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
256
261
  // 预热(避免首条日志触发 import/初始化开销)
@@ -260,6 +265,8 @@
260
265
  if (this.enabled) {
261
266
  // 启动时尝试发送失败缓存
262
267
  this.flushFailed();
268
+ // 添加页面可见性监听(确保页面关闭时数据不丢失)
269
+ this.setupVisibilityListener();
263
270
  // 初始化后立即启动请求监听
264
271
  this.startRequestMonitor(options.requestMonitor);
265
272
  // 初始化后立即启动错误监控/性能监控
@@ -296,6 +303,89 @@
296
303
  return auto;
297
304
  return undefined;
298
305
  }
306
+ /**
307
+ * 设置页面可见性监听
308
+ * - visibilitychange: 页面隐藏时使用 sendBeacon 发送队列
309
+ * - pagehide: 作为移动端 fallback
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
+ this.flushBatchSync();
320
+ }
321
+ };
322
+ const handlePageHide = () => {
323
+ this.flushBatchSync();
324
+ };
325
+ document.addEventListener('visibilitychange', handleVisibilityChange);
326
+ window.addEventListener('pagehide', handlePageHide);
327
+ this.visibilityCleanup = () => {
328
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
329
+ window.removeEventListener('pagehide', handlePageHide);
330
+ };
331
+ }
332
+ /**
333
+ * 同步发送内存队列(使用 sendBeacon)
334
+ * - 用于页面关闭时确保数据发送
335
+ * - sendBeacon 不可用时降级为缓存到 localStorage
336
+ */
337
+ flushBatchSync() {
338
+ if (this.memoryQueue.length === 0)
339
+ return;
340
+ // 清除定时器
341
+ if (this.batchTimer) {
342
+ try {
343
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
344
+ cancelIdleCallback(this.batchTimer);
345
+ }
346
+ else {
347
+ clearTimeout(this.batchTimer);
348
+ }
349
+ }
350
+ catch {
351
+ // ignore
352
+ }
353
+ this.batchTimer = null;
354
+ }
355
+ this.batchTimerDueAt = null;
356
+ const logs = [...this.memoryQueue];
357
+ this.memoryQueue = [];
358
+ // 优先使用 sendBeacon(页面关闭时可靠发送)
359
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
360
+ try {
361
+ const payload = this.buildSendBeaconPayload(logs);
362
+ const blob = new Blob([payload], { type: 'application/json' });
363
+ const url = `${this.endpoint}/structuredlog?topic_id=${this.topicId}`;
364
+ const success = navigator.sendBeacon(url, blob);
365
+ if (!success) {
366
+ // sendBeacon 返回 false 时,降级缓存
367
+ this.cacheFailedReportLogs(logs);
368
+ }
369
+ }
370
+ catch {
371
+ this.cacheFailedReportLogs(logs);
372
+ }
373
+ }
374
+ else {
375
+ // 不支持 sendBeacon,降级缓存到 localStorage
376
+ this.cacheFailedReportLogs(logs);
377
+ }
378
+ }
379
+ /**
380
+ * 构建 sendBeacon 的 payload
381
+ */
382
+ buildSendBeaconPayload(logs) {
383
+ const logList = logs.map((log) => this.buildReportFields(log));
384
+ return JSON.stringify({
385
+ source: this.source,
386
+ logs: logList,
387
+ });
388
+ }
299
389
  startRequestMonitor(requestMonitor) {
300
390
  if (this.requestMonitorStarted)
301
391
  return;
@@ -550,25 +640,55 @@
550
640
  const desiredDelay = Math.max(0, desiredDueAt - now);
551
641
  if (!this.batchTimer) {
552
642
  this.batchTimerDueAt = desiredDueAt;
553
- this.batchTimer = setTimeout(() => {
554
- void this.flushBatch();
555
- }, desiredDelay);
643
+ this.scheduleFlush(desiredDelay);
556
644
  return;
557
645
  }
558
- // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
646
+ // 启动合并窗口内:如果当前 timer 会"更早"触发,则延后到窗口结束,尽量减少多次发送
559
647
  if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
560
- try {
561
- clearTimeout(this.batchTimer);
562
- }
563
- catch {
564
- // ignore
565
- }
648
+ this.cancelScheduledFlush();
566
649
  this.batchTimerDueAt = desiredDueAt;
650
+ this.scheduleFlush(desiredDelay);
651
+ }
652
+ }
653
+ /**
654
+ * 调度批量发送
655
+ * - 支持 requestIdleCallback(浏览器空闲时执行)
656
+ * - 降级为 setTimeout
657
+ */
658
+ scheduleFlush(desiredDelay) {
659
+ if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
660
+ // 使用 requestIdleCallback,设置 timeout 保证最终执行
661
+ const idleId = requestIdleCallback(() => {
662
+ void this.flushBatch();
663
+ }, { timeout: Math.max(desiredDelay, this.idleTimeout) });
664
+ // 存储 idleId 以便清理(类型兼容处理)
665
+ this.batchTimer = idleId;
666
+ }
667
+ else {
567
668
  this.batchTimer = setTimeout(() => {
568
669
  void this.flushBatch();
569
670
  }, desiredDelay);
570
671
  }
571
672
  }
673
+ /**
674
+ * 取消已调度的批量发送
675
+ */
676
+ cancelScheduledFlush() {
677
+ if (!this.batchTimer)
678
+ return;
679
+ try {
680
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
681
+ cancelIdleCallback(this.batchTimer);
682
+ }
683
+ else {
684
+ clearTimeout(this.batchTimer);
685
+ }
686
+ }
687
+ catch {
688
+ // ignore
689
+ }
690
+ this.batchTimer = null;
691
+ }
572
692
  getDesiredBatchFlushDueAt(nowTs) {
573
693
  const start = this.initTs || nowTs;
574
694
  const startupDelay = Number.isFinite(this.startupDelayMs) ? Math.max(0, this.startupDelayMs) : 0;
@@ -579,7 +699,7 @@
579
699
  }
580
700
  return nowTs + this.batchIntervalMs;
581
701
  }
582
- info(message, data = {}) {
702
+ info(message, data = {}, options) {
583
703
  let msg = '';
584
704
  let extra = {};
585
705
  if (message instanceof Error) {
@@ -595,9 +715,18 @@
595
715
  extra = data;
596
716
  }
597
717
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'info');
598
- this.report({ type: 'info', data: payload, timestamp: Date.now() });
718
+ const log = { type: 'info', data: payload, timestamp: Date.now() };
719
+ // info 默认走批量队列,支持 immediate 选项立即发送
720
+ if (options?.immediate) {
721
+ void this.sendReportLogs([log]).catch(() => {
722
+ this.cacheFailedReportLogs([log]);
723
+ });
724
+ }
725
+ else {
726
+ this.report(log);
727
+ }
599
728
  }
600
- warn(message, data = {}) {
729
+ warn(message, data = {}, options) {
601
730
  let msg = '';
602
731
  let extra = {};
603
732
  if (message instanceof Error) {
@@ -613,9 +742,18 @@
613
742
  extra = data;
614
743
  }
615
744
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'warn');
616
- this.report({ type: 'warn', data: payload, timestamp: Date.now() });
745
+ const log = { type: 'warn', data: payload, timestamp: Date.now() };
746
+ // warn 默认走批量队列,支持 immediate 选项立即发送
747
+ if (options?.immediate) {
748
+ void this.sendReportLogs([log]).catch(() => {
749
+ this.cacheFailedReportLogs([log]);
750
+ });
751
+ }
752
+ else {
753
+ this.report(log);
754
+ }
617
755
  }
618
- error(message, data = {}) {
756
+ error(message, data = {}, options) {
619
757
  let msg = '';
620
758
  let extra = {};
621
759
  if (message instanceof Error) {
@@ -631,7 +769,17 @@
631
769
  extra = data;
632
770
  }
633
771
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'error');
634
- this.report({ type: 'error', data: payload, timestamp: Date.now() });
772
+ const log = { type: 'error', data: payload, timestamp: Date.now() };
773
+ // error 默认即时上报,除非显式指定 immediate: false
774
+ const immediate = options?.immediate ?? true;
775
+ if (immediate) {
776
+ void this.sendReportLogs([log]).catch(() => {
777
+ this.cacheFailedReportLogs([log]);
778
+ });
779
+ }
780
+ else {
781
+ this.report(log);
782
+ }
635
783
  }
636
784
  track(trackType, data = {}) {
637
785
  if (!trackType)
@@ -646,10 +794,7 @@
646
794
  * 立即发送内存队列
647
795
  */
648
796
  async flushBatch() {
649
- if (this.batchTimer) {
650
- clearTimeout(this.batchTimer);
651
- this.batchTimer = null;
652
- }
797
+ this.cancelScheduledFlush();
653
798
  this.batchTimerDueAt = null;
654
799
  if (this.memoryQueue.length === 0)
655
800
  return;
package/dist/mini.esm.js CHANGED
@@ -178,6 +178,9 @@ class ClsLoggerCore {
178
178
  this.batchTimerDueAt = null;
179
179
  this.initTs = 0;
180
180
  this.startupDelayMs = 0;
181
+ this.useIdleCallback = false;
182
+ this.idleTimeout = 3000;
183
+ this.visibilityCleanup = null;
181
184
  // 参考文档:失败缓存 + 重试
182
185
  this.failedCacheKey = 'cls_failed_logs';
183
186
  this.failedCacheMax = 200;
@@ -247,6 +250,8 @@ class ClsLoggerCore {
247
250
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
248
251
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
249
252
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
253
+ this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
254
+ this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
250
255
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
251
256
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
252
257
  // 预热(避免首条日志触发 import/初始化开销)
@@ -256,6 +261,8 @@ class ClsLoggerCore {
256
261
  if (this.enabled) {
257
262
  // 启动时尝试发送失败缓存
258
263
  this.flushFailed();
264
+ // 添加页面可见性监听(确保页面关闭时数据不丢失)
265
+ this.setupVisibilityListener();
259
266
  // 初始化后立即启动请求监听
260
267
  this.startRequestMonitor(options.requestMonitor);
261
268
  // 初始化后立即启动错误监控/性能监控
@@ -292,6 +299,89 @@ class ClsLoggerCore {
292
299
  return auto;
293
300
  return undefined;
294
301
  }
302
+ /**
303
+ * 设置页面可见性监听
304
+ * - visibilitychange: 页面隐藏时使用 sendBeacon 发送队列
305
+ * - pagehide: 作为移动端 fallback
306
+ */
307
+ setupVisibilityListener() {
308
+ if (typeof document === 'undefined' || typeof window === 'undefined')
309
+ return;
310
+ // 避免重复监听
311
+ if (this.visibilityCleanup)
312
+ return;
313
+ const handleVisibilityChange = () => {
314
+ if (document.visibilityState === 'hidden') {
315
+ this.flushBatchSync();
316
+ }
317
+ };
318
+ const handlePageHide = () => {
319
+ this.flushBatchSync();
320
+ };
321
+ document.addEventListener('visibilitychange', handleVisibilityChange);
322
+ window.addEventListener('pagehide', handlePageHide);
323
+ this.visibilityCleanup = () => {
324
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
325
+ window.removeEventListener('pagehide', handlePageHide);
326
+ };
327
+ }
328
+ /**
329
+ * 同步发送内存队列(使用 sendBeacon)
330
+ * - 用于页面关闭时确保数据发送
331
+ * - sendBeacon 不可用时降级为缓存到 localStorage
332
+ */
333
+ flushBatchSync() {
334
+ if (this.memoryQueue.length === 0)
335
+ return;
336
+ // 清除定时器
337
+ if (this.batchTimer) {
338
+ try {
339
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
340
+ cancelIdleCallback(this.batchTimer);
341
+ }
342
+ else {
343
+ clearTimeout(this.batchTimer);
344
+ }
345
+ }
346
+ catch {
347
+ // ignore
348
+ }
349
+ this.batchTimer = null;
350
+ }
351
+ this.batchTimerDueAt = null;
352
+ const logs = [...this.memoryQueue];
353
+ this.memoryQueue = [];
354
+ // 优先使用 sendBeacon(页面关闭时可靠发送)
355
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
356
+ try {
357
+ const payload = this.buildSendBeaconPayload(logs);
358
+ const blob = new Blob([payload], { type: 'application/json' });
359
+ const url = `${this.endpoint}/structuredlog?topic_id=${this.topicId}`;
360
+ const success = navigator.sendBeacon(url, blob);
361
+ if (!success) {
362
+ // sendBeacon 返回 false 时,降级缓存
363
+ this.cacheFailedReportLogs(logs);
364
+ }
365
+ }
366
+ catch {
367
+ this.cacheFailedReportLogs(logs);
368
+ }
369
+ }
370
+ else {
371
+ // 不支持 sendBeacon,降级缓存到 localStorage
372
+ this.cacheFailedReportLogs(logs);
373
+ }
374
+ }
375
+ /**
376
+ * 构建 sendBeacon 的 payload
377
+ */
378
+ buildSendBeaconPayload(logs) {
379
+ const logList = logs.map((log) => this.buildReportFields(log));
380
+ return JSON.stringify({
381
+ source: this.source,
382
+ logs: logList,
383
+ });
384
+ }
295
385
  startRequestMonitor(requestMonitor) {
296
386
  if (this.requestMonitorStarted)
297
387
  return;
@@ -546,25 +636,55 @@ class ClsLoggerCore {
546
636
  const desiredDelay = Math.max(0, desiredDueAt - now);
547
637
  if (!this.batchTimer) {
548
638
  this.batchTimerDueAt = desiredDueAt;
549
- this.batchTimer = setTimeout(() => {
550
- void this.flushBatch();
551
- }, desiredDelay);
639
+ this.scheduleFlush(desiredDelay);
552
640
  return;
553
641
  }
554
- // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
642
+ // 启动合并窗口内:如果当前 timer 会"更早"触发,则延后到窗口结束,尽量减少多次发送
555
643
  if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
556
- try {
557
- clearTimeout(this.batchTimer);
558
- }
559
- catch {
560
- // ignore
561
- }
644
+ this.cancelScheduledFlush();
562
645
  this.batchTimerDueAt = desiredDueAt;
646
+ this.scheduleFlush(desiredDelay);
647
+ }
648
+ }
649
+ /**
650
+ * 调度批量发送
651
+ * - 支持 requestIdleCallback(浏览器空闲时执行)
652
+ * - 降级为 setTimeout
653
+ */
654
+ scheduleFlush(desiredDelay) {
655
+ if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
656
+ // 使用 requestIdleCallback,设置 timeout 保证最终执行
657
+ const idleId = requestIdleCallback(() => {
658
+ void this.flushBatch();
659
+ }, { timeout: Math.max(desiredDelay, this.idleTimeout) });
660
+ // 存储 idleId 以便清理(类型兼容处理)
661
+ this.batchTimer = idleId;
662
+ }
663
+ else {
563
664
  this.batchTimer = setTimeout(() => {
564
665
  void this.flushBatch();
565
666
  }, desiredDelay);
566
667
  }
567
668
  }
669
+ /**
670
+ * 取消已调度的批量发送
671
+ */
672
+ cancelScheduledFlush() {
673
+ if (!this.batchTimer)
674
+ return;
675
+ try {
676
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
677
+ cancelIdleCallback(this.batchTimer);
678
+ }
679
+ else {
680
+ clearTimeout(this.batchTimer);
681
+ }
682
+ }
683
+ catch {
684
+ // ignore
685
+ }
686
+ this.batchTimer = null;
687
+ }
568
688
  getDesiredBatchFlushDueAt(nowTs) {
569
689
  const start = this.initTs || nowTs;
570
690
  const startupDelay = Number.isFinite(this.startupDelayMs) ? Math.max(0, this.startupDelayMs) : 0;
@@ -575,7 +695,7 @@ class ClsLoggerCore {
575
695
  }
576
696
  return nowTs + this.batchIntervalMs;
577
697
  }
578
- info(message, data = {}) {
698
+ info(message, data = {}, options) {
579
699
  let msg = '';
580
700
  let extra = {};
581
701
  if (message instanceof Error) {
@@ -591,9 +711,18 @@ class ClsLoggerCore {
591
711
  extra = data;
592
712
  }
593
713
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'info');
594
- this.report({ type: 'info', data: payload, timestamp: Date.now() });
714
+ const log = { type: 'info', data: payload, timestamp: Date.now() };
715
+ // info 默认走批量队列,支持 immediate 选项立即发送
716
+ if (options?.immediate) {
717
+ void this.sendReportLogs([log]).catch(() => {
718
+ this.cacheFailedReportLogs([log]);
719
+ });
720
+ }
721
+ else {
722
+ this.report(log);
723
+ }
595
724
  }
596
- warn(message, data = {}) {
725
+ warn(message, data = {}, options) {
597
726
  let msg = '';
598
727
  let extra = {};
599
728
  if (message instanceof Error) {
@@ -609,9 +738,18 @@ class ClsLoggerCore {
609
738
  extra = data;
610
739
  }
611
740
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'warn');
612
- this.report({ type: 'warn', data: payload, timestamp: Date.now() });
741
+ const log = { type: 'warn', data: payload, timestamp: Date.now() };
742
+ // warn 默认走批量队列,支持 immediate 选项立即发送
743
+ if (options?.immediate) {
744
+ void this.sendReportLogs([log]).catch(() => {
745
+ this.cacheFailedReportLogs([log]);
746
+ });
747
+ }
748
+ else {
749
+ this.report(log);
750
+ }
613
751
  }
614
- error(message, data = {}) {
752
+ error(message, data = {}, options) {
615
753
  let msg = '';
616
754
  let extra = {};
617
755
  if (message instanceof Error) {
@@ -627,7 +765,17 @@ class ClsLoggerCore {
627
765
  extra = data;
628
766
  }
629
767
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'error');
630
- this.report({ type: 'error', data: payload, timestamp: Date.now() });
768
+ const log = { type: 'error', data: payload, timestamp: Date.now() };
769
+ // error 默认即时上报,除非显式指定 immediate: false
770
+ const immediate = options?.immediate ?? true;
771
+ if (immediate) {
772
+ void this.sendReportLogs([log]).catch(() => {
773
+ this.cacheFailedReportLogs([log]);
774
+ });
775
+ }
776
+ else {
777
+ this.report(log);
778
+ }
631
779
  }
632
780
  track(trackType, data = {}) {
633
781
  if (!trackType)
@@ -642,10 +790,7 @@ class ClsLoggerCore {
642
790
  * 立即发送内存队列
643
791
  */
644
792
  async flushBatch() {
645
- if (this.batchTimer) {
646
- clearTimeout(this.batchTimer);
647
- this.batchTimer = null;
648
- }
793
+ this.cancelScheduledFlush();
649
794
  this.batchTimerDueAt = null;
650
795
  if (this.memoryQueue.length === 0)
651
796
  return;