@be-link/cls-logger 1.0.1-beta.0 → 1.0.1-beta.10

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
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('tencentcloud-cls-sdk-js-web')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'tencentcloud-cls-sdk-js-web'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.BeLinkClsLogger = {}, global.tencentcloudClsSdkJsWeb));
5
- })(this, (function (exports, tencentcloudClsSdkJsWeb) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.BeLinkClsLogger = {}));
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
7
  function isPlainObject(value) {
8
8
  return Object.prototype.toString.call(value) === '[object Object]';
@@ -138,6 +138,10 @@
138
138
  }
139
139
 
140
140
  const DEFAULT_IGNORE$1 = ['cls.tencentcs.com', /\/cls\//i];
141
+ function isClsSendingNow() {
142
+ const g = globalThis;
143
+ return (g.__beLinkClsLoggerSendingCount__ ?? 0) > 0;
144
+ }
141
145
  function shouldIgnoreUrl$1(url, ignoreUrls) {
142
146
  for (const rule of ignoreUrls) {
143
147
  if (typeof rule === 'string') {
@@ -215,6 +219,9 @@
215
219
  w.__beLinkClsLoggerFetchInstalled__ = true;
216
220
  w.__beLinkClsLoggerRawFetch__ = w.fetch;
217
221
  w.fetch = async (input, init) => {
222
+ // 避免 CLS SDK 上报请求被 requestMonitor 捕获后递归上报(尤其在跨域失败时会“上报失败→再上报”)
223
+ if (isClsSendingNow())
224
+ return w.__beLinkClsLoggerRawFetch__(input, init);
218
225
  let url = '';
219
226
  let method = '';
220
227
  let body = undefined;
@@ -304,6 +311,9 @@
304
311
  return rawOpen.apply(this, args);
305
312
  };
306
313
  XHR.prototype.send = function (...args) {
314
+ // CLS SDK 发起上报时:跳过监控,避免递归上报
315
+ if (isClsSendingNow())
316
+ return rawSend.apply(this, args);
307
317
  const startTs = Date.now();
308
318
  try {
309
319
  const method = String(this.__beLinkClsLoggerMethod__ ?? 'GET');
@@ -530,6 +540,8 @@
530
540
  }
531
541
 
532
542
  const DEFAULT_MAX_TEXT = 4000;
543
+ const DEFAULT_DEDUPE_WINDOW_MS = 3000;
544
+ const DEFAULT_DEDUPE_MAX_KEYS = 200;
533
545
  function truncate$2(s, maxLen) {
534
546
  if (!s)
535
547
  return s;
@@ -555,7 +567,17 @@
555
567
  function normalizeErrorLike(err, maxTextLength) {
556
568
  if (err && typeof err === 'object') {
557
569
  const anyErr = err;
558
- const message = truncate$2(String(anyErr.message ?? anyErr.toString?.() ?? ''), maxTextLength);
570
+ let rawMsg = anyErr.message;
571
+ if (!rawMsg) {
572
+ const str = anyErr.toString?.();
573
+ if (!str || str === '[object Object]') {
574
+ rawMsg = stringifyLogValue(anyErr);
575
+ }
576
+ else {
577
+ rawMsg = str;
578
+ }
579
+ }
580
+ const message = truncate$2(String(rawMsg ?? ''), maxTextLength);
559
581
  const name = truncate$2(String(anyErr.name ?? ''), 200);
560
582
  const stack = truncate$2(String(anyErr.stack ?? ''), maxTextLength);
561
583
  return { message, name, stack };
@@ -563,6 +585,54 @@
563
585
  const message = truncate$2(stringifyLogValue(err), maxTextLength);
564
586
  return { message, name: '', stack: '' };
565
587
  }
588
+ function createDedupeGuard(options) {
589
+ const cache = new Map(); // key -> lastReportAt
590
+ const maxKeys = Math.max(0, options.dedupeMaxKeys);
591
+ const windowMs = Math.max(0, options.dedupeWindowMs);
592
+ function touch(key, now) {
593
+ // refresh insertion order
594
+ if (cache.has(key))
595
+ cache.delete(key);
596
+ cache.set(key, now);
597
+ if (maxKeys > 0) {
598
+ while (cache.size > maxKeys) {
599
+ const first = cache.keys().next().value;
600
+ if (!first)
601
+ break;
602
+ cache.delete(first);
603
+ }
604
+ }
605
+ }
606
+ return (key) => {
607
+ if (!key)
608
+ return true;
609
+ if (windowMs <= 0 || maxKeys === 0)
610
+ return true;
611
+ const now = Date.now();
612
+ const last = cache.get(key);
613
+ if (typeof last === 'number' && now - last < windowMs)
614
+ return false;
615
+ touch(key, now);
616
+ return true;
617
+ };
618
+ }
619
+ function buildErrorKey(type, payload) {
620
+ // 使用最核心、最稳定的字段构造签名,避免把瞬态字段(如 time)带入导致失效
621
+ const parts = [
622
+ type,
623
+ String(payload.source ?? ''),
624
+ String(payload.pagePath ?? ''),
625
+ String(payload.message ?? ''),
626
+ String(payload.errorName ?? ''),
627
+ String(payload.stack ?? ''),
628
+ String(payload.filename ?? ''),
629
+ String(payload.lineno ?? ''),
630
+ String(payload.colno ?? ''),
631
+ String(payload.tagName ?? ''),
632
+ String(payload.resourceUrl ?? ''),
633
+ ];
634
+ return parts.join('|');
635
+ }
566
636
  function installBrowserErrorMonitor(report, options) {
567
637
  if (typeof window === 'undefined')
568
638
  return;
@@ -570,6 +640,7 @@
570
640
  if (w.__beLinkClsLoggerErrorInstalled__)
571
641
  return;
572
642
  w.__beLinkClsLoggerErrorInstalled__ = true;
643
+ const shouldReport = createDedupeGuard(options);
573
644
  window.addEventListener('error', (event) => {
574
645
  try {
575
646
  if (!sampleHit$1(options.sampleRate))
@@ -592,6 +663,8 @@
592
663
  if (e.stack)
593
664
  payload.stack = e.stack;
594
665
  }
666
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
667
+ return;
595
668
  report(options.reportType, payload);
596
669
  return;
597
670
  }
@@ -603,6 +676,8 @@
603
676
  payload.source = 'resource.error';
604
677
  payload.tagName = tagName;
605
678
  payload.resourceUrl = truncate$2(url, 2000);
679
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
680
+ return;
606
681
  report(options.reportType, payload);
607
682
  }
608
683
  }
@@ -623,6 +698,8 @@
623
698
  errorName: e.name,
624
699
  stack: e.stack,
625
700
  };
701
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
702
+ return;
626
703
  report(options.reportType, payload);
627
704
  }
628
705
  catch {
@@ -635,6 +712,7 @@
635
712
  if (g.__beLinkClsLoggerMpErrorInstalled__)
636
713
  return;
637
714
  g.__beLinkClsLoggerMpErrorInstalled__ = true;
715
+ const shouldReport = createDedupeGuard(options);
638
716
  const wxAny = globalThis.wx;
639
717
  // wx.* 事件(兼容在 App 已经创建后的场景)
640
718
  try {
@@ -643,10 +721,16 @@
643
721
  try {
644
722
  if (!sampleHit$1(options.sampleRate))
645
723
  return;
646
- report(options.reportType, {
724
+ const e = normalizeErrorLike(msg, options.maxTextLength);
725
+ const payload = {
647
726
  source: 'wx.onError',
648
- message: truncate$2(String(msg ?? ''), options.maxTextLength),
649
- });
727
+ message: e.message,
728
+ errorName: e.name,
729
+ stack: e.stack,
730
+ };
731
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
732
+ return;
733
+ report(options.reportType, payload);
650
734
  }
651
735
  catch {
652
736
  // ignore
@@ -664,12 +748,15 @@
664
748
  if (!sampleHit$1(options.sampleRate))
665
749
  return;
666
750
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
667
- report(options.reportType, {
751
+ const payload = {
668
752
  source: 'wx.onUnhandledRejection',
669
753
  message: e.message,
670
754
  errorName: e.name,
671
755
  stack: e.stack,
672
- });
756
+ };
757
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
758
+ return;
759
+ report(options.reportType, payload);
673
760
  }
674
761
  catch {
675
762
  // ignore
@@ -691,10 +778,15 @@
691
778
  next.onError = function (...args) {
692
779
  try {
693
780
  if (sampleHit$1(options.sampleRate)) {
694
- report(options.reportType, {
781
+ const e = normalizeErrorLike(args?.[0], options.maxTextLength);
782
+ const payload = {
695
783
  source: 'App.onError',
696
- message: truncate$2(String(args?.[0] ?? ''), options.maxTextLength),
697
- });
784
+ message: e.message,
785
+ errorName: e.name,
786
+ stack: e.stack,
787
+ };
788
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
789
+ report(options.reportType, payload);
698
790
  }
699
791
  }
700
792
  catch {
@@ -710,12 +802,14 @@
710
802
  if (sampleHit$1(options.sampleRate)) {
711
803
  const reason = args?.[0]?.reason ?? args?.[0];
712
804
  const e = normalizeErrorLike(reason, options.maxTextLength);
713
- report(options.reportType, {
805
+ const payload = {
714
806
  source: 'App.onUnhandledRejection',
715
807
  message: e.message,
716
808
  errorName: e.name,
717
809
  stack: e.stack,
718
- });
810
+ };
811
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
812
+ report(options.reportType, payload);
719
813
  }
720
814
  }
721
815
  catch {
@@ -744,6 +838,8 @@
744
838
  sampleRate: raw.sampleRate ?? 1,
745
839
  captureResourceError: raw.captureResourceError ?? true,
746
840
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
841
+ dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
842
+ dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
747
843
  };
748
844
  if (isMiniProgramEnv()) {
749
845
  installMiniProgramErrorMonitor(report, options);
@@ -994,140 +1090,58 @@
994
1090
  }
995
1091
  }
996
1092
  }
997
- function wrapMiniProgramRouteApi(report, reportType, apiName, options) {
998
- const wxAny = globalThis.wx;
999
- if (!wxAny || typeof wxAny[apiName] !== 'function')
1000
- return;
1001
- const flagKey = `__beLinkClsLoggerMpRouteWrapped__${apiName}`;
1002
- if (wxAny[flagKey])
1003
- return;
1004
- wxAny[flagKey] = true;
1005
- const raw = wxAny[apiName].bind(wxAny);
1006
- wxAny[apiName] = (opts) => {
1007
- const start = Date.now();
1008
- const url = opts?.url ? String(opts.url) : '';
1009
- const wrapCb = (cb, success) => {
1010
- return (...args) => {
1011
- try {
1012
- if (sampleHit(options.sampleRate)) {
1013
- report(reportType, {
1014
- metric: 'route',
1015
- api: apiName,
1016
- url,
1017
- duration: Date.now() - start,
1018
- success: success ? 1 : 0,
1019
- error: success ? '' : truncate$1(stringifyLogValue(args?.[0]), options.maxTextLength),
1020
- unit: 'ms',
1021
- });
1022
- }
1023
- }
1024
- catch {
1025
- // ignore
1026
- }
1027
- if (typeof cb === 'function')
1028
- return cb(...args);
1029
- return undefined;
1030
- };
1031
- };
1032
- const next = { ...(opts ?? {}) };
1033
- next.success = wrapCb(next.success, true);
1034
- next.fail = wrapCb(next.fail, false);
1035
- return raw(next);
1036
- };
1037
- }
1038
- function installMiniProgramPageRenderMonitor(report, reportType, options) {
1039
- const g = globalThis;
1040
- if (typeof g.Page !== 'function')
1041
- return;
1042
- if (g.__beLinkClsLoggerPageWrapped__)
1043
- return;
1044
- g.__beLinkClsLoggerPageWrapped__ = true;
1045
- const rawPage = g.Page;
1046
- g.Page = (pageOptions) => {
1047
- const next = { ...(pageOptions ?? {}) };
1048
- const rawOnLoad = next.onLoad;
1049
- const rawOnReady = next.onReady;
1050
- next.onLoad = function (...args) {
1051
- try {
1052
- this.__beLinkClsLoggerPageLoadTs__ = Date.now();
1053
- }
1054
- catch {
1055
- // ignore
1056
- }
1057
- if (typeof rawOnLoad === 'function')
1058
- return rawOnLoad.apply(this, args);
1059
- return undefined;
1060
- };
1061
- next.onReady = function (...args) {
1062
- try {
1063
- const start = this.__beLinkClsLoggerPageLoadTs__;
1064
- if (typeof start === 'number' && sampleHit(options.sampleRate)) {
1065
- report(reportType, {
1066
- metric: 'page-render',
1067
- route: this?.route ? String(this.route) : '',
1068
- duration: Date.now() - start,
1069
- unit: 'ms',
1070
- });
1071
- }
1072
- }
1073
- catch {
1074
- // ignore
1075
- }
1076
- if (typeof rawOnReady === 'function')
1077
- return rawOnReady.apply(this, args);
1078
- return undefined;
1079
- };
1080
- return rawPage(next);
1081
- };
1082
- }
1083
1093
  function installMiniProgramPerformanceMonitor(report, options) {
1084
1094
  const g = globalThis;
1095
+ const ctx = g.wx || g.Taro;
1096
+ if (!ctx || typeof ctx.getPerformance !== 'function')
1097
+ return;
1085
1098
  if (g.__beLinkClsLoggerMpPerfInstalled__)
1086
1099
  return;
1087
1100
  g.__beLinkClsLoggerMpPerfInstalled__ = true;
1088
- // 路由切换耗时(用 API 回调近似)
1089
- for (const apiName of ['navigateTo', 'redirectTo', 'switchTab', 'reLaunch']) {
1090
- try {
1091
- wrapMiniProgramRouteApi(report, options.reportType, apiName, options);
1092
- }
1093
- catch {
1094
- // ignore
1095
- }
1096
- }
1097
- // 页面渲染耗时(onLoad -> onReady)
1098
- try {
1099
- installMiniProgramPageRenderMonitor(report, options.reportType, options);
1100
- }
1101
- catch {
1102
- // ignore
1103
- }
1104
- // wx.getPerformance()(若可用,尝试读取已有 entries)
1105
1101
  try {
1106
- const wxAny = globalThis.wx;
1107
- if (wxAny && typeof wxAny.getPerformance === 'function') {
1108
- const perf = wxAny.getPerformance();
1109
- if (perf && isPlainObject(perf)) {
1110
- // 不同基础库实现差异较大:尽量容错
1111
- setTimeout(() => {
1112
- try {
1113
- if (!sampleHit(options.sampleRate))
1114
- return;
1115
- const entries = typeof perf.getEntries === 'function'
1116
- ? perf.getEntries()
1117
- : typeof perf.getEntriesByType === 'function'
1118
- ? perf.getEntriesByType('navigation')
1119
- : [];
1102
+ const perf = ctx.getPerformance();
1103
+ if (!perf || typeof perf.createObserver !== 'function')
1104
+ return;
1105
+ const observer = perf.createObserver((entryList) => {
1106
+ try {
1107
+ const entries = entryList.getEntries();
1108
+ for (const entry of entries) {
1109
+ if (!sampleHit(options.sampleRate))
1110
+ continue;
1111
+ // Page Render: firstRender
1112
+ if (entry.entryType === 'render' && entry.name === 'firstRender') {
1120
1113
  report(options.reportType, {
1121
- metric: 'mp-performance',
1122
- entries: truncate$1(stringifyLogValue(entries), options.maxTextLength),
1114
+ metric: 'page-render',
1115
+ duration: entry.duration,
1116
+ pagePath: entry.path || '',
1117
+ unit: 'ms',
1123
1118
  });
1124
1119
  }
1125
- catch {
1126
- // ignore
1120
+ // Route Switch: route
1121
+ else if (entry.entryType === 'navigation' && entry.name === 'route') {
1122
+ report(options.reportType, {
1123
+ metric: 'route',
1124
+ duration: entry.duration,
1125
+ pagePath: entry.path || '',
1126
+ unit: 'ms',
1127
+ });
1127
1128
  }
1128
- }, 0);
1129
+ // App Launch: appLaunch (Cold)
1130
+ else if (entry.entryType === 'navigation' && entry.name === 'appLaunch') {
1131
+ report(options.reportType, {
1132
+ metric: 'app-launch',
1133
+ duration: entry.duration,
1134
+ launchType: 'cold',
1135
+ unit: 'ms',
1136
+ });
1137
+ }
1138
+ }
1129
1139
  }
1130
- }
1140
+ catch {
1141
+ // ignore
1142
+ }
1143
+ });
1144
+ observer.observe({ entryTypes: ['navigation', 'render'] });
1131
1145
  }
1132
1146
  catch {
1133
1147
  // ignore
@@ -1451,7 +1465,7 @@
1451
1465
  const tag = (el.tagName || '').toLowerCase();
1452
1466
  const trackId = getAttr(el, clickTrackIdAttr);
1453
1467
  // 过滤无效点击:白名单 tag + 没有 trackId
1454
- if (clickWhiteList.includes(tag) && !trackId)
1468
+ if (clickWhiteList.includes(tag) || !trackId)
1455
1469
  return;
1456
1470
  void uvStatePromise.then(({ uvId, meta }) => {
1457
1471
  if (destroyed)
@@ -1492,8 +1506,12 @@
1492
1506
  g.Page = function patchedPage(conf) {
1493
1507
  const originalOnShow = conf?.onShow;
1494
1508
  conf.onShow = function (...args) {
1495
- if (pvEnabled)
1496
- reportPv(getPagePath());
1509
+ if (pvEnabled) {
1510
+ const pagePath = getPagePath();
1511
+ if (pagePath?.length > 0) {
1512
+ reportPv(pagePath);
1513
+ }
1514
+ }
1497
1515
  return typeof originalOnShow === 'function' ? originalOnShow.apply(this, args) : undefined;
1498
1516
  };
1499
1517
  // 点击:wrap 页面 methods(bindtap 等会调用到这里的 handler)
@@ -1820,9 +1838,29 @@
1820
1838
  };
1821
1839
  }
1822
1840
 
1823
- class ClsLogger {
1841
+ function enterClsSendingGuard() {
1842
+ const g = globalThis;
1843
+ const next = (g.__beLinkClsLoggerSendingCount__ ?? 0) + 1;
1844
+ g.__beLinkClsLoggerSendingCount__ = next;
1845
+ return () => {
1846
+ const cur = g.__beLinkClsLoggerSendingCount__ ?? 0;
1847
+ g.__beLinkClsLoggerSendingCount__ = cur > 0 ? cur - 1 : 0;
1848
+ };
1849
+ }
1850
+ /**
1851
+ * CLS Logger 核心基类
1852
+ * - 负责所有上报、队列、监控逻辑
1853
+ * - 不包含具体的 SDK 加载实现(由子类负责)
1854
+ * - 这样可以把 web/mini 的 SDK 依赖彻底解耦到子入口
1855
+ */
1856
+ class ClsLoggerCore {
1824
1857
  constructor() {
1858
+ this.sdk = null;
1859
+ this.sdkPromise = null;
1860
+ this.sdkOverride = null;
1861
+ this.sdkLoaderOverride = null;
1825
1862
  this.client = null;
1863
+ this.clientPromise = null;
1826
1864
  this.topicId = null;
1827
1865
  this.endpoint = 'ap-shanghai.cls.tencentcs.com';
1828
1866
  this.retryTimes = 10;
@@ -1841,6 +1879,9 @@
1841
1879
  this.batchMaxSize = 20;
1842
1880
  this.batchIntervalMs = 500;
1843
1881
  this.batchTimer = null;
1882
+ this.batchTimerDueAt = null;
1883
+ this.initTs = 0;
1884
+ this.startupDelayMs = 0;
1844
1885
  // 参考文档:失败缓存 + 重试
1845
1886
  this.failedCacheKey = 'cls_failed_logs';
1846
1887
  this.failedCacheMax = 200;
@@ -1850,7 +1891,22 @@
1850
1891
  this.behaviorMonitorStarted = false;
1851
1892
  this.behaviorMonitorCleanup = null;
1852
1893
  }
1894
+ /**
1895
+ * 子类可按需重写(默认检测 wx)
1896
+ */
1897
+ detectEnvType() {
1898
+ const g = globalThis;
1899
+ // 微信、支付宝、字节跳动、UniApp 等小程序环境通常都有特定全局变量
1900
+ if ((g.wx && typeof g.wx.getSystemInfoSync === 'function') ||
1901
+ (g.my && typeof g.my.getSystemInfoSync === 'function') ||
1902
+ (g.tt && typeof g.tt.getSystemInfoSync === 'function') ||
1903
+ (g.uni && typeof g.uni.getSystemInfoSync === 'function')) {
1904
+ return 'miniprogram';
1905
+ }
1906
+ return 'browser';
1907
+ }
1853
1908
  init(options) {
1909
+ this.initTs = Date.now();
1854
1910
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId ?? null;
1855
1911
  const endpoint = options?.tencentCloud?.endpoint ?? options?.endpoint ?? this.endpoint;
1856
1912
  const retryTimes = options?.tencentCloud?.retry_times ?? options?.retry_times ?? this.retryTimes;
@@ -1860,25 +1916,46 @@
1860
1916
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
1861
1917
  return;
1862
1918
  }
1919
+ const nextEnvType = options.envType ?? this.detectEnvType();
1920
+ // envType/endpoint/retryTimes 变化时:重置 client(以及可能的 sdk)
1921
+ const envChanged = nextEnvType !== this.envType;
1922
+ const endpointChanged = endpoint !== this.endpoint;
1923
+ const retryChanged = retryTimes !== this.retryTimes;
1924
+ if (envChanged || endpointChanged || retryChanged) {
1925
+ this.client = null;
1926
+ this.clientPromise = null;
1927
+ }
1928
+ if (envChanged) {
1929
+ this.sdk = null;
1930
+ this.sdkPromise = null;
1931
+ }
1863
1932
  this.topicId = topicId;
1864
1933
  this.endpoint = endpoint;
1865
1934
  this.retryTimes = retryTimes;
1866
1935
  this.source = source;
1936
+ this.userId = options.userId ?? this.userId;
1937
+ this.userName = options.userName ?? this.userName;
1867
1938
  this.projectId = options.projectId ?? this.projectId;
1868
1939
  this.projectName = options.projectName ?? this.projectName;
1869
1940
  this.appId = options.appId ?? this.appId;
1870
1941
  this.appVersion = options.appVersion ?? this.appVersion;
1871
- this.envType = options.envType ?? this.detectEnvType();
1942
+ this.envType = nextEnvType;
1943
+ // 可选:外部注入 SDK(优先级:sdkLoader > sdk)
1944
+ this.sdkLoaderOverride = options.sdkLoader ?? this.sdkLoaderOverride;
1945
+ this.sdkOverride = options.sdk ?? this.sdkOverride;
1872
1946
  this.userGenerateBaseFields = options.generateBaseFields ?? this.userGenerateBaseFields;
1873
1947
  this.autoGenerateBaseFields = createAutoDeviceInfoBaseFields(this.envType, options.deviceInfo);
1874
1948
  this.storageKey = options.storageKey ?? this.storageKey;
1875
1949
  this.batchSize = options.batchSize ?? this.batchSize;
1876
1950
  this.batchMaxSize = options.batch?.maxSize ?? this.batchMaxSize;
1877
1951
  this.batchIntervalMs = options.batch?.intervalMs ?? this.batchIntervalMs;
1952
+ this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
1878
1953
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
1879
1954
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
1880
- // 预热 client
1881
- this.getInstance();
1955
+ // 预热(避免首条日志触发 import/初始化开销)
1956
+ void this.getInstance().catch(() => {
1957
+ // ignore
1958
+ });
1882
1959
  // 启动时尝试发送失败缓存
1883
1960
  this.flushFailed();
1884
1961
  // 初始化后立即启动请求监听
@@ -1903,7 +1980,7 @@
1903
1980
  try {
1904
1981
  const userRaw = this.userGenerateBaseFields ? this.userGenerateBaseFields() : undefined;
1905
1982
  if (userRaw && isPlainObject(userRaw))
1906
- user = normalizeFlatFields(userRaw, 'generateBaseFields');
1983
+ user = normalizeFlatFields({ ...userRaw }, 'generateBaseFields');
1907
1984
  }
1908
1985
  catch {
1909
1986
  user = undefined;
@@ -1982,20 +2059,29 @@
1982
2059
  this.behaviorMonitorCleanup = null;
1983
2060
  this.behaviorMonitorStarted = false;
1984
2061
  }
1985
- getInstance() {
2062
+ /**
2063
+ * 获取 CLS client(按环境懒加载 SDK)
2064
+ */
2065
+ async getInstance() {
1986
2066
  if (this.client)
1987
2067
  return this.client;
1988
- this.client = new tencentcloudClsSdkJsWeb.AsyncClient({
1989
- endpoint: this.endpoint,
1990
- retry_times: this.retryTimes,
2068
+ if (this.clientPromise)
2069
+ return this.clientPromise;
2070
+ this.clientPromise = this.loadSdk()
2071
+ .then(({ AsyncClient }) => {
2072
+ const client = new AsyncClient({
2073
+ endpoint: this.endpoint,
2074
+ retry_times: this.retryTimes,
2075
+ });
2076
+ this.client = client;
2077
+ return client;
2078
+ })
2079
+ .catch((err) => {
2080
+ // 失败后允许下次重试
2081
+ this.clientPromise = null;
2082
+ throw err;
1991
2083
  });
1992
- return this.client;
1993
- }
1994
- detectEnvType() {
1995
- const wxAny = globalThis.wx;
1996
- if (wxAny && typeof wxAny.getSystemInfoSync === 'function')
1997
- return 'miniprogram';
1998
- return 'browser';
2084
+ return this.clientPromise;
1999
2085
  }
2000
2086
  /**
2001
2087
  * 直接上报:埋点入参必须是一维(扁平)Object
@@ -2021,16 +2107,33 @@
2021
2107
  appVersion: this.appVersion || undefined,
2022
2108
  ...normalizedFields,
2023
2109
  });
2024
- const client = this.getInstance();
2025
- const logGroup = new tencentcloudClsSdkJsWeb.LogGroup('127.0.0.1');
2110
+ // 同步 API:内部异步发送,避免把网络异常冒泡到业务(尤其小程序)
2111
+ void this.putAsync(finalFields).catch(() => {
2112
+ // ignore
2113
+ });
2114
+ }
2115
+ async putAsync(finalFields) {
2116
+ if (!this.topicId)
2117
+ return;
2118
+ const sdk = await this.loadSdk();
2119
+ const client = await this.getInstance();
2120
+ const logGroup = new sdk.LogGroup('127.0.0.1');
2026
2121
  logGroup.setSource(this.source);
2027
- const log = new tencentcloudClsSdkJsWeb.Log(Date.now());
2122
+ const log = new sdk.Log(Date.now());
2028
2123
  for (const key of Object.keys(finalFields)) {
2029
2124
  log.addContent(key, stringifyLogValue(finalFields[key]));
2030
2125
  }
2031
2126
  logGroup.addLog(log);
2032
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2033
- client.PutLogs(request);
2127
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2128
+ const exit = enterClsSendingGuard();
2129
+ let p;
2130
+ try {
2131
+ p = client.PutLogs(request);
2132
+ }
2133
+ finally {
2134
+ exit();
2135
+ }
2136
+ await p;
2034
2137
  }
2035
2138
  /**
2036
2139
  * 直接上报:把 data 序列化后放入指定 key(默认 “日志内容”)
@@ -2089,11 +2192,19 @@
2089
2192
  console.warn('ClsLogger.putBatch:未初始化 topic_id');
2090
2193
  return;
2091
2194
  }
2092
- const client = this.getInstance();
2093
- const logGroup = new tencentcloudClsSdkJsWeb.LogGroup('127.0.0.1');
2195
+ void this.putBatchAsync(queue).catch(() => {
2196
+ // ignore
2197
+ });
2198
+ }
2199
+ async putBatchAsync(queue) {
2200
+ if (!this.topicId)
2201
+ return;
2202
+ const sdk = await this.loadSdk();
2203
+ const client = await this.getInstance();
2204
+ const logGroup = new sdk.LogGroup('127.0.0.1');
2094
2205
  logGroup.setSource(this.source);
2095
2206
  for (const item of queue) {
2096
- const log = new tencentcloudClsSdkJsWeb.Log(item.time);
2207
+ const log = new sdk.Log(item.time);
2097
2208
  const data = item.data ?? {};
2098
2209
  for (const key of Object.keys(data)) {
2099
2210
  log.addContent(key, stringifyLogValue(data[key]));
@@ -2102,8 +2213,16 @@
2102
2213
  }
2103
2214
  if (logGroup.getLogs().length === 0)
2104
2215
  return;
2105
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2106
- client.PutLogs(request);
2216
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2217
+ const exit = enterClsSendingGuard();
2218
+ let p;
2219
+ try {
2220
+ p = client.PutLogs(request);
2221
+ }
2222
+ finally {
2223
+ exit();
2224
+ }
2225
+ await p;
2107
2226
  }
2108
2227
  /**
2109
2228
  * 参考《一、概述》:统一上报入口(内存队列 + 批量发送)
@@ -2121,11 +2240,39 @@
2121
2240
  void this.flushBatch();
2122
2241
  return;
2123
2242
  }
2243
+ const now = Date.now();
2244
+ const desiredDueAt = this.getDesiredBatchFlushDueAt(now);
2245
+ const desiredDelay = Math.max(0, desiredDueAt - now);
2124
2246
  if (!this.batchTimer) {
2247
+ this.batchTimerDueAt = desiredDueAt;
2248
+ this.batchTimer = setTimeout(() => {
2249
+ void this.flushBatch();
2250
+ }, desiredDelay);
2251
+ return;
2252
+ }
2253
+ // 启动合并窗口内:如果当前 timer 会“更早”触发,则延后到窗口结束,尽量减少多次发送
2254
+ if (this.batchTimerDueAt !== null && this.batchTimerDueAt < desiredDueAt) {
2255
+ try {
2256
+ clearTimeout(this.batchTimer);
2257
+ }
2258
+ catch {
2259
+ // ignore
2260
+ }
2261
+ this.batchTimerDueAt = desiredDueAt;
2125
2262
  this.batchTimer = setTimeout(() => {
2126
2263
  void this.flushBatch();
2127
- }, this.batchIntervalMs);
2264
+ }, desiredDelay);
2265
+ }
2266
+ }
2267
+ getDesiredBatchFlushDueAt(nowTs) {
2268
+ const start = this.initTs || nowTs;
2269
+ const startupDelay = Number.isFinite(this.startupDelayMs) ? Math.max(0, this.startupDelayMs) : 0;
2270
+ if (startupDelay > 0) {
2271
+ const end = start + startupDelay;
2272
+ if (nowTs < end)
2273
+ return end;
2128
2274
  }
2275
+ return nowTs + this.batchIntervalMs;
2129
2276
  }
2130
2277
  info(message, data = {}) {
2131
2278
  const payload = normalizeFlatFields({ message, ...data }, 'info');
@@ -2156,6 +2303,7 @@
2156
2303
  clearTimeout(this.batchTimer);
2157
2304
  this.batchTimer = null;
2158
2305
  }
2306
+ this.batchTimerDueAt = null;
2159
2307
  if (this.memoryQueue.length === 0)
2160
2308
  return;
2161
2309
  const logs = [...this.memoryQueue];
@@ -2181,18 +2329,19 @@
2181
2329
  appId: this.appId || undefined,
2182
2330
  appVersion: this.appVersion || undefined,
2183
2331
  // 保证“一维字段”:业务数据以 JSON 字符串形式落到 CLS
2184
- data: stringifyLogValue(mergedData),
2332
+ ...mergedData,
2185
2333
  };
2186
2334
  }
2187
2335
  async sendReportLogs(logs) {
2188
2336
  if (!this.topicId)
2189
2337
  return;
2190
- const client = this.getInstance();
2191
- const logGroup = new tencentcloudClsSdkJsWeb.LogGroup('127.0.0.1');
2338
+ const sdk = await this.loadSdk();
2339
+ const client = await this.getInstance();
2340
+ const logGroup = new sdk.LogGroup('127.0.0.1');
2192
2341
  logGroup.setSource(this.source);
2193
2342
  for (const item of logs) {
2194
2343
  const fields = this.buildReportFields(item);
2195
- const log = new tencentcloudClsSdkJsWeb.Log(fields.timestamp);
2344
+ const log = new sdk.Log(fields.timestamp);
2196
2345
  for (const key of Object.keys(fields)) {
2197
2346
  if (key === 'timestamp')
2198
2347
  continue;
@@ -2200,8 +2349,17 @@
2200
2349
  }
2201
2350
  logGroup.addLog(log);
2202
2351
  }
2203
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2204
- await client.PutLogs(request);
2352
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2353
+ // 只在“发起网络请求”的同步阶段打标记,避免 requestMonitor 监控 CLS 上报请求导致递归
2354
+ const exit = enterClsSendingGuard();
2355
+ let p;
2356
+ try {
2357
+ p = client.PutLogs(request);
2358
+ }
2359
+ finally {
2360
+ exit();
2361
+ }
2362
+ await p;
2205
2363
  }
2206
2364
  retrySendReportLogs(logs, retryCount) {
2207
2365
  if (retryCount > this.retryTimes) {
@@ -2267,6 +2425,146 @@
2267
2425
  }
2268
2426
  }
2269
2427
 
2428
+ function readGlobal(key) {
2429
+ try {
2430
+ const g = globalThis;
2431
+ return g[key] ?? null;
2432
+ }
2433
+ catch {
2434
+ return null;
2435
+ }
2436
+ }
2437
+ function tryRequire(moduleName) {
2438
+ try {
2439
+ // 说明:
2440
+ // - ESM 构建(exports.import/module)里通常不存在模块作用域的 require
2441
+ // - 一些小程序运行时/构建链路会把 require 挂到 globalThis 上
2442
+ // 因此这里同时探测“模块作用域 require”与 “globalThis.require”
2443
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
2444
+ const localReq = (typeof require === 'function' ? require : null);
2445
+ const globalReq = readGlobal('require');
2446
+ const candidates = [localReq, globalReq].filter((fn) => typeof fn === 'function');
2447
+ for (const fn of candidates) {
2448
+ try {
2449
+ return fn(moduleName);
2450
+ }
2451
+ catch {
2452
+ // continue
2453
+ }
2454
+ }
2455
+ return null;
2456
+ }
2457
+ catch {
2458
+ return null;
2459
+ }
2460
+ }
2461
+
2462
+ /**
2463
+ * 兼容版 ClsLogger(默认入口)
2464
+ * - 保留了自动识别环境 + 动态 import 的逻辑
2465
+ * - 如果业务想瘦身,建议改用 @be-link/cls-logger/web 或 /mini
2466
+ */
2467
+ class ClsLogger extends ClsLoggerCore {
2468
+ async loadSdk() {
2469
+ if (this.sdk)
2470
+ return this.sdk;
2471
+ if (this.sdkPromise)
2472
+ return this.sdkPromise;
2473
+ const normalizeSdk = (m) => {
2474
+ const mod = (m?.default && m.default.AsyncClient ? m.default : m);
2475
+ if (mod?.AsyncClient && mod?.Log && mod?.LogGroup && mod?.PutLogsRequest) {
2476
+ return {
2477
+ AsyncClient: mod.AsyncClient,
2478
+ Log: mod.Log,
2479
+ LogGroup: mod.LogGroup,
2480
+ PutLogsRequest: mod.PutLogsRequest,
2481
+ };
2482
+ }
2483
+ return null;
2484
+ };
2485
+ // 1) 外部注入的 loader(最高优先级)
2486
+ if (this.sdkLoaderOverride) {
2487
+ try {
2488
+ const loaded = await this.sdkLoaderOverride();
2489
+ const sdk = normalizeSdk(loaded);
2490
+ if (sdk) {
2491
+ this.sdk = sdk;
2492
+ return sdk;
2493
+ }
2494
+ }
2495
+ catch {
2496
+ // ignore and fallback
2497
+ }
2498
+ }
2499
+ // 2) 外部直接注入的 sdk
2500
+ if (this.sdkOverride) {
2501
+ const sdk = normalizeSdk(this.sdkOverride);
2502
+ if (sdk) {
2503
+ this.sdk = sdk;
2504
+ return sdk;
2505
+ }
2506
+ }
2507
+ const isMini = this.envType === 'miniprogram';
2508
+ // 3) 尝试 require / 全局变量
2509
+ // Mini Program: 尝试直接 require
2510
+ if (isMini) {
2511
+ const reqMod = tryRequire('tencentcloud-cls-sdk-js-mini');
2512
+ const reqSdk = normalizeSdk(reqMod);
2513
+ if (reqSdk) {
2514
+ this.sdk = reqSdk;
2515
+ return reqSdk;
2516
+ }
2517
+ }
2518
+ else {
2519
+ // Web: 优先读全局变量 (UMD)
2520
+ const g = readGlobal('tencentcloudClsSdkJsWeb');
2521
+ const sdk = normalizeSdk(g);
2522
+ if (sdk) {
2523
+ this.sdk = sdk;
2524
+ return sdk;
2525
+ }
2526
+ // Web: 尝试 require
2527
+ const reqMod = tryRequire('tencentcloud-cls-sdk-js-web');
2528
+ const reqSdk = normalizeSdk(reqMod);
2529
+ if (reqSdk) {
2530
+ this.sdk = reqSdk;
2531
+ return reqSdk;
2532
+ }
2533
+ }
2534
+ // 4) 动态 import
2535
+ // 使用字符串字面量,确保 Bundler 能正确识别并进行 Code Splitting
2536
+ if (isMini) {
2537
+ this.sdkPromise = import('tencentcloud-cls-sdk-js-mini')
2538
+ .then((m) => {
2539
+ const sdk = normalizeSdk(m);
2540
+ if (!sdk)
2541
+ throw new Error(`ClsLogger.loadSdk: invalid sdk module for mini`);
2542
+ this.sdk = sdk;
2543
+ return sdk;
2544
+ })
2545
+ .catch((err) => {
2546
+ this.sdkPromise = null;
2547
+ throw err;
2548
+ });
2549
+ }
2550
+ else {
2551
+ this.sdkPromise = import('tencentcloud-cls-sdk-js-web')
2552
+ .then((m) => {
2553
+ const sdk = normalizeSdk(m);
2554
+ if (!sdk)
2555
+ throw new Error(`ClsLogger.loadSdk: invalid sdk module for web`);
2556
+ this.sdk = sdk;
2557
+ return sdk;
2558
+ })
2559
+ .catch((err) => {
2560
+ this.sdkPromise = null;
2561
+ throw err;
2562
+ });
2563
+ }
2564
+ return this.sdkPromise;
2565
+ }
2566
+ }
2567
+
2270
2568
  const clsLogger = new ClsLogger();
2271
2569
 
2272
2570
  exports.ClsLogger = ClsLogger;