@be-link/cls-logger 1.0.1-beta.1 → 1.0.1-beta.11

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
  }
@@ -540,6 +538,8 @@ function installRequestMonitor(report, opts = {}) {
540
538
  }
541
539
 
542
540
  const DEFAULT_MAX_TEXT = 4000;
541
+ const DEFAULT_DEDUPE_WINDOW_MS = 3000;
542
+ const DEFAULT_DEDUPE_MAX_KEYS = 200;
543
543
  function truncate$2(s, maxLen) {
544
544
  if (!s)
545
545
  return s;
@@ -565,7 +565,17 @@ function getPagePath$1() {
565
565
  function normalizeErrorLike(err, maxTextLength) {
566
566
  if (err && typeof err === 'object') {
567
567
  const anyErr = err;
568
- 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);
569
579
  const name = truncate$2(String(anyErr.name ?? ''), 200);
570
580
  const stack = truncate$2(String(anyErr.stack ?? ''), maxTextLength);
571
581
  return { message, name, stack };
@@ -573,6 +583,54 @@ function normalizeErrorLike(err, maxTextLength) {
573
583
  const message = truncate$2(stringifyLogValue(err), maxTextLength);
574
584
  return { message, name: '', stack: '' };
575
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
+ }
576
634
  function installBrowserErrorMonitor(report, options) {
577
635
  if (typeof window === 'undefined')
578
636
  return;
@@ -580,6 +638,7 @@ function installBrowserErrorMonitor(report, options) {
580
638
  if (w.__beLinkClsLoggerErrorInstalled__)
581
639
  return;
582
640
  w.__beLinkClsLoggerErrorInstalled__ = true;
641
+ const shouldReport = createDedupeGuard(options);
583
642
  window.addEventListener('error', (event) => {
584
643
  try {
585
644
  if (!sampleHit$1(options.sampleRate))
@@ -602,6 +661,8 @@ function installBrowserErrorMonitor(report, options) {
602
661
  if (e.stack)
603
662
  payload.stack = e.stack;
604
663
  }
664
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
665
+ return;
605
666
  report(options.reportType, payload);
606
667
  return;
607
668
  }
@@ -613,6 +674,8 @@ function installBrowserErrorMonitor(report, options) {
613
674
  payload.source = 'resource.error';
614
675
  payload.tagName = tagName;
615
676
  payload.resourceUrl = truncate$2(url, 2000);
677
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
678
+ return;
616
679
  report(options.reportType, payload);
617
680
  }
618
681
  }
@@ -633,6 +696,8 @@ function installBrowserErrorMonitor(report, options) {
633
696
  errorName: e.name,
634
697
  stack: e.stack,
635
698
  };
699
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
700
+ return;
636
701
  report(options.reportType, payload);
637
702
  }
638
703
  catch {
@@ -645,6 +710,7 @@ function installMiniProgramErrorMonitor(report, options) {
645
710
  if (g.__beLinkClsLoggerMpErrorInstalled__)
646
711
  return;
647
712
  g.__beLinkClsLoggerMpErrorInstalled__ = true;
713
+ const shouldReport = createDedupeGuard(options);
648
714
  const wxAny = globalThis.wx;
649
715
  // wx.* 事件(兼容在 App 已经创建后的场景)
650
716
  try {
@@ -653,10 +719,16 @@ function installMiniProgramErrorMonitor(report, options) {
653
719
  try {
654
720
  if (!sampleHit$1(options.sampleRate))
655
721
  return;
656
- report(options.reportType, {
722
+ const e = normalizeErrorLike(msg, options.maxTextLength);
723
+ const payload = {
657
724
  source: 'wx.onError',
658
- message: truncate$2(String(msg ?? ''), options.maxTextLength),
659
- });
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);
660
732
  }
661
733
  catch {
662
734
  // ignore
@@ -674,12 +746,15 @@ function installMiniProgramErrorMonitor(report, options) {
674
746
  if (!sampleHit$1(options.sampleRate))
675
747
  return;
676
748
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
677
- report(options.reportType, {
749
+ const payload = {
678
750
  source: 'wx.onUnhandledRejection',
679
751
  message: e.message,
680
752
  errorName: e.name,
681
753
  stack: e.stack,
682
- });
754
+ };
755
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
756
+ return;
757
+ report(options.reportType, payload);
683
758
  }
684
759
  catch {
685
760
  // ignore
@@ -701,10 +776,15 @@ function installMiniProgramErrorMonitor(report, options) {
701
776
  next.onError = function (...args) {
702
777
  try {
703
778
  if (sampleHit$1(options.sampleRate)) {
704
- report(options.reportType, {
779
+ const e = normalizeErrorLike(args?.[0], options.maxTextLength);
780
+ const payload = {
705
781
  source: 'App.onError',
706
- message: truncate$2(String(args?.[0] ?? ''), options.maxTextLength),
707
- });
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);
708
788
  }
709
789
  }
710
790
  catch {
@@ -720,12 +800,14 @@ function installMiniProgramErrorMonitor(report, options) {
720
800
  if (sampleHit$1(options.sampleRate)) {
721
801
  const reason = args?.[0]?.reason ?? args?.[0];
722
802
  const e = normalizeErrorLike(reason, options.maxTextLength);
723
- report(options.reportType, {
803
+ const payload = {
724
804
  source: 'App.onUnhandledRejection',
725
805
  message: e.message,
726
806
  errorName: e.name,
727
807
  stack: e.stack,
728
- });
808
+ };
809
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
810
+ report(options.reportType, payload);
729
811
  }
730
812
  }
731
813
  catch {
@@ -754,6 +836,8 @@ function installErrorMonitor(report, opts = {}) {
754
836
  sampleRate: raw.sampleRate ?? 1,
755
837
  captureResourceError: raw.captureResourceError ?? true,
756
838
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
839
+ dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
840
+ dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
757
841
  };
758
842
  if (isMiniProgramEnv()) {
759
843
  installMiniProgramErrorMonitor(report, options);
@@ -1004,140 +1088,58 @@ function installBrowserPerformanceMonitor(report, options) {
1004
1088
  }
1005
1089
  }
1006
1090
  }
1007
- function wrapMiniProgramRouteApi(report, reportType, apiName, options) {
1008
- const wxAny = globalThis.wx;
1009
- if (!wxAny || typeof wxAny[apiName] !== 'function')
1010
- return;
1011
- const flagKey = `__beLinkClsLoggerMpRouteWrapped__${apiName}`;
1012
- if (wxAny[flagKey])
1013
- return;
1014
- wxAny[flagKey] = true;
1015
- const raw = wxAny[apiName].bind(wxAny);
1016
- wxAny[apiName] = (opts) => {
1017
- const start = Date.now();
1018
- const url = opts?.url ? String(opts.url) : '';
1019
- const wrapCb = (cb, success) => {
1020
- return (...args) => {
1021
- try {
1022
- if (sampleHit(options.sampleRate)) {
1023
- report(reportType, {
1024
- metric: 'route',
1025
- api: apiName,
1026
- url,
1027
- duration: Date.now() - start,
1028
- success: success ? 1 : 0,
1029
- error: success ? '' : truncate$1(stringifyLogValue(args?.[0]), options.maxTextLength),
1030
- unit: 'ms',
1031
- });
1032
- }
1033
- }
1034
- catch {
1035
- // ignore
1036
- }
1037
- if (typeof cb === 'function')
1038
- return cb(...args);
1039
- return undefined;
1040
- };
1041
- };
1042
- const next = { ...(opts ?? {}) };
1043
- next.success = wrapCb(next.success, true);
1044
- next.fail = wrapCb(next.fail, false);
1045
- return raw(next);
1046
- };
1047
- }
1048
- function installMiniProgramPageRenderMonitor(report, reportType, options) {
1049
- const g = globalThis;
1050
- if (typeof g.Page !== 'function')
1051
- return;
1052
- if (g.__beLinkClsLoggerPageWrapped__)
1053
- return;
1054
- g.__beLinkClsLoggerPageWrapped__ = true;
1055
- const rawPage = g.Page;
1056
- g.Page = (pageOptions) => {
1057
- const next = { ...(pageOptions ?? {}) };
1058
- const rawOnLoad = next.onLoad;
1059
- const rawOnReady = next.onReady;
1060
- next.onLoad = function (...args) {
1061
- try {
1062
- this.__beLinkClsLoggerPageLoadTs__ = Date.now();
1063
- }
1064
- catch {
1065
- // ignore
1066
- }
1067
- if (typeof rawOnLoad === 'function')
1068
- return rawOnLoad.apply(this, args);
1069
- return undefined;
1070
- };
1071
- next.onReady = function (...args) {
1072
- try {
1073
- const start = this.__beLinkClsLoggerPageLoadTs__;
1074
- if (typeof start === 'number' && sampleHit(options.sampleRate)) {
1075
- report(reportType, {
1076
- metric: 'page-render',
1077
- route: this?.route ? String(this.route) : '',
1078
- duration: Date.now() - start,
1079
- unit: 'ms',
1080
- });
1081
- }
1082
- }
1083
- catch {
1084
- // ignore
1085
- }
1086
- if (typeof rawOnReady === 'function')
1087
- return rawOnReady.apply(this, args);
1088
- return undefined;
1089
- };
1090
- return rawPage(next);
1091
- };
1092
- }
1093
1091
  function installMiniProgramPerformanceMonitor(report, options) {
1094
1092
  const g = globalThis;
1093
+ const ctx = g.wx || g.Taro;
1094
+ if (!ctx || typeof ctx.getPerformance !== 'function')
1095
+ return;
1095
1096
  if (g.__beLinkClsLoggerMpPerfInstalled__)
1096
1097
  return;
1097
1098
  g.__beLinkClsLoggerMpPerfInstalled__ = true;
1098
- // 路由切换耗时(用 API 回调近似)
1099
- for (const apiName of ['navigateTo', 'redirectTo', 'switchTab', 'reLaunch']) {
1100
- try {
1101
- wrapMiniProgramRouteApi(report, options.reportType, apiName, options);
1102
- }
1103
- catch {
1104
- // ignore
1105
- }
1106
- }
1107
- // 页面渲染耗时(onLoad -> onReady)
1108
- try {
1109
- installMiniProgramPageRenderMonitor(report, options.reportType, options);
1110
- }
1111
- catch {
1112
- // ignore
1113
- }
1114
- // wx.getPerformance()(若可用,尝试读取已有 entries)
1115
1099
  try {
1116
- const wxAny = globalThis.wx;
1117
- if (wxAny && typeof wxAny.getPerformance === 'function') {
1118
- const perf = wxAny.getPerformance();
1119
- if (perf && isPlainObject(perf)) {
1120
- // 不同基础库实现差异较大:尽量容错
1121
- setTimeout(() => {
1122
- try {
1123
- if (!sampleHit(options.sampleRate))
1124
- return;
1125
- const entries = typeof perf.getEntries === 'function'
1126
- ? perf.getEntries()
1127
- : typeof perf.getEntriesByType === 'function'
1128
- ? perf.getEntriesByType('navigation')
1129
- : [];
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') {
1130
1111
  report(options.reportType, {
1131
- metric: 'mp-performance',
1132
- entries: truncate$1(stringifyLogValue(entries), options.maxTextLength),
1112
+ metric: 'page-render',
1113
+ duration: entry.duration,
1114
+ pagePath: entry.path || '',
1115
+ unit: 'ms',
1133
1116
  });
1134
1117
  }
1135
- catch {
1136
- // 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
+ });
1137
1135
  }
1138
- }, 0);
1136
+ }
1139
1137
  }
1140
- }
1138
+ catch {
1139
+ // ignore
1140
+ }
1141
+ });
1142
+ observer.observe({ entryTypes: ['navigation', 'render'] });
1141
1143
  }
1142
1144
  catch {
1143
1145
  // ignore
@@ -1461,7 +1463,7 @@ function installBehaviorMonitor(report, envType, options = {}) {
1461
1463
  const tag = (el.tagName || '').toLowerCase();
1462
1464
  const trackId = getAttr(el, clickTrackIdAttr);
1463
1465
  // 过滤无效点击:白名单 tag + 没有 trackId
1464
- if (clickWhiteList.includes(tag) && !trackId)
1466
+ if (clickWhiteList.includes(tag) || !trackId)
1465
1467
  return;
1466
1468
  void uvStatePromise.then(({ uvId, meta }) => {
1467
1469
  if (destroyed)
@@ -1502,8 +1504,12 @@ function installBehaviorMonitor(report, envType, options = {}) {
1502
1504
  g.Page = function patchedPage(conf) {
1503
1505
  const originalOnShow = conf?.onShow;
1504
1506
  conf.onShow = function (...args) {
1505
- if (pvEnabled)
1506
- reportPv(getPagePath());
1507
+ if (pvEnabled) {
1508
+ const pagePath = getPagePath();
1509
+ if (pagePath?.length > 0) {
1510
+ reportPv(pagePath);
1511
+ }
1512
+ }
1507
1513
  return typeof originalOnShow === 'function' ? originalOnShow.apply(this, args) : undefined;
1508
1514
  };
1509
1515
  // 点击:wrap 页面 methods(bindtap 等会调用到这里的 handler)
@@ -1839,9 +1845,20 @@ function enterClsSendingGuard() {
1839
1845
  g.__beLinkClsLoggerSendingCount__ = cur > 0 ? cur - 1 : 0;
1840
1846
  };
1841
1847
  }
1842
- class ClsLogger {
1848
+ /**
1849
+ * CLS Logger 核心基类
1850
+ * - 负责所有上报、队列、监控逻辑
1851
+ * - 不包含具体的 SDK 加载实现(由子类负责)
1852
+ * - 这样可以把 web/mini 的 SDK 依赖彻底解耦到子入口
1853
+ */
1854
+ class ClsLoggerCore {
1843
1855
  constructor() {
1856
+ this.sdk = null;
1857
+ this.sdkPromise = null;
1858
+ this.sdkOverride = null;
1859
+ this.sdkLoaderOverride = null;
1844
1860
  this.client = null;
1861
+ this.clientPromise = null;
1845
1862
  this.topicId = null;
1846
1863
  this.endpoint = 'ap-shanghai.cls.tencentcs.com';
1847
1864
  this.retryTimes = 10;
@@ -1872,6 +1889,20 @@ class ClsLogger {
1872
1889
  this.behaviorMonitorStarted = false;
1873
1890
  this.behaviorMonitorCleanup = null;
1874
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
+ }
1875
1906
  init(options) {
1876
1907
  this.initTs = Date.now();
1877
1908
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId ?? null;
@@ -1883,15 +1914,33 @@ class ClsLogger {
1883
1914
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
1884
1915
  return;
1885
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
+ }
1886
1930
  this.topicId = topicId;
1887
1931
  this.endpoint = endpoint;
1888
1932
  this.retryTimes = retryTimes;
1889
1933
  this.source = source;
1934
+ this.userId = options.userId ?? this.userId;
1935
+ this.userName = options.userName ?? this.userName;
1890
1936
  this.projectId = options.projectId ?? this.projectId;
1891
1937
  this.projectName = options.projectName ?? this.projectName;
1892
1938
  this.appId = options.appId ?? this.appId;
1893
1939
  this.appVersion = options.appVersion ?? this.appVersion;
1894
- 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;
1895
1944
  this.userGenerateBaseFields = options.generateBaseFields ?? this.userGenerateBaseFields;
1896
1945
  this.autoGenerateBaseFields = createAutoDeviceInfoBaseFields(this.envType, options.deviceInfo);
1897
1946
  this.storageKey = options.storageKey ?? this.storageKey;
@@ -1901,8 +1950,10 @@ class ClsLogger {
1901
1950
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
1902
1951
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
1903
1952
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
1904
- // 预热 client
1905
- this.getInstance();
1953
+ // 预热(避免首条日志触发 import/初始化开销)
1954
+ void this.getInstance().catch(() => {
1955
+ // ignore
1956
+ });
1906
1957
  // 启动时尝试发送失败缓存
1907
1958
  this.flushFailed();
1908
1959
  // 初始化后立即启动请求监听
@@ -1927,7 +1978,7 @@ class ClsLogger {
1927
1978
  try {
1928
1979
  const userRaw = this.userGenerateBaseFields ? this.userGenerateBaseFields() : undefined;
1929
1980
  if (userRaw && isPlainObject(userRaw))
1930
- user = normalizeFlatFields(userRaw, 'generateBaseFields');
1981
+ user = normalizeFlatFields({ ...userRaw }, 'generateBaseFields');
1931
1982
  }
1932
1983
  catch {
1933
1984
  user = undefined;
@@ -2006,20 +2057,29 @@ class ClsLogger {
2006
2057
  this.behaviorMonitorCleanup = null;
2007
2058
  this.behaviorMonitorStarted = false;
2008
2059
  }
2009
- getInstance() {
2060
+ /**
2061
+ * 获取 CLS client(按环境懒加载 SDK)
2062
+ */
2063
+ async getInstance() {
2010
2064
  if (this.client)
2011
2065
  return this.client;
2012
- this.client = new tencentcloudClsSdkJsWeb.AsyncClient({
2013
- endpoint: this.endpoint,
2014
- 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;
2015
2081
  });
2016
- return this.client;
2017
- }
2018
- detectEnvType() {
2019
- const wxAny = globalThis.wx;
2020
- if (wxAny && typeof wxAny.getSystemInfoSync === 'function')
2021
- return 'miniprogram';
2022
- return 'browser';
2082
+ return this.clientPromise;
2023
2083
  }
2024
2084
  /**
2025
2085
  * 直接上报:埋点入参必须是一维(扁平)Object
@@ -2045,22 +2105,33 @@ class ClsLogger {
2045
2105
  appVersion: this.appVersion || undefined,
2046
2106
  ...normalizedFields,
2047
2107
  });
2048
- const client = this.getInstance();
2049
- 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');
2050
2119
  logGroup.setSource(this.source);
2051
- const log = new tencentcloudClsSdkJsWeb.Log(Date.now());
2120
+ const log = new sdk.Log(Date.now());
2052
2121
  for (const key of Object.keys(finalFields)) {
2053
2122
  log.addContent(key, stringifyLogValue(finalFields[key]));
2054
2123
  }
2055
2124
  logGroup.addLog(log);
2056
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2125
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2057
2126
  const exit = enterClsSendingGuard();
2127
+ let p;
2058
2128
  try {
2059
- client.PutLogs(request);
2129
+ p = client.PutLogs(request);
2060
2130
  }
2061
2131
  finally {
2062
2132
  exit();
2063
2133
  }
2134
+ await p;
2064
2135
  }
2065
2136
  /**
2066
2137
  * 直接上报:把 data 序列化后放入指定 key(默认 “日志内容”)
@@ -2119,11 +2190,19 @@ class ClsLogger {
2119
2190
  console.warn('ClsLogger.putBatch:未初始化 topic_id');
2120
2191
  return;
2121
2192
  }
2122
- const client = this.getInstance();
2123
- 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');
2124
2203
  logGroup.setSource(this.source);
2125
2204
  for (const item of queue) {
2126
- const log = new tencentcloudClsSdkJsWeb.Log(item.time);
2205
+ const log = new sdk.Log(item.time);
2127
2206
  const data = item.data ?? {};
2128
2207
  for (const key of Object.keys(data)) {
2129
2208
  log.addContent(key, stringifyLogValue(data[key]));
@@ -2132,14 +2211,16 @@ class ClsLogger {
2132
2211
  }
2133
2212
  if (logGroup.getLogs().length === 0)
2134
2213
  return;
2135
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2214
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2136
2215
  const exit = enterClsSendingGuard();
2216
+ let p;
2137
2217
  try {
2138
- client.PutLogs(request);
2218
+ p = client.PutLogs(request);
2139
2219
  }
2140
2220
  finally {
2141
2221
  exit();
2142
2222
  }
2223
+ await p;
2143
2224
  }
2144
2225
  /**
2145
2226
  * 参考《一、概述》:统一上报入口(内存队列 + 批量发送)
@@ -2252,12 +2333,13 @@ class ClsLogger {
2252
2333
  async sendReportLogs(logs) {
2253
2334
  if (!this.topicId)
2254
2335
  return;
2255
- const client = this.getInstance();
2256
- 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');
2257
2339
  logGroup.setSource(this.source);
2258
2340
  for (const item of logs) {
2259
2341
  const fields = this.buildReportFields(item);
2260
- const log = new tencentcloudClsSdkJsWeb.Log(fields.timestamp);
2342
+ const log = new sdk.Log(fields.timestamp);
2261
2343
  for (const key of Object.keys(fields)) {
2262
2344
  if (key === 'timestamp')
2263
2345
  continue;
@@ -2265,7 +2347,7 @@ class ClsLogger {
2265
2347
  }
2266
2348
  logGroup.addLog(log);
2267
2349
  }
2268
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2350
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2269
2351
  // 只在“发起网络请求”的同步阶段打标记,避免 requestMonitor 监控 CLS 上报请求导致递归
2270
2352
  const exit = enterClsSendingGuard();
2271
2353
  let p;
@@ -2341,6 +2423,162 @@ class ClsLogger {
2341
2423
  }
2342
2424
  }
2343
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
+ // 显式使用字面量 require,帮助打包工具识别依赖(尤其是 CJS 构建模式下)
2510
+ try {
2511
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
2512
+ const req = typeof require === 'function' ? require('tencentcloud-cls-sdk-js-mini') : null;
2513
+ const reqSdk = normalizeSdk(req);
2514
+ if (reqSdk) {
2515
+ this.sdk = reqSdk;
2516
+ return reqSdk;
2517
+ }
2518
+ }
2519
+ catch {
2520
+ // ignore
2521
+ }
2522
+ // 尝试使用 tryRequire(作为补充)
2523
+ const reqMod = tryRequire('tencentcloud-cls-sdk-js-mini');
2524
+ const reqSdk = normalizeSdk(reqMod);
2525
+ if (reqSdk) {
2526
+ this.sdk = reqSdk;
2527
+ return reqSdk;
2528
+ }
2529
+ }
2530
+ else {
2531
+ // Web: 优先读全局变量 (UMD)
2532
+ const g = readGlobal('tencentcloudClsSdkJsWeb');
2533
+ const sdk = normalizeSdk(g);
2534
+ if (sdk) {
2535
+ this.sdk = sdk;
2536
+ return sdk;
2537
+ }
2538
+ // Web: 尝试 require
2539
+ const reqMod = tryRequire('tencentcloud-cls-sdk-js-web');
2540
+ const reqSdk = normalizeSdk(reqMod);
2541
+ if (reqSdk) {
2542
+ this.sdk = reqSdk;
2543
+ return reqSdk;
2544
+ }
2545
+ }
2546
+ // 4) 动态 import
2547
+ // 使用字符串字面量,确保 Bundler 能正确识别并进行 Code Splitting
2548
+ if (isMini) {
2549
+ this.sdkPromise = import('tencentcloud-cls-sdk-js-mini')
2550
+ .then((m) => {
2551
+ const sdk = normalizeSdk(m);
2552
+ if (!sdk)
2553
+ throw new Error(`ClsLogger.loadSdk: invalid sdk module for mini`);
2554
+ this.sdk = sdk;
2555
+ return sdk;
2556
+ })
2557
+ .catch((err) => {
2558
+ this.sdkPromise = null;
2559
+ console.error('[ClsLogger] Failed to load mini sdk:', err);
2560
+ throw err;
2561
+ });
2562
+ }
2563
+ else {
2564
+ this.sdkPromise = import('tencentcloud-cls-sdk-js-web')
2565
+ .then((m) => {
2566
+ const sdk = normalizeSdk(m);
2567
+ if (!sdk)
2568
+ throw new Error(`ClsLogger.loadSdk: invalid sdk module for web`);
2569
+ this.sdk = sdk;
2570
+ return sdk;
2571
+ })
2572
+ .catch((err) => {
2573
+ this.sdkPromise = null;
2574
+ console.error('[ClsLogger] Failed to load web sdk:', err);
2575
+ throw err;
2576
+ });
2577
+ }
2578
+ return this.sdkPromise;
2579
+ }
2580
+ }
2581
+
2344
2582
  const clsLogger = new ClsLogger();
2345
2583
 
2346
2584
  exports.ClsLogger = ClsLogger;