@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/web.js CHANGED
@@ -201,6 +201,11 @@ class ClsLoggerCore {
201
201
  this.batchTimerDueAt = null;
202
202
  this.initTs = 0;
203
203
  this.startupDelayMs = 0;
204
+ this.startupMaxSize = 0; // 启动窗口内的 maxSize,0 表示使用默认计算值
205
+ this.useIdleCallback = false;
206
+ this.idleTimeout = 3000;
207
+ this.pendingIdleCallback = null; // requestIdleCallback 的 id
208
+ this.visibilityCleanup = null;
204
209
  // 参考文档:失败缓存 + 重试
205
210
  this.failedCacheKey = 'cls_failed_logs';
206
211
  this.failedCacheMax = 200;
@@ -224,7 +229,7 @@ class ClsLoggerCore {
224
229
  }
225
230
  return 'browser';
226
231
  }
227
- init(options) {
232
+ async init(options) {
228
233
  this.initTs = Date.now();
229
234
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId;
230
235
  const endpoint = options?.tencentCloud?.endpoint ?? options?.endpoint ?? this.endpoint;
@@ -233,7 +238,7 @@ class ClsLoggerCore {
233
238
  if (!topicId) {
234
239
  // eslint-disable-next-line no-console
235
240
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
236
- return;
241
+ return false;
237
242
  }
238
243
  const nextEnvType = options.envType ?? this.detectEnvType();
239
244
  // envType/endpoint/retryTimes 变化时:重置 client(以及可能的 sdk)
@@ -270,15 +275,21 @@ class ClsLoggerCore {
270
275
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
271
276
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
272
277
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
278
+ this.useIdleCallback = options.batch?.useIdleCallback ?? this.useIdleCallback;
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);
273
282
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
274
283
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
275
284
  // 预热(避免首条日志触发 import/初始化开销)
276
- void this.getInstance().catch(() => {
277
- // ignore
278
- });
285
+ const sdkReadyPromise = this.getInstance()
286
+ .then(() => true)
287
+ .catch(() => false);
279
288
  if (this.enabled) {
280
289
  // 启动时尝试发送失败缓存
281
290
  this.flushFailed();
291
+ // 添加页面可见性监听(确保页面关闭时数据不丢失)
292
+ this.setupVisibilityListener();
282
293
  // 初始化后立即启动请求监听
283
294
  this.startRequestMonitor(options.requestMonitor);
284
295
  // 初始化后立即启动错误监控/性能监控
@@ -287,6 +298,7 @@ class ClsLoggerCore {
287
298
  // 初始化后立即启动行为埋点(PV/UV/点击)
288
299
  this.startBehaviorMonitor(options.behaviorMonitor);
289
300
  }
301
+ return sdkReadyPromise;
290
302
  }
291
303
  getBaseFields() {
292
304
  let auto = undefined;
@@ -315,6 +327,96 @@ class ClsLoggerCore {
315
327
  return auto;
316
328
  return undefined;
317
329
  }
330
+ /**
331
+ * 设置页面可见性监听
332
+ * - visibilitychange: 页面隐藏时使用 sendBeacon 发送队列
333
+ * - pagehide: 作为移动端 fallback
334
+ */
335
+ setupVisibilityListener() {
336
+ if (typeof document === 'undefined' || typeof window === 'undefined')
337
+ return;
338
+ // 避免重复监听
339
+ if (this.visibilityCleanup)
340
+ return;
341
+ const handleVisibilityChange = () => {
342
+ if (document.visibilityState === 'hidden') {
343
+ // 使用微任务延迟 flush,确保 web-vitals 等第三方库的 visibilitychange 回调先执行
344
+ // 这样 LCP/CLS/INP 等指标能先入队,再被 flush 发送
345
+ // 注意:queueMicrotask 比 setTimeout(0) 更可靠,不会被延迟太久
346
+ queueMicrotask(() => {
347
+ this.flushBatchSync();
348
+ });
349
+ }
350
+ };
351
+ const handlePageHide = () => {
352
+ // pagehide 不能延迟,因为浏览器可能立即关闭页面
353
+ // 但 pagehide 通常在 visibilitychange 之后触发,此时队列应该已经包含 web-vitals 指标
354
+ this.flushBatchSync();
355
+ };
356
+ document.addEventListener('visibilitychange', handleVisibilityChange);
357
+ window.addEventListener('pagehide', handlePageHide);
358
+ this.visibilityCleanup = () => {
359
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
360
+ window.removeEventListener('pagehide', handlePageHide);
361
+ };
362
+ }
363
+ /**
364
+ * 同步发送内存队列(使用 sendBeacon)
365
+ * - 用于页面关闭时确保数据发送
366
+ * - sendBeacon 不可用时降级为缓存到 localStorage
367
+ */
368
+ flushBatchSync() {
369
+ if (this.memoryQueue.length === 0)
370
+ return;
371
+ // 清除定时器
372
+ if (this.batchTimer) {
373
+ try {
374
+ if (this.useIdleCallback && typeof cancelIdleCallback !== 'undefined') {
375
+ cancelIdleCallback(this.batchTimer);
376
+ }
377
+ else {
378
+ clearTimeout(this.batchTimer);
379
+ }
380
+ }
381
+ catch {
382
+ // ignore
383
+ }
384
+ this.batchTimer = null;
385
+ }
386
+ this.batchTimerDueAt = null;
387
+ const logs = [...this.memoryQueue];
388
+ this.memoryQueue = [];
389
+ // 优先使用 sendBeacon(页面关闭时可靠发送)
390
+ if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
391
+ try {
392
+ const payload = this.buildSendBeaconPayload(logs);
393
+ const blob = new Blob([payload], { type: 'application/json' });
394
+ const url = `${this.endpoint}/structuredlog?topic_id=${this.topicId}`;
395
+ const success = navigator.sendBeacon(url, blob);
396
+ if (!success) {
397
+ // sendBeacon 返回 false 时,降级缓存
398
+ this.cacheFailedReportLogs(logs);
399
+ }
400
+ }
401
+ catch {
402
+ this.cacheFailedReportLogs(logs);
403
+ }
404
+ }
405
+ else {
406
+ // 不支持 sendBeacon,降级缓存到 localStorage
407
+ this.cacheFailedReportLogs(logs);
408
+ }
409
+ }
410
+ /**
411
+ * 构建 sendBeacon 的 payload
412
+ */
413
+ buildSendBeaconPayload(logs) {
414
+ const logList = logs.map((log) => this.buildReportFields(log));
415
+ return JSON.stringify({
416
+ source: this.source,
417
+ logs: logList,
418
+ });
419
+ }
318
420
  startRequestMonitor(requestMonitor) {
319
421
  if (this.requestMonitorStarted)
320
422
  return;
@@ -560,32 +662,75 @@ class ClsLoggerCore {
560
662
  return;
561
663
  }
562
664
  this.memoryQueue.push(log);
563
- 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) {
564
671
  void this.flushBatch();
565
672
  return;
566
673
  }
567
- const now = Date.now();
568
674
  const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
569
675
  const desiredDelay = Math.max(0, desiredDueAt - now);
570
676
  if (!this.batchTimer) {
571
677
  this.batchTimerDueAt = desiredDueAt;
678
+ this.scheduleFlush(desiredDelay);
679
+ return;
680
+ }
681
+ // 启动合并窗口内:如果当前 timer 会"更早"触发,则延后到窗口结束,尽量减少多次发送
682
+ if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
683
+ this.cancelScheduledFlush();
684
+ this.batchTimerDueAt = desiredDueAt;
685
+ this.scheduleFlush(desiredDelay);
686
+ }
687
+ }
688
+ /**
689
+ * 调度批量发送
690
+ * - 先使用 setTimeout 保证最小延迟(desiredDelay)
691
+ * - 若开启 useIdleCallback,在延迟结束后等待浏览器空闲再执行
692
+ */
693
+ scheduleFlush(desiredDelay) {
694
+ if (this.useIdleCallback && typeof requestIdleCallback !== 'undefined') {
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);
703
+ }
704
+ else {
572
705
  this.batchTimer = setTimeout(() => {
573
706
  void this.flushBatch();
574
707
  }, desiredDelay);
575
- return;
576
708
  }
577
- // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
578
- if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
709
+ }
710
+ /**
711
+ * 取消已调度的批量发送
712
+ * - 同时清理 setTimeout 和可能的 requestIdleCallback
713
+ */
714
+ cancelScheduledFlush() {
715
+ // 清理 setTimeout
716
+ if (this.batchTimer) {
579
717
  try {
580
718
  clearTimeout(this.batchTimer);
581
719
  }
582
720
  catch {
583
721
  // ignore
584
722
  }
585
- this.batchTimerDueAt = desiredDueAt;
586
- this.batchTimer = setTimeout(() => {
587
- void this.flushBatch();
588
- }, desiredDelay);
723
+ this.batchTimer = null;
724
+ }
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;
589
734
  }
