@be-link/cls-logger 1.0.1-beta.22 → 1.0.1-beta.24

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/README.md CHANGED
@@ -144,6 +144,7 @@ clsLogger.init({
144
144
  maxSize: 50, // 队列达到此数量立即发送,默认 20
145
145
  intervalMs: 1000, // 定时批量发送间隔,默认 500ms
146
146
  startupDelayMs: 3000, // 启动合并窗口,默认 0(不启用)
147
+ startupMaxSize: 500, // 启动窗口内的队列阈值,默认 maxSize*10(至少200)
147
148
  useIdleCallback: true, // 使用浏览器空闲时间上报,默认 false
148
149
  idleTimeout: 3000, // 空闲回调超时时间,默认 3000ms
149
150
  },
@@ -224,13 +225,19 @@ clsLogger.init({
224
225
  clsLogger.init({
225
226
  batch: {
226
227
  startupDelayMs: 3000, // 启动 3 秒内的日志合并发送
227
- maxSize: 100, // 提高队列阈值
228
- intervalMs: 1000, // 延长批量间隔
229
- useIdleCallback: true, // 使用浏览器空闲时间上报
228
+ startupMaxSize: 500, // 启动窗口内允许更多日志积累(防止 maxSize 打断合并)
229
+ maxSize: 100, // 正常运行时的队列阈值
230
+ intervalMs: 2000, // 延长批量间隔
231
+ useIdleCallback: true, // 等待延迟后在浏览器空闲时上报
232
+ },
233
+ performanceMonitor: {
234
+ resourceTiming: false, // 关闭资源监控(首屏资源多时可大幅减少日志量)
230
235
  },
231
236
  });
232
237
  ```
233
238
 
239
+ > **说明**:`startupMaxSize` 确保启动窗口内不会因为 `maxSize` 被频繁触发而打断日志合并。
240
+
234
241
  ### 场景 2:减少资源监控日志
235
242
 
236
243
  开发环境资源过多,或生产环境不需要全量资源监控:
@@ -274,31 +281,174 @@ clsLogger.init({
274
281
  ```ts
275
282
  clsLogger.init({
276
283
  batch: {
277
- maxSize: 100, // 提高队列阈值
284
+ startupDelayMs: 3000, // 启动合并窗口
285
+ startupMaxSize: 500, // 启动窗口内的队列阈值
286
+ maxSize: 100, // 正常运行时的队列阈值
278
287
  intervalMs: 2000, // 延长批量间隔
279
- useIdleCallback: true, // 空闲时上报
288
+ useIdleCallback: true, // 空闲时上报,不阻塞主线程
280
289
  },
281
290
  performanceMonitor: {
282
291
  resourceTiming: false, // 关闭资源监控
283
292
  },
284
293
  requestMonitor: {
285
294
  sampleRate: 0.1, // 请求监控采样 10%
295
+ ignoreUrls: [
296
+ /heartbeat/i, // 忽略心跳请求
297
+ /polling/i, // 忽略轮询请求
298
+ /danmaku|barrage/i, // 忽略弹幕请求
299
+ ],
286
300
  },
287
301
  });
288
302
  ```
289
303
 
304
+ ## 最佳实践
305
+
306
+ ### 1. 批量配置的正确姿势
307
+
308
+ #### `useIdleCallback` 工作原理
309
+
310
+ 当 `useIdleCallback: true` 时,SDK 的上报流程为:
311
+
312
+ ```
313
+ 日志产生 → 等待 intervalMs/startupDelayMs → 等待浏览器空闲 → 发送
314
+ ↑ 最小等待时间 ↑ 在 idleTimeout 内空闲就发送
315
+ ```
316
+
317
+ **关键点**:
318
+
319
+ - `intervalMs`/`startupDelayMs` 是**最小等待时间**,保证不会过早发送
320
+ - `idleTimeout` 是空闲等待的**最大时间**,防止浏览器一直繁忙导致日志积压
321
+
322
+ #### `startupMaxSize` 的作用
323
+
324
+ 启动阶段(`startupDelayMs` 窗口内)日志量通常很大(性能指标、资源加载等),如果 `maxSize` 设置过小,会频繁触发队列满而打断合并:
325
+
326
+ ```ts
327
+ // ❌ 错误配置:startupDelayMs 会被 maxSize 打断
328
+ batch: {
329
+ startupDelayMs: 3000,
330
+ maxSize: 20, // 启动期间可能产生 200+ 条日志,会触发 10+ 次发送
331
+ }
332
+
333
+ // ✅ 正确配置:使用 startupMaxSize 保护启动窗口
334
+ batch: {
335
+ startupDelayMs: 3000,
336
+ maxSize: 20,
337
+ startupMaxSize: 500, // 启动期间允许更多日志积累
338
+ }
339
+ ```
340
+
341
+ ### 2. 推荐配置模板
342
+
343
+ #### 普通 H5 页面
344
+
345
+ ```ts
346
+ clsLogger.init({
347
+ batch: {
348
+ startupDelayMs: 2000,
349
+ startupMaxSize: 300,
350
+ maxSize: 50,
351
+ intervalMs: 1000,
352
+ useIdleCallback: true,
353
+ idleTimeout: 3000,
354
+ },
355
+ });
356
+ ```
357
+
358
+ #### 首屏性能敏感(电商、活动页)
359
+
360
+ ```ts
361
+ clsLogger.init({
362
+ batch: {
363
+ startupDelayMs: 5000, // 更长的启动窗口
364
+ startupMaxSize: 500,
365
+ maxSize: 100,
366
+ intervalMs: 2000,
367
+ useIdleCallback: true,
368
+ idleTimeout: 5000,
369
+ },
370
+ performanceMonitor: {
371
+ sampleRate: 0.3, // 性能监控采样
372
+ },
373
+ });
374
+ ```
375
+
376
+ #### 高频交互页面(直播间、游戏)
377
+
378
+ ```ts
379
+ clsLogger.init({
380
+ batch: {
381
+ startupDelayMs: 3000,
382
+ startupMaxSize: 500,
383
+ maxSize: 100,
384
+ intervalMs: 3000, // 更长的间隔
385
+ useIdleCallback: true,
386
+ idleTimeout: 5000,
387
+ },
388
+ performanceMonitor: {
389
+ resourceTiming: false, // 关闭资源监控
390
+ },
391
+ requestMonitor: {
392
+ sampleRate: 0.1, // 请求监控采样
393
+ ignoreUrls: [/heartbeat/, /polling/, /danmaku/],
394
+ },
395
+ });
396
+ ```
397
+
398
+ ### 3. 日志量优化技巧
399
+
400
+ | 优化项 | 配置 | 效果 |
401
+ | ------------ | ------------------------------------------ | ------------------------- |
402
+ | 关闭资源监控 | `performanceMonitor.resourceTiming: false` | 减少 80%+ 的 perf 日志 |
403
+ | 性能采样 | `performanceMonitor.sampleRate: 0.1` | 只采集 10% 用户的性能数据 |
404
+ | 请求采样 | `requestMonitor.sampleRate: 0.1` | 只采集 10% 的请求日志 |
405
+ | 忽略高频请求 | `requestMonitor.ignoreUrls: [/heartbeat/]` | 过滤心跳等高频请求 |
406
+ | 错误去重 | `errorMonitor.dedupeWindowMs: 5000` | 5秒内相同错误只上报一次 |
407
+
408
+ ### 4. 常见问题排查
409
+
410
+ #### 问题:首屏请求过多
411
+
412
+ **现象**:页面加载时发出 10+ 个日志请求
413
+
414
+ **排查步骤**:
415
+
416
+ 1. 检查 `startupDelayMs` 是否配置
417
+ 2. 检查 `startupMaxSize` 是否足够大(建议 300-500)
418
+ 3. 检查 `resourceTiming` 是否需要开启(这是日志量最大的来源)
419
+
420
+ #### 问题:日志延迟过高
421
+
422
+ **现象**:日志上报明显滞后
423
+
424
+ **排查步骤**:
425
+
426
+ 1. 检查 `useIdleCallback` + `idleTimeout` 配置
427
+ 2. 检查 `intervalMs` 是否设置过大
428
+ 3. 考虑关闭 `useIdleCallback` 或减小 `idleTimeout`
429
+
430
+ #### 问题:页面关闭丢日志
431
+
432
+ **现象**:用户快速关闭页面时日志丢失
433
+
434
+ **说明**:SDK 已通过 `sendBeacon` 处理此场景,如仍有丢失:
435
+
436
+ 1. 检查日志是否在 `memoryQueue` 中(可能还未触发 flush)
437
+ 2. 考虑对关键日志使用 `{ immediate: true }`
438
+
290
439
  ## 功能说明
291
440
 
292
441
  ### 1. 批量上报机制
293
442
 
294
443
  SDK 使用内存队列批量上报日志:
295
444
 
296
- | 触发条件 | 说明 |
297
- | -------- | ------------------------------------------------------ |
298
- | 队列满 | 达到 `maxSize` 立即发送 |
299
- | 定时器 | 每 `intervalMs` 毫秒发送一次 |
300
- | 页面关闭 | `visibilitychange` + `pagehide` 触发 `sendBeacon` 发送 |
301
- | 启动合并 | `startupDelayMs` 窗口内的日志尽量合并 |
445
+ | 触发条件 | 说明 |
446
+ | -------- | ------------------------------------------------------- |
447
+ | 队列满 | 达到 `maxSize`(启动窗口内为 `startupMaxSize`)立即发送 |
448
+ | 定时器 | 每 `intervalMs` 毫秒发送一次 |
449
+ | 页面关闭 | `visibilitychange` + `pagehide` 触发 `sendBeacon` 发送 |
450
+ | 启动合并 | `startupDelayMs` 窗口内的日志尽量合并 |
451
+ | 空闲上报 | `useIdleCallback` 开启时,等待浏览器空闲后发送 |
302
452
 
303
453
  ### 2. 即时上报 vs 批量上报
304
454
 
@@ -38,8 +38,10 @@ export declare abstract class ClsLoggerCore {
38
38
  protected batchTimerDueAt: number | null;
39
39
  protected initTs: number;
40
40
  protected startupDelayMs: number;
41
+ protected startupMaxSize: number;
41
42
  protected useIdleCallback: boolean;
42
43
  protected idleTimeout: number;
44
+ protected pendingIdleCallback: number | null;
43
45
  protected visibilityCleanup: (() => void) | null;
44
46
  protected failedCacheKey: string;
45
47
  protected failedCacheMax: number;
@@ -142,12 +144,13 @@ export declare abstract class ClsLoggerCore {
142
144
  report(log: ReportLog): void;
143
145
  /**
144
146
  * 调度批量发送
145
- * - 支持 requestIdleCallback(浏览器空闲时执行)
146
- * - 降级为 setTimeout
147
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
148
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
147
149
  */
148
150
  private scheduleFlush;
149
151
  /**
150
152
  * 取消已调度的批量发送
153
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
151
154
  */
152
155
  private cancelScheduledFlush;
153
156
  private getDesiredBatchFlushDueAt;
@@ -1 +1 @@
1
- {"version":3,"file":"ClsLoggerCore.d.ts","sourceRoot":"","sources":["../src/ClsLoggerCore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ClsLoggerCore.d.ts","sourceRoot":"","sources":["../src/ClsLoggerCore.ts"],"names":[],"mappings":"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"}
package/dist/index.esm.js CHANGED
@@ -178,8 +178,10 @@ class ClsLoggerCore {
178
178
  this.batchTimerDueAt = null;
179
179
  this.initTs = 0;
180
180
  this.startupDelayMs = 0;
181
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
181
182
  this.useIdleCallback = false;
182
183
  this.idleTimeout = 3000;
184
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
183
185
  this.visibilityCleanup = null;
184
186
  // 参考文档:失败缓存 + 重试
185
187
  this.failedCacheKey = 'cls_failed_logs';
@@ -252,6 +254,8 @@ class ClsLoggerCore {
252
254
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
253
255
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
254
256
  this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
257
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
258
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
255
259
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
256
260
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
257
261
  // 预热(避免首条日志触发 import/初始化开销)
@@ -312,10 +316,17 @@ class ClsLoggerCore {
312
316
  return;
313
317
  const handleVisibilityChange = () => {
314
318
  if (document.visibilityState === 'hidden') {
315
- this.flushBatchSync();
319
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
320
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
321
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
322
+ queueMicrotask(() => {
323
+ this.flushBatchSync();
324
+ });
316
325
  }
317
326
  };
318
327
  const handlePageHide = () => {
328
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
329
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
319
330
  this.flushBatchSync();
320
331
  };
321
332
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -627,11 +638,15 @@ class ClsLoggerCore {
627
638
  return;
628
639
  }
629
640
  this.memoryQueue.push(log);
630
- 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) {
631
647
  void this.flushBatch();
632
648
  return;
633
649
  }
634
- const now = Date.now();
635
650
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
636
651
  const desiredDelay = Math.max(0, desiredDueAt - now);
637
652
  if (!this.batchTimer) {
@@ -648,17 +663,19 @@ class ClsLoggerCore {
648
663
  }
649
664
  /**
650
665
  * 调度批量发送
651
- * - 支持 requestIdleCallback(浏览器空闲时执行)
652
- * - 降级为 setTimeout
666
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
667
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
653
668
  */
654
669
  scheduleFlush(desiredDelay) {
655
670
  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;
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);
662
679
  }
663
680
  else {
664
681
  this.batchTimer = setTimeout(() => {
@@ -668,22 +685,29 @@ class ClsLoggerCore {
668
685
  }
669
686
  /**
670
687
  * 取消已调度的批量发送
688
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
671
689
  */
672
690
  cancelScheduledFlush() {
673
- if (!this.batchTimer)
674
- return;
675
- try {
676
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
677
- cancelIdleCallback(this.batchTimer);
678
- }
679
- else {
691
+ // 清理 setTimeout
692
+ if (this.batchTimer) {
693
+ try {
680
694
  clearTimeout(this.batchTimer);
681
695
  }
696
+ catch {
697
+ // ignore
698
+ }
699
+ this.batchTimer = null;
682
700
  }
683
- catch {
684
- // ignore
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;
685
710
  }
686
- this.batchTimer = null;
687
711
  }
688
712
  getDesiredBatchFlushDueAt(nowTs) {
689
713
  const start = this.initTs || nowTs;
@@ -896,7 +920,14 @@ class ClsLoggerCore {
896
920
  // 先清空,再尝试发送
897
921
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
898
922
  this.memoryQueue.unshift(...logs);
899
- 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
+ }
900
931
  }
901
932
  /**
902
933
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/dist/index.js CHANGED
@@ -182,8 +182,10 @@ class ClsLoggerCore {
182
182
  this.batchTimerDueAt = null;
183
183
  this.initTs = 0;
184
184
  this.startupDelayMs = 0;
185
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
185
186
  this.useIdleCallback = false;
186
187
  this.idleTimeout = 3000;
188
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
187
189
  this.visibilityCleanup = null;
188
190
  // 参考文档:失败缓存 + 重试
189
191
  this.failedCacheKey = 'cls_failed_logs';
@@ -256,6 +258,8 @@ class ClsLoggerCore {
256
258
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
257
259
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
258
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);
259
263
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
260
264
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
261
265
  // 预热(避免首条日志触发 import/初始化开销)
@@ -316,10 +320,17 @@ class ClsLoggerCore {
316
320
  return;
317
321
  const handleVisibilityChange = () => {
318
322
  if (document.visibilityState === 'hidden') {
319
- this.flushBatchSync();
323
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
324
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
325
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
326
+ queueMicrotask(() => {
327
+ this.flushBatchSync();
328
+ });
320
329
  }
321
330
  };
322
331
  const handlePageHide = () => {
332
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
333
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
323
334
  this.flushBatchSync();
324
335
  };
325
336
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -631,11 +642,15 @@ class ClsLoggerCore {
631
642
  return;
632
643
  }
633
644
  this.memoryQueue.push(log);
634
- if (this.memoryQueue.length >= this.batchMaxSize) {
645
+ const now = Date.now();
646
+ // 判断是否在启动合并窗口内
647
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
648
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
649
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
650
+ if (this.memoryQueue.length >= effectiveMaxSize) {
635
651
  void this.flushBatch();
636
652
  return;
637
653
  }
638
- const now = Date.now();
639
654
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
640
655
  const desiredDelay = Math.max(0, desiredDueAt - now);
641
656
  if (!this.batchTimer) {
@@ -652,17 +667,19 @@ class ClsLoggerCore {
652
667
  }
653
668
  /**
654
669
  * 调度批量发送
655
- * - 支持 requestIdleCallback(浏览器空闲时执行)
656
- * - 降级为 setTimeout
670
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
671
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
657
672
  */
658
673
  scheduleFlush(desiredDelay) {
659
674
  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;
675
+ // setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
676
+ this.batchTimer = setTimeout(() => {
677
+ const idleId = requestIdleCallback(() => {
678
+ this.pendingIdleCallback = null;
679
+ void this.flushBatch();
680
+ }, { timeout: this.idleTimeout });
681
+ this.pendingIdleCallback = idleId;
682
+ }, desiredDelay);
666
683
  }
667
684
  else {
668
685
  this.batchTimer = setTimeout(() => {
@@ -672,22 +689,29 @@ class ClsLoggerCore {
672
689
  }
673
690
  /**
674
691
  * 取消已调度的批量发送
692
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
675
693
  */
676
694
  cancelScheduledFlush() {
677
- if (!this.batchTimer)
678
- return;
679
- try {
680
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
681
- cancelIdleCallback(this.batchTimer);
682
- }
683
- else {
695
+ // 清理 setTimeout
696
+ if (this.batchTimer) {
697
+ try {
684
698
  clearTimeout(this.batchTimer);
685
699
  }
700
+ catch {
701
+ // ignore
702
+ }
703
+ this.batchTimer = null;
686
704
  }
687
- catch {
688
- // ignore
705
+ // 清理可能的 pendingIdleCallback
706
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
707
+ try {
708
+ cancelIdleCallback(this.pendingIdleCallback);
709
+ }
710
+ catch {
711
+ // ignore
712
+ }
713
+ this.pendingIdleCallback = null;
689
714
  }
690
- this.batchTimer = null;
691
715
  }
692
716
  getDesiredBatchFlushDueAt(nowTs) {
693
717
  const start = this.initTs || nowTs;
@@ -900,7 +924,14 @@ class ClsLoggerCore {
900
924
  // 先清空,再尝试发送
901
925
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
902
926
  this.memoryQueue.unshift(...logs);
903
- void this.flushBatch();
927
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
928
+ if (!this.batchTimer) {
929
+ const now = Date.now();
930
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
931
+ const desiredDelay = Math.max(0, desiredDueAt - now);
932
+ this.batchTimerDueAt = desiredDueAt;
933
+ this.scheduleFlush(desiredDelay);
934
+ }
904
935
  }
905
936
  /**
906
937
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/dist/index.umd.js CHANGED
@@ -182,8 +182,10 @@
182
182
  this.batchTimerDueAt = null;
183
183
  this.initTs = 0;
184
184
  this.startupDelayMs = 0;
185
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
185
186
  this.useIdleCallback = false;
186
187
  this.idleTimeout = 3000;
188
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
187
189
  this.visibilityCleanup = null;
188
190
  // 参考文档:失败缓存 + 重试
189
191
  this.failedCacheKey = 'cls_failed_logs';
@@ -256,6 +258,8 @@
256
258
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
257
259
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
258
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);
259
263
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
260
264
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
261
265
  // 预热(避免首条日志触发 import/初始化开销)
@@ -316,10 +320,17 @@
316
320
  return;
317
321
  const handleVisibilityChange = () => {
318
322
  if (document.visibilityState === 'hidden') {
319
- this.flushBatchSync();
323
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
324
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
325
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
326
+ queueMicrotask(() => {
327
+ this.flushBatchSync();
328
+ });
320
329
  }
321
330
  };
322
331
  const handlePageHide = () => {
332
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
333
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
323
334
  this.flushBatchSync();
324
335
  };
325
336
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -631,11 +642,15 @@
631
642
  return;
632
643
  }
633
644
  this.memoryQueue.push(log);
634
- if (this.memoryQueue.length >= this.batchMaxSize) {
645
+ const now = Date.now();
646
+ // 判断是否在启动合并窗口内
647
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
648
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
649
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
650
+ if (this.memoryQueue.length >= effectiveMaxSize) {
635
651
  void this.flushBatch();
636
652
  return;
637
653
  }
638
- const now = Date.now();
639
654
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
640
655
  const desiredDelay = Math.max(0, desiredDueAt - now);
641
656
  if (!this.batchTimer) {
@@ -652,17 +667,19 @@
652
667
  }
653
668
  /**
654
669
  * 调度批量发送
655
- * - 支持 requestIdleCallback(浏览器空闲时执行)
656
- * - 降级为 setTimeout
670
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
671
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
657
672
  */
658
673
  scheduleFlush(desiredDelay) {
659
674
  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;
675
+ // setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
676
+ this.batchTimer = setTimeout(() => {
677
+ const idleId = requestIdleCallback(() => {
678
+ this.pendingIdleCallback = null;
679
+ void this.flushBatch();
680
+ }, { timeout: this.idleTimeout });
681
+ this.pendingIdleCallback = idleId;
682
+ }, desiredDelay);
666
683
  }
667
684
  else {
668
685
  this.batchTimer = setTimeout(() => {
@@ -672,22 +689,29 @@
672
689
  }
673
690
  /**
674
691
  * 取消已调度的批量发送
692
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
675
693
  */
676
694
  cancelScheduledFlush() {
677
- if (!this.batchTimer)
678
- return;
679
- try {
680
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
681
- cancelIdleCallback(this.batchTimer);
682
- }
683
- else {
695
+ // 清理 setTimeout
696
+ if (this.batchTimer) {
697
+ try {
684
698
  clearTimeout(this.batchTimer);
685
699
  }
700
+ catch {
701
+ // ignore
702
+ }
703
+ this.batchTimer = null;
686
704
  }
687
- catch {
688
- // ignore
705
+ // 清理可能的 pendingIdleCallback
706
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
707
+ try {
708
+ cancelIdleCallback(this.pendingIdleCallback);
709
+ }
710
+ catch {
711
+ // ignore
712
+ }
713
+ this.pendingIdleCallback = null;
689
714
  }
690
- this.batchTimer = null;
691
715
  }
692
716
  getDesiredBatchFlushDueAt(nowTs) {
693
717
  const start = this.initTs || nowTs;
@@ -900,7 +924,14 @@
900
924
  // 先清空,再尝试发送
901
925
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
902
926
  this.memoryQueue.unshift(...logs);
903
- void this.flushBatch();
927
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
928
+ if (!this.batchTimer) {
929
+ const now = Date.now();
930
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
931
+ const desiredDelay = Math.max(0, desiredDueAt - now);
932
+ this.batchTimerDueAt = desiredDueAt;
933
+ this.scheduleFlush(desiredDelay);
934
+ }
904
935
  }
905
936
  /**
906
937
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/dist/mini.esm.js CHANGED
@@ -178,8 +178,10 @@ class ClsLoggerCore {
178
178
  this.batchTimerDueAt = null;
179
179
  this.initTs = 0;
180
180
  this.startupDelayMs = 0;
181
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
181
182
  this.useIdleCallback = false;
182
183
  this.idleTimeout = 3000;
184
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
183
185
  this.visibilityCleanup = null;
184
186
  // 参考文档:失败缓存 + 重试
185
187
  this.failedCacheKey = 'cls_failed_logs';
@@ -252,6 +254,8 @@ class ClsLoggerCore {
252
254
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
253
255
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
254
256
  this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
257
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
258
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
255
259
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
256
260
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
257
261
  // 预热(避免首条日志触发 import/初始化开销)
@@ -312,10 +316,17 @@ class ClsLoggerCore {
312
316
  return;
313
317
  const handleVisibilityChange = () => {
314
318
  if (document.visibilityState === 'hidden') {
315
- this.flushBatchSync();
319
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
320
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
321
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
322
+ queueMicrotask(() => {
323
+ this.flushBatchSync();
324
+ });
316
325
  }
317
326
  };
318
327
  const handlePageHide = () => {
328
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
329
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
319
330
  this.flushBatchSync();
320
331
  };
321
332
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -627,11 +638,15 @@ class ClsLoggerCore {
627
638
  return;
628
639
  }
629
640
  this.memoryQueue.push(log);
630
- 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) {
631
647
  void this.flushBatch();
632
648
  return;
633
649
  }
634
- const now = Date.now();
635
650
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
636
651
  const desiredDelay = Math.max(0, desiredDueAt - now);
637
652
  if (!this.batchTimer) {
@@ -648,17 +663,19 @@ class ClsLoggerCore {
648
663
  }
649
664
  /**
650
665
  * 调度批量发送
651
- * - 支持 requestIdleCallback(浏览器空闲时执行)
652
- * - 降级为 setTimeout
666
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
667
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
653
668
  */
654
669
  scheduleFlush(desiredDelay) {
655
670
  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;
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);
662
679
  }
663
680
  else {
664
681
  this.batchTimer = setTimeout(() => {
@@ -668,22 +685,29 @@ class ClsLoggerCore {
668
685
  }
669
686
  /**
670
687
  * 取消已调度的批量发送
688
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
671
689
  */
672
690
  cancelScheduledFlush() {
673
- if (!this.batchTimer)
674
- return;
675
- try {
676
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
677
- cancelIdleCallback(this.batchTimer);
678
- }
679
- else {
691
+ // 清理 setTimeout
692
+ if (this.batchTimer) {
693
+ try {
680
694
  clearTimeout(this.batchTimer);
681
695
  }
696
+ catch {
697
+ // ignore
698
+ }
699
+ this.batchTimer = null;
682
700
  }
683
- catch {
684
- // ignore
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;
685
710
  }
686
- this.batchTimer = null;
687
711
  }
688
712
  getDesiredBatchFlushDueAt(nowTs) {
689
713
  const start = this.initTs || nowTs;
@@ -896,7 +920,14 @@ class ClsLoggerCore {
896
920
  // 先清空,再尝试发送
897
921
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
898
922
  this.memoryQueue.unshift(...logs);
899
- 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
+ }
900
931
  }
901
932
  /**
902
933
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/dist/mini.js CHANGED
@@ -201,8 +201,10 @@ class ClsLoggerCore {
201
201
  this.batchTimerDueAt = null;
202
202
  this.initTs = 0;
203
203
  this.startupDelayMs = 0;
204
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
204
205
  this.useIdleCallback = false;
205
206
  this.idleTimeout = 3000;
207
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
206
208
  this.visibilityCleanup = null;
207
209
  // 参考文档:失败缓存 + 重试
208
210
  this.failedCacheKey = 'cls_failed_logs';
@@ -275,6 +277,8 @@ class ClsLoggerCore {
275
277
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
276
278
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
277
279
  this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
280
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
281
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
278
282
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
279
283
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
280
284
  // 预热(避免首条日志触发 import/初始化开销)
@@ -335,10 +339,17 @@ class ClsLoggerCore {
335
339
  return;
336
340
  const handleVisibilityChange = () => {
337
341
  if (document.visibilityState === 'hidden') {
338
- this.flushBatchSync();
342
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
343
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
344
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
345
+ queueMicrotask(() => {
346
+ this.flushBatchSync();
347
+ });
339
348
  }
340
349
  };
341
350
  const handlePageHide = () => {
351
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
352
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
342
353
  this.flushBatchSync();
343
354
  };
344
355
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -650,11 +661,15 @@ class ClsLoggerCore {
650
661
  return;
651
662
  }
652
663
  this.memoryQueue.push(log);
653
- if (this.memoryQueue.length >= this.batchMaxSize) {
664
+ const now = Date.now();
665
+ // 判断是否在启动合并窗口内
666
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
667
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
668
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
669
+ if (this.memoryQueue.length >= effectiveMaxSize) {
654
670
  void this.flushBatch();
655
671
  return;
656
672
  }
657
- const now = Date.now();
658
673
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
659
674
  const desiredDelay = Math.max(0, desiredDueAt - now);
660
675
  if (!this.batchTimer) {
@@ -671,17 +686,19 @@ class ClsLoggerCore {
671
686
  }
672
687
  /**
673
688
  * 调度批量发送
674
- * - 支持 requestIdleCallback(浏览器空闲时执行)
675
- * - 降级为 setTimeout
689
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
690
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
676
691
  */
677
692
  scheduleFlush(desiredDelay) {
678
693
  if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
679
- // 使用 requestIdleCallback,设置 timeout 保证最终执行
680
- const idleId = requestIdleCallback(() => {
681
- void this.flushBatch();
682
- }, { timeout: Math.max(desiredDelay, this.idleTimeout) });
683
- // 存储 idleId 以便清理(类型兼容处理)
684
- this.batchTimer = idleId;
694
+ // setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
695
+ this.batchTimer = setTimeout(() => {
696
+ const idleId = requestIdleCallback(() => {
697
+ this.pendingIdleCallback = null;
698
+ void this.flushBatch();
699
+ }, { timeout: this.idleTimeout });
700
+ this.pendingIdleCallback = idleId;
701
+ }, desiredDelay);
685
702
  }
686
703
  else {
687
704
  this.batchTimer = setTimeout(() => {
@@ -691,22 +708,29 @@ class ClsLoggerCore {
691
708
  }
692
709
  /**
693
710
  * 取消已调度的批量发送
711
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
694
712
  */
695
713
  cancelScheduledFlush() {
696
- if (!this.batchTimer)
697
- return;
698
- try {
699
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
700
- cancelIdleCallback(this.batchTimer);
701
- }
702
- else {
714
+ // 清理 setTimeout
715
+ if (this.batchTimer) {
716
+ try {
703
717
  clearTimeout(this.batchTimer);
704
718
  }
719
+ catch {
720
+ // ignore
721
+ }
722
+ this.batchTimer = null;
705
723
  }
706
- catch {
707
- // ignore
724
+ // 清理可能的 pendingIdleCallback
725
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
726
+ try {
727
+ cancelIdleCallback(this.pendingIdleCallback);
728
+ }
729
+ catch {
730
+ // ignore
731
+ }
732
+ this.pendingIdleCallback = null;
708
733
  }
709
- this.batchTimer = null;
710
734
  }
711
735
  getDesiredBatchFlushDueAt(nowTs) {
712
736
  const start = this.initTs || nowTs;
@@ -919,7 +943,14 @@ class ClsLoggerCore {
919
943
  // 先清空,再尝试发送
920
944
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
921
945
  this.memoryQueue.unshift(...logs);
922
- void this.flushBatch();
946
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
947
+ if (!this.batchTimer) {
948
+ const now = Date.now();
949
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
950
+ const desiredDelay = Math.max(0, desiredDueAt - now);
951
+ this.batchTimerDueAt = desiredDueAt;
952
+ this.scheduleFlush(desiredDelay);
953
+ }
923
954
  }
924
955
  /**
925
956
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/dist/types.d.ts CHANGED
@@ -29,13 +29,20 @@ export interface BatchOptions {
29
29
  /**
30
30
  * 启动阶段合并窗口(ms)
31
31
  * - 目的:减少初始化/首屏阶段(perf/resource 等日志密集期)被 intervalMs 拆成多次上报
32
- * - 行为:在该窗口内尽量延迟 flush 到窗口结束(但仍受 maxSize 约束,达到阈值会立即发送)
32
+ * - 行为:在该窗口内尽量延迟 flush 到窗口结束
33
33
  * - 默认:0(不开启)
34
34
  */
35
35
  startupDelayMs?: number;
36
+ /**
37
+ * 启动阶段的 maxSize(启动窗口内生效)
38
+ * - 目的:防止启动期间日志密集导致 maxSize 被频繁触发而打断 startupDelayMs
39
+ * - 行为:在 startupDelayMs 窗口内使用此值作为队列阈值
40
+ * - 默认:maxSize * 10(若未设置则为 200)
41
+ */
42
+ startupMaxSize?: number;
36
43
  /**
37
44
  * 是否使用浏览器空闲时间上报
38
- * - 开启后会使用 requestIdleCallback 代替 setTimeout 调度批量发送
45
+ * - 开启后会先等待 intervalMs/startupDelayMs,然后在浏览器空闲时执行发送
39
46
  * - 配合 idleTimeout 使用,保证即使浏览器繁忙也能在超时后发送
40
47
  * - 默认:false
41
48
  */
@@ -43,7 +50,7 @@ export interface BatchOptions {
43
50
  /**
44
51
  * 空闲回调超时时间(ms)
45
52
  * - 当 useIdleCallback 为 true 时生效
46
- * - 即使浏览器繁忙,也会在此时间后强制执行
53
+ * - 在等待空闲期间,即使浏览器繁忙,也会在此时间后强制执行
47
54
  * - 默认:3000
48
55
  */
49
56
  idleTimeout?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,sCAAsC;AACtC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAExD,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC;AAEpC,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAErC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,UAAU,CAAC;IAEtC,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,wCAAwC;IACxC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,qCAAqC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0BAA0B;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;IAEjD;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;IAE7C;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,yBAAyB,CAAC;IAEzD;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC;IAEzC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;CACpD;AAED,MAAM,WAAW,mBAAoB,SAAQ,oBAAoB;IAC/D,yCAAyC;IACzC,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,2BAA4B,SAAQ,oBAAoB;IACvE,8BAA8B;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG,2BAA2B,CAAC;AAErF,MAAM,WAAW,UAAU;IACzB,iDAAiD;IACjD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,MAAM,GAAG;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW;IACX,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,mBAAmB;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACpC,0BAA0B;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,yBAAyB;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,mBAAmB;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,uCAAuC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,yBAAyB;IACxC,mBAAmB;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACpC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,+BAA+B;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,qCAAqC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gCAAgC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kDAAkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mCAAmC;IACnC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yBAAyB;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sBAAsB;IACtB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,sBAAsB;IACtB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,qBAAqB;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzC,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gCAAgC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;IAE3B;;OAEG;IACH,YAAY,CAAC,EAAE;QACb;;;;WAIG;QACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,yBAAyB;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/web.esm.js CHANGED
@@ -179,8 +179,10 @@ class ClsLoggerCore {
179
179
  this.batchTimerDueAt = null;
180
180
  this.initTs = 0;
181
181
  this.startupDelayMs = 0;
182
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
182
183
  this.useIdleCallback = false;
183
184
  this.idleTimeout = 3000;
185
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
184
186
  this.visibilityCleanup = null;
185
187
  // 参考文档:失败缓存 + 重试
186
188
  this.failedCacheKey = 'cls_failed_logs';
@@ -253,6 +255,8 @@ class ClsLoggerCore {
253
255
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
254
256
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
255
257
  this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
258
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
259
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
256
260
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
257
261
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
258
262
  // 预热(避免首条日志触发 import/初始化开销)
@@ -313,10 +317,17 @@ class ClsLoggerCore {
313
317
  return;
314
318
  const handleVisibilityChange = () => {
315
319
  if (document.visibilityState === 'hidden') {
316
- this.flushBatchSync();
320
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
321
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
322
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
323
+ queueMicrotask(() => {
324
+ this.flushBatchSync();
325
+ });
317
326
  }
318
327
  };
319
328
  const handlePageHide = () => {
329
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
330
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
320
331
  this.flushBatchSync();
321
332
  };
322
333
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -628,11 +639,15 @@ class ClsLoggerCore {
628
639
  return;
629
640
  }
630
641
  this.memoryQueue.push(log);
631
- if (this.memoryQueue.length >= this.batchMaxSize) {
642
+ const now = Date.now();
643
+ // 判断是否在启动合并窗口内
644
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
645
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
646
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
647
+ if (this.memoryQueue.length >= effectiveMaxSize) {
632
648
  void this.flushBatch();
633
649
  return;
634
650
  }
635
- const now = Date.now();
636
651
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
637
652
  const desiredDelay = Math.max(0, desiredDueAt - now);
638
653
  if (!this.batchTimer) {
@@ -649,17 +664,19 @@ class ClsLoggerCore {
649
664
  }
650
665
  /**
651
666
  * 调度批量发送
652
- * - 支持 requestIdleCallback(浏览器空闲时执行)
653
- * - 降级为 setTimeout
667
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
668
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
654
669
  */
655
670
  scheduleFlush(desiredDelay) {
656
671
  if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
657
- // 使用 requestIdleCallback,设置 timeout 保证最终执行
658
- const idleId = requestIdleCallback(() => {
659
- void this.flushBatch();
660
- }, { timeout: Math.max(desiredDelay, this.idleTimeout) });
661
- // 存储 idleId 以便清理(类型兼容处理)
662
- this.batchTimer = idleId;
672
+ // setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
673
+ this.batchTimer = setTimeout(() => {
674
+ const idleId = requestIdleCallback(() => {
675
+ this.pendingIdleCallback = null;
676
+ void this.flushBatch();
677
+ }, { timeout: this.idleTimeout });
678
+ this.pendingIdleCallback = idleId;
679
+ }, desiredDelay);
663
680
  }
664
681
  else {
665
682
  this.batchTimer = setTimeout(() => {
@@ -669,22 +686,29 @@ class ClsLoggerCore {
669
686
  }
670
687
  /**
671
688
  * 取消已调度的批量发送
689
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
672
690
  */
673
691
  cancelScheduledFlush() {
674
- if (!this.batchTimer)
675
- return;
676
- try {
677
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
678
- cancelIdleCallback(this.batchTimer);
679
- }
680
- else {
692
+ // 清理 setTimeout
693
+ if (this.batchTimer) {
694
+ try {
681
695
  clearTimeout(this.batchTimer);
682
696
  }
697
+ catch {
698
+ // ignore
699
+ }
700
+ this.batchTimer = null;
683
701
  }
684
- catch {
685
- // ignore
702
+ // 清理可能的 pendingIdleCallback
703
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
704
+ try {
705
+ cancelIdleCallback(this.pendingIdleCallback);
706
+ }
707
+ catch {
708
+ // ignore
709
+ }
710
+ this.pendingIdleCallback = null;
686
711
  }
687
- this.batchTimer = null;
688
712
  }
689
713
  getDesiredBatchFlushDueAt(nowTs) {
690
714
  const start = this.initTs || nowTs;
@@ -897,7 +921,14 @@ class ClsLoggerCore {
897
921
  // 先清空,再尝试发送
898
922
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
899
923
  this.memoryQueue.unshift(...logs);
900
- void this.flushBatch();
924
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
925
+ if (!this.batchTimer) {
926
+ const now = Date.now();
927
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
928
+ const desiredDelay = Math.max(0, desiredDueAt - now);
929
+ this.batchTimerDueAt = desiredDueAt;
930
+ this.scheduleFlush(desiredDelay);
931
+ }
901
932
  }
902
933
  /**
903
934
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/dist/web.js CHANGED
@@ -202,8 +202,10 @@ class ClsLoggerCore {
202
202
  this.batchTimerDueAt = null;
203
203
  this.initTs = 0;
204
204
  this.startupDelayMs = 0;
205
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
205
206
  this.useIdleCallback = false;
206
207
  this.idleTimeout = 3000;
208
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
207
209
  this.visibilityCleanup = null;
208
210
  // 参考文档:失败缓存 + 重试
209
211
  this.failedCacheKey = 'cls_failed_logs';
@@ -276,6 +278,8 @@ class ClsLoggerCore {
276
278
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
277
279
  this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
278
280
  this.idleTimeout = options.batch?.idleTimeout ?? this.idleTimeout;
281
+ // startupMaxSize:启动窗口内的队列阈值,默认为 maxSize * 10(至少 200)
282
+ this.startupMaxSize = options.batch?.startupMaxSize ?? Math.max(this.batchMaxSize * 10, 200);
279
283
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
280
284
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
281
285
  // 预热(避免首条日志触发 import/初始化开销)
@@ -336,10 +340,17 @@ class ClsLoggerCore {
336
340
  return;
337
341
  const handleVisibilityChange = () => {
338
342
  if (document.visibilityState === 'hidden') {
339
- this.flushBatchSync();
343
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
344
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
345
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
346
+ queueMicrotask(() => {
347
+ this.flushBatchSync();
348
+ });
340
349
  }
341
350
  };
342
351
  const handlePageHide = () => {
352
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
353
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
343
354
  this.flushBatchSync();
344
355
  };
345
356
  document.addEventListener('visibilitychange', handleVisibilityChange);
@@ -651,11 +662,15 @@ class ClsLoggerCore {
651
662
  return;
652
663
  }
653
664
  this.memoryQueue.push(log);
654
- if (this.memoryQueue.length >= this.batchMaxSize) {
665
+ const now = Date.now();
666
+ // 判断是否在启动合并窗口内
667
+ const inStartupWindow = this.startupDelayMs > 0 && now - this.initTs < this.startupDelayMs;
668
+ // 启动窗口内使用 startupMaxSize,正常情况使用 batchMaxSize
669
+ const effectiveMaxSize = inStartupWindow ? this.startupMaxSize : this.batchMaxSize;
670
+ if (this.memoryQueue.length >= effectiveMaxSize) {
655
671
  void this.flushBatch();
656
672
  return;
657
673
  }
658
- const now = Date.now();
659
674
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
660
675
  const desiredDelay = Math.max(0, desiredDueAt - now);
661
676
  if (!this.batchTimer) {
@@ -672,17 +687,19 @@ class ClsLoggerCore {
672
687
  }
673
688
  /**
674
689
  * 调度批量发送
675
- * - 支持 requestIdleCallback(浏览器空闲时执行)
676
- * - 降级为 setTimeout
690
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
691
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
677
692
  */
678
693
  scheduleFlush(desiredDelay) {
679
694
  if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
680
- // 使用 requestIdleCallback,设置 timeout 保证最终执行
681
- const idleId = requestIdleCallback(() => {
682
- void this.flushBatch();
683
- }, { timeout: Math.max(desiredDelay, this.idleTimeout) });
684
- // 存储 idleId 以便清理(类型兼容处理)
685
- this.batchTimer = idleId;
695
+ // setTimeout 保证最小延迟,再 requestIdleCallback 在空闲时执行
696
+ this.batchTimer = setTimeout(() => {
697
+ const idleId = requestIdleCallback(() => {
698
+ this.pendingIdleCallback = null;
699
+ void this.flushBatch();
700
+ }, { timeout: this.idleTimeout });
701
+ this.pendingIdleCallback = idleId;
702
+ }, desiredDelay);
686
703
  }
687
704
  else {
688
705
  this.batchTimer = setTimeout(() => {
@@ -692,22 +709,29 @@ class ClsLoggerCore {
692
709
  }
693
710
  /**
694
711
  * 取消已调度的批量发送
712
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
695
713
  */
696
714
  cancelScheduledFlush() {
697
- if (!this.batchTimer)
698
- return;
699
- try {
700
- if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
701
- cancelIdleCallback(this.batchTimer);
702
- }
703
- else {
715
+ // 清理 setTimeout
716
+ if (this.batchTimer) {
717
+ try {
704
718
  clearTimeout(this.batchTimer);
705
719
  }
720
+ catch {
721
+ // ignore
722
+ }
723
+ this.batchTimer = null;
706
724
  }
707
- catch {
708
- // ignore
725
+ // 清理可能的 pendingIdleCallback
726
+ if (this.pendingIdleCallback !== null && typeof cancelIdleCallback !== 'undefined') {
727
+ try {
728
+ cancelIdleCallback(this.pendingIdleCallback);
729
+ }
730
+ catch {
731
+ // ignore
732
+ }
733
+ this.pendingIdleCallback = null;
709
734
  }
710
- this.batchTimer = null;
711
735
  }
712
736
  getDesiredBatchFlushDueAt(nowTs) {
713
737
  const start = this.initTs || nowTs;
@@ -920,7 +944,14 @@ class ClsLoggerCore {
920
944
  // 先清空,再尝试发送
921
945
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
922
946
  this.memoryQueue.unshift(...logs);
923
- void this.flushBatch();
947
+ // 触发定时器而非直接 flush,以尊重 startupDelayMs 配置
948
+ if (!this.batchTimer) {
949
+ const now = Date.now();
950
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
951
+ const desiredDelay = Math.max(0, desiredDueAt - now);
952
+ this.batchTimerDueAt = desiredDueAt;
953
+ this.scheduleFlush(desiredDelay);
954
+ }
924
955
  }
925
956
  /**
926
957
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@be-link/cls-logger",
3
- "version": "1.0.1-beta.22",
3
+ "version": "1.0.1-beta.24",
4
4
  "description": "@be-link cls-logger - 腾讯云 CLS 日志上报封装",
5
5
  "homepage": "https://github.com/snowmountain-top/be-link",
6
6
  "author": "zhuiyi",