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