590
735
  }
591
736
  getDesiredBatchFlushDueAt(nowTs) {
@@ -598,7 +743,7 @@ class ClsLoggerCore {
598
743
  }
599
744
  return nowTs + this.batchIntervalMs;
600
745
  }
601
- info(message, data = {}) {
746
+ info(message, data = {}, options) {
602
747
  let msg = '';
603
748
  let extra = {};
604
749
  if (message instanceof Error) {
@@ -614,9 +759,18 @@ class ClsLoggerCore {
614
759
  extra = data;
615
760
  }
616
761
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'info');
617
- this.report({ type: 'info', data: payload, timestamp: Date.now() });
762
+ const log = { type: 'info', data: payload, timestamp: Date.now() };
763
+ // info 默认走批量队列,支持 immediate 选项立即发送
764
+ if (options?.immediate) {
765
+ void this.sendReportLogs([log]).catch(() => {
766
+ this.cacheFailedReportLogs([log]);
767
+ });
768
+ }
769
+ else {
770
+ this.report(log);
771
+ }
618
772
  }
619
- warn(message, data = {}) {
773
+ warn(message, data = {}, options) {
620
774
  let msg = '';
621
775
  let extra = {};
622
776
  if (message instanceof Error) {
@@ -632,9 +786,18 @@ class ClsLoggerCore {
632
786
  extra = data;
633
787
  }
634
788
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'warn');
635
- this.report({ type: 'warn', data: payload, timestamp: Date.now() });
789
+ const log = { type: 'warn', data: payload, timestamp: Date.now() };
790
+ // warn 默认走批量队列,支持 immediate 选项立即发送
791
+ if (options?.immediate) {
792
+ void this.sendReportLogs([log]).catch(() => {
793
+ this.cacheFailedReportLogs([log]);
794
+ });
795
+ }
796
+ else {
797
+ this.report(log);
798
+ }
636
799
  }
637
- error(message, data = {}) {
800
+ error(message, data = {}, options) {
638
801
  let msg = '';
639
802
  let extra = {};
640
803
  if (message instanceof Error) {
@@ -650,7 +813,17 @@ class ClsLoggerCore {
650
813
  extra = data;
651
814
  }
652
815
  const payload = normalizeFlatFields({ message: msg, ...extra }, 'error');
653
- this.report({ type: 'error', data: payload, timestamp: Date.now() });
816
+ const log = { type: 'error', data: payload, timestamp: Date.now() };
817
+ // error 默认即时上报,除非显式指定 immediate: false
818
+ const immediate = options?.immediate ?? true;
819
+ if (immediate) {
820
+ void this.sendReportLogs([log]).catch(() => {
821
+ this.cacheFailedReportLogs([log]);
822
+ });
823
+ }
824
+ else {
825
+ this.report(log);
826
+ }
654
827
  }
655
828
  track(trackType, data = {}) {
656
829
  if (!trackType)
@@ -665,10 +838,7 @@ class ClsLoggerCore {
665
838
  * 立即发送内存队列
666
839
  */
667
840
  async flushBatch() {
668
- if (this.batchTimer) {
669
- clearTimeout(this.batchTimer);
670
- this.batchTimer = null;
671
- }
841
+ this.cancelScheduledFlush();
672
842
  this.batchTimerDueAt = null;
673
843
  if (this.memoryQueue.length === 0)
674
844
  return;
@@ -774,7 +944,14 @@ class ClsLoggerCore {
774
944
  // 先清空,再尝试发送
775
945
  writeStringStorage(this.failedCacheKey, JSON.stringify([]));
776
946
  this.memoryQueue.unshift(...logs);
777
- 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
+ }
778
955
  }
779
956
  /**
780
957
  * 统计/计数类日志:按字段展开上报(若 data 为空默认 1)
@@ -1068,6 +1245,13 @@ function installWebRequestMonitor(report, opts = {}) {
1068
1245
  const DEFAULT_MAX_TEXT = 4000;
1069
1246
  const DEFAULT_DEDUPE_WINDOW_MS = 3000;
1070
1247
  const DEFAULT_DEDUPE_MAX_KEYS = 200;
1248
+ /** 默认忽略的无意义错误信息 */
1249
+ const DEFAULT_IGNORE_MESSAGES = [
1250
+ 'Script error.',
1251
+ 'Script error',
1252
+ /^ResizeObserver loop/,
1253
+ 'Permission was denied',
1254
+ ];
1071
1255
  function truncate$2(s, maxLen) {
1072
1256
  if (!s)
1073
1257
  return s;
@@ -1080,11 +1264,22 @@ function sampleHit$1(sampleRate) {
1080
1264
  return false;
1081
1265
  return Math.random() < sampleRate;
1082
1266
  }
1267
+ /** 检查消息是否应该被忽略 */
1268
+ function shouldIgnoreMessage(message, ignorePatterns) {
1269
+ if (!message || ignorePatterns.length === 0)
1270
+ return false;
1271
+ return ignorePatterns.some((pattern) => {
1272
+ if (typeof pattern === 'string') {
1273
+ return message === pattern || message.includes(pattern);
1274
+ }
1275
+ return pattern.test(message);
1276
+ });
1277
+ }
1083
1278
  function getPagePath$1() {
1084
1279
  try {
1085
1280
  if (typeof window === 'undefined')
1086
1281
  return '';
1087
- return window.location?.pathname ?? '';
1282
+ return window.location?.href ?? '';
1088
1283
  }
1089
1284
  catch {
1090
1285
  return '';
@@ -1175,6 +1370,9 @@ function installBrowserErrorMonitor(report, options) {
1175
1370
  };
1176
1371
  if (event && typeof event === 'object' && 'message' in event) {
1177
1372
  payload.message = truncate$2(String(event.message ?? ''), options.maxTextLength);
1373
+ // 检查是否应该忽略此错误
1374
+ if (shouldIgnoreMessage(String(event.message ?? ''), options.ignoreMessages))
1375
+ return;
1178
1376
  payload.filename = truncate$2(String(event.filename ?? ''), 500);
1179
1377
  payload.lineno = typeof event.lineno === 'number' ? event.lineno : undefined;
1180
1378
  payload.colno = typeof event.colno === 'number' ? event.colno : undefined;
@@ -1213,6 +1411,9 @@ function installBrowserErrorMonitor(report, options) {
1213
1411
  return;
1214
1412
  const reason = event?.reason;
1215
1413
  const e = normalizeErrorLike(reason, options.maxTextLength);
1414
+ // 检查是否应该忽略此错误
1415
+ if (shouldIgnoreMessage(e.message, options.ignoreMessages))
1416
+ return;
1216
1417
  const payload = {
1217
1418
  pagePath: getPagePath$1(),
1218
1419
  source: 'unhandledrejection',
@@ -1242,6 +1443,7 @@ function installWebErrorMonitor(report, opts = {}) {
1242
1443
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
1243
1444
  dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
1244
1445
  dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
1446
+ ignoreMessages: raw.ignoreMessages ?? DEFAULT_IGNORE_MESSAGES,
1245
1447
  };
1246
1448
  installBrowserErrorMonitor(report, options);
1247
1449
  }
@@ -1280,7 +1482,7 @@ function getPagePath() {
1280
1482
  try {
1281
1483
  if (typeof window === 'undefined')
1282
1484
  return '';
1283
- return window.location?.pathname ?? '';
1485
+ return window.location?.href ?? '';
1284
1486
  }
1285
1487
  catch {
1286
1488
  return '';
@@ -1452,9 +1654,25 @@ function installBrowserPerformanceMonitor(report, options) {
1452
1654
  if (!name || shouldIgnoreUrl(name, ignoreUrls))
1453
1655
  continue;
1454
1656
  const initiatorType = String(entry?.initiatorType ?? '');
1455
- // 对齐文档:关注 fetch/xhr/img/script(同时允许 css/other 但不强制)
1657
+ // 关注 fetch/xhr/img/script(同时允许 css/other 但不强制)
1456
1658
  if (!['xmlhttprequest', 'fetch', 'img', 'script', 'css'].includes(initiatorType))
1457
1659
  continue;
1660
+ // 时序分解(便于定位慢在哪个阶段)
1661
+ const domainLookupStart = entry.domainLookupStart ?? 0;
1662
+ const domainLookupEnd = entry.domainLookupEnd ?? 0;
1663
+ const connectStart = entry.connectStart ?? 0;
1664
+ const connectEnd = entry.connectEnd ?? 0;
1665
+ const requestStart = entry.requestStart ?? 0;
1666
+ const responseStart = entry.responseStart ?? 0;
1667
+ const responseEnd = entry.responseEnd ?? 0;
1668
+ const dns = domainLookupEnd - domainLookupStart;
1669
+ const tcp = connectEnd - connectStart;
1670
+ const ttfb = responseStart - requestStart;
1671
+ const download = responseEnd - responseStart;
1672
+ // 缓存检测:transferSize=0 且 decodedBodySize>0 表示缓存命中
1673
+ const transferSize = entry.transferSize ?? 0;
1674
+ const decodedBodySize = entry.decodedBodySize ?? 0;
1675
+ const cached = transferSize === 0 && decodedBodySize > 0;
1458
1676
  const payload = {
1459
1677
  pagePath: getPagePath(),
1460
1678
  metric: 'resource',
@@ -1462,16 +1680,26 @@ function installBrowserPerformanceMonitor(report, options) {
1462
1680
  url: truncate$1(name, options.maxTextLength),
1463
1681
  startTime: typeof entry?.startTime === 'number' ? entry.startTime : undefined,
1464
1682
  duration: typeof entry?.duration === 'number' ? entry.duration : undefined,
1683
+ // 时序分解(跨域资源可能为 0)
1684
+ dns: dns > 0 ? Math.round(dns) : undefined,
1685
+ tcp: tcp > 0 ? Math.round(tcp) : undefined,
1686
+ ttfb: ttfb > 0 ? Math.round(ttfb) : undefined,
1687
+ download: download > 0 ? Math.round(download) : undefined,
1688
+ // 缓存标记
1689
+ cached,
1465
1690
  };
1466
- // 兼容字段(部分浏览器支持)
1467
- if (typeof entry?.transferSize === 'number')
1468
- payload.transferSize = entry.transferSize;
1469
- if (typeof entry?.encodedBodySize === 'number')
1691
+ // 尺寸字段(跨域资源可能为 0)
1692
+ if (transferSize > 0)
1693
+ payload.transferSize = transferSize;
1694
+ if (typeof entry?.encodedBodySize === 'number' && entry.encodedBodySize > 0) {
1470
1695
  payload.encodedBodySize = entry.encodedBodySize;
1471
- if (typeof entry?.decodedBodySize === 'number')
1472
- payload.decodedBodySize = entry.decodedBodySize;
1473
- if (typeof entry?.nextHopProtocol === 'string')
1696
+ }
1697
+ if (decodedBodySize > 0)
1698
+ payload.decodedBodySize = decodedBodySize;
1699
+ // 协议和状态
1700
+ if (typeof entry?.nextHopProtocol === 'string' && entry.nextHopProtocol) {
1474
1701
  payload.nextHopProtocol = entry.nextHopProtocol;
1702
+ }
1475
1703
  if (typeof entry?.responseStatus === 'number')
1476
1704
  payload.status = entry.responseStatus;
1477
1705
  report(options.reportType, payload);
@@ -1549,7 +1777,7 @@ function writeUvMeta(key, meta) {
1549
1777
  function getWebPagePath() {
1550
1778
  if (typeof window === 'undefined')
1551
1779
  return '';
1552
- return window.location?.pathname || '';
1780
+ return window.location?.href || '';
1553
1781
  }
1554
1782
  function buildCommonUvFields(uvId, uvMeta, isFirstVisit) {
1555
1783
  return {
@@ -1860,7 +2088,7 @@ function getBrowserDeviceInfo(options) {
1860
2088
  if (options.includeNetwork) {
1861
2089
  try {
1862
2090
  const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
1863
- if (conn && isPlainObject(conn)) {
2091
+ if (conn && typeof conn === 'object') {
1864
2092
  if (typeof conn.effectiveType === 'string')
1865
2093
  out.netEffectiveType = conn.effectiveType;
1866
2094
  if (typeof conn.downlink === 'number')
@@ -1875,6 +2103,43 @@ function getBrowserDeviceInfo(options) {
1875
2103
  // ignore
1876
2104
  }
1877
2105
  }
2106
+ if (options.includeNetworkType) {
2107
+ try {
2108
+ const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
2109
+ if (conn && typeof conn === 'object') {
2110
+ const type = conn.type;
2111
+ const eff = conn.effectiveType;
2112
+ let val = '';
2113
+ if (type === 'wifi' || type === 'ethernet') {
2114
+ val = 'WIFI';
2115
+ }
2116
+ else if (type === 'cellular' || type === 'wimax') {
2117
+ if (eff === 'slow-2g' || eff === '2g')
2118
+ val = '2G';
2119
+ else if (eff === '3g')
2120
+ val = '3G';
2121
+ else if (eff === '4g')
2122
+ val = '4G';
2123
+ else
2124
+ val = '4G';
2125
+ }
2126
+ else {
2127
+ // type 未知或不支持,降级使用 effectiveType
2128
+ if (eff === 'slow-2g' || eff === '2g')
2129
+ val = '2G';
2130
+ else if (eff === '3g')
2131
+ val = '3G';
2132
+ else if (eff === '4g')
2133
+ val = '4G';
2134
+ }
2135
+ if (val)
2136
+ out.networkType = val;
2137
+ }
2138
+ }
2139
+ catch {
2140
+ // ignore
2141
+ }
2142
+ }
1878
2143
  return out;
1879
2144
  }
1880
2145
  function createWebDeviceInfoBaseFields(opts) {
@@ -1904,7 +2169,7 @@ function createWebDeviceInfoBaseFields(opts) {
1904
2169
  try {
1905
2170
  const g = globalThis;
1906
2171
  const extra = g[globalKey];
1907
- if (extra && isPlainObject(extra))
2172
+ if (extra && typeof extra === 'object')
1908
2173
  return { ...base, ...extra };
1909
2174
  }
1910
2175
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@be-link/cls-logger",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "@be-link cls-logger - 腾讯云 CLS 日志上报封装",
5
5
  "homepage": "https://github.com/snowmountain-top/be-link",
6
6
  "author": "zhuiyi",