@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.esm.js CHANGED
@@ -1,5 +1,3 @@
1
- import { AsyncClient, LogGroup, Log, PutLogsRequest } from 'tencentcloud-cls-sdk-js-web';
2
-
3
1
  function isPlainObject(value) {
4
2
  return Object.prototype.toString.call(value) === '[object Object]';
5
3
  }
@@ -536,6 +534,8 @@ function installRequestMonitor(report, opts = {}) {
536
534
  }
537
535
 
538
536
  const DEFAULT_MAX_TEXT = 4000;
537
+ const DEFAULT_DEDUPE_WINDOW_MS = 3000;
538
+ const DEFAULT_DEDUPE_MAX_KEYS = 200;
539
539
  function truncate$2(s, maxLen) {
540
540
  if (!s)
541
541
  return s;
@@ -561,7 +561,17 @@ function getPagePath$1() {
561
561
  function normalizeErrorLike(err, maxTextLength) {
562
562
  if (err && typeof err === 'object') {
563
563
  const anyErr = err;
564
- const message = truncate$2(String(anyErr.message ?? anyErr.toString?.() ?? ''), maxTextLength);
564
+ let rawMsg = anyErr.message;
565
+ if (!rawMsg) {
566
+ const str = anyErr.toString?.();
567
+ if (!str || str === '[object Object]') {
568
+ rawMsg = stringifyLogValue(anyErr);
569
+ }
570
+ else {
571
+ rawMsg = str;
572
+ }
573
+ }
574
+ const message = truncate$2(String(rawMsg ?? ''), maxTextLength);
565
575
  const name = truncate$2(String(anyErr.name ?? ''), 200);
566
576
  const stack = truncate$2(String(anyErr.stack ?? ''), maxTextLength);
567
577
  return { message, name, stack };
@@ -569,6 +579,54 @@ function normalizeErrorLike(err, maxTextLength) {
569
579
  const message = truncate$2(stringifyLogValue(err), maxTextLength);
570
580
  return { message, name: '', stack: '' };
571
581
  }
582
+ function createDedupeGuard(options) {
583
+ const cache = new Map(); // key -> lastReportAt
584
+ const maxKeys = Math.max(0, options.dedupeMaxKeys);
585
+ const windowMs = Math.max(0, options.dedupeWindowMs);
586
+ function touch(key, now) {
587
+ // refresh insertion order
588
+ if (cache.has(key))
589
+ cache.delete(key);
590
+ cache.set(key, now);
591
+ if (maxKeys > 0) {
592
+ while (cache.size > maxKeys) {
593
+ const first = cache.keys().next().value;
594
+ if (!first)
595
+ break;
596
+ cache.delete(first);
597
+ }
598
+ }
599
+ }
600
+ return (key) => {
601
+ if (!key)
602
+ return true;
603
+ if (windowMs <= 0 || maxKeys === 0)
604
+ return true;
605
+ const now = Date.now();
606
+ const last = cache.get(key);
607
+ if (typeof last === 'number' && now - last < windowMs)
608
+ return false;
609
+ touch(key, now);
610
+ return true;
611
+ };
612
+ }
613
+ function buildErrorKey(type, payload) {
614
+ // 使用最核心、最稳定的字段构造签名,避免把瞬态字段(如 time)带入导致失效
615
+ const parts = [
616
+ type,
617
+ String(payload.source ?? ''),
618
+ String(payload.pagePath ?? ''),
619
+ String(payload.message ?? ''),
620
+ String(payload.errorName ?? ''),
621
+ String(payload.stack ?? ''),
622
+ String(payload.filename ?? ''),
623
+ String(payload.lineno ?? ''),
624
+ String(payload.colno ?? ''),
625
+ String(payload.tagName ?? ''),
626
+ String(payload.resourceUrl ?? ''),
627
+ ];
628
+ return parts.join('|');
629
+ }
572
630
  function installBrowserErrorMonitor(report, options) {
573
631
  if (typeof window === 'undefined')
574
632
  return;
@@ -576,6 +634,7 @@ function installBrowserErrorMonitor(report, options) {
576
634
  if (w.__beLinkClsLoggerErrorInstalled__)
577
635
  return;
578
636
  w.__beLinkClsLoggerErrorInstalled__ = true;
637
+ const shouldReport = createDedupeGuard(options);
579
638
  window.addEventListener('error', (event) => {
580
639
  try {
581
640
  if (!sampleHit$1(options.sampleRate))
@@ -598,6 +657,8 @@ function installBrowserErrorMonitor(report, options) {
598
657
  if (e.stack)
599
658
  payload.stack = e.stack;
600
659
  }
660
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
661
+ return;
601
662
  report(options.reportType, payload);
602
663
  return;
603
664
  }
@@ -609,6 +670,8 @@ function installBrowserErrorMonitor(report, options) {
609
670
  payload.source = 'resource.error';
610
671
  payload.tagName = tagName;
611
672
  payload.resourceUrl = truncate$2(url, 2000);
673
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
674
+ return;
612
675
  report(options.reportType, payload);
613
676
  }
614
677
  }
@@ -629,6 +692,8 @@ function installBrowserErrorMonitor(report, options) {
629
692
  errorName: e.name,
630
693
  stack: e.stack,
631
694
  };
695
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
696
+ return;
632
697
  report(options.reportType, payload);
633
698
  }
634
699
  catch {
@@ -641,6 +706,7 @@ function installMiniProgramErrorMonitor(report, options) {
641
706
  if (g.__beLinkClsLoggerMpErrorInstalled__)
642
707
  return;
643
708
  g.__beLinkClsLoggerMpErrorInstalled__ = true;
709
+ const shouldReport = createDedupeGuard(options);
644
710
  const wxAny = globalThis.wx;
645
711
  // wx.* 事件(兼容在 App 已经创建后的场景)
646
712
  try {
@@ -649,10 +715,16 @@ function installMiniProgramErrorMonitor(report, options) {
649
715
  try {
650
716
  if (!sampleHit$1(options.sampleRate))
651
717
  return;
652
- report(options.reportType, {
718
+ const e = normalizeErrorLike(msg, options.maxTextLength);
719
+ const payload = {
653
720
  source: 'wx.onError',
654
- message: truncate$2(String(msg ?? ''), options.maxTextLength),
655
- });
721
+ message: e.message,
722
+ errorName: e.name,
723
+ stack: e.stack,
724
+ };
725
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
726
+ return;
727
+ report(options.reportType, payload);
656
728
  }
657
729
  catch {
658
730
  // ignore
@@ -670,12 +742,15 @@ function installMiniProgramErrorMonitor(report, options) {
670
742
  if (!sampleHit$1(options.sampleRate))
671
743
  return;
672
744
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
673
- report(options.reportType, {
745
+ const payload = {
674
746
  source: 'wx.onUnhandledRejection',
675
747
  message: e.message,
676
748
  errorName: e.name,
677
749
  stack: e.stack,
678
- });
750
+ };
751
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
752
+ return;
753
+ report(options.reportType, payload);
679
754
  }
680
755
  catch {
681
756
  // ignore
@@ -697,10 +772,15 @@ function installMiniProgramErrorMonitor(report, options) {
697
772
  next.onError = function (...args) {
698
773
  try {
699
774
  if (sampleHit$1(options.sampleRate)) {
700
- report(options.reportType, {
775
+ const e = normalizeErrorLike(args?.[0], options.maxTextLength);
776
+ const payload = {
701
777
  source: 'App.onError',
702
- message: truncate$2(String(args?.[0] ?? ''), options.maxTextLength),
703
- });
778
+ message: e.message,
779
+ errorName: e.name,
780
+ stack: e.stack,
781
+ };
782
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
783
+ report(options.reportType, payload);
704
784
  }
705
785
  }
706
786
  catch {
@@ -716,12 +796,14 @@ function installMiniProgramErrorMonitor(report, options) {
716
796
  if (sampleHit$1(options.sampleRate)) {
717
797
  const reason = args?.[0]?.reason ?? args?.[0];
718
798
  const e = normalizeErrorLike(reason, options.maxTextLength);
719
- report(options.reportType, {
799
+ const payload = {
720
800
  source: 'App.onUnhandledRejection',
721
801
  message: e.message,
722
802
  errorName: e.name,
723
803
  stack: e.stack,
724
- });
804
+ };
805
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
806
+ report(options.reportType, payload);
725
807
  }
726
808
  }
727
809
  catch {
@@ -750,6 +832,8 @@ function installErrorMonitor(report, opts = {}) {
750
832
  sampleRate: raw.sampleRate ?? 1,
751
833
  captureResourceError: raw.captureResourceError ?? true,
752
834
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
835
+ dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
836
+ dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
753
837
  };
754
838
  if (isMiniProgramEnv()) {
755
839
  installMiniProgramErrorMonitor(report, options);
@@ -1000,140 +1084,58 @@ function installBrowserPerformanceMonitor(report, options) {
1000
1084
  }
1001
1085
  }
1002
1086
  }
1003
- function wrapMiniProgramRouteApi(report, reportType, apiName, options) {
1004
- const wxAny = globalThis.wx;
1005
- if (!wxAny || typeof wxAny[apiName] !== 'function')
1006
- return;
1007
- const flagKey = `__beLinkClsLoggerMpRouteWrapped__${apiName}`;
1008
- if (wxAny[flagKey])
1009
- return;
1010
- wxAny[flagKey] = true;
1011
- const raw = wxAny[apiName].bind(wxAny);
1012
- wxAny[apiName] = (opts) => {
1013
- const start = Date.now();
1014
- const url = opts?.url ? String(opts.url) : '';
1015
- const wrapCb = (cb, success) => {
1016
- return (...args) => {
1017
- try {
1018
- if (sampleHit(options.sampleRate)) {
1019
- report(reportType, {
1020
- metric: 'route',
1021
- api: apiName,
1022
- url,
1023
- duration: Date.now() - start,
1024
- success: success ? 1 : 0,
1025
- error: success ? '' : truncate$1(stringifyLogValue(args?.[0]), options.maxTextLength),
1026
- unit: 'ms',
1027
- });
1028
- }
1029
- }
1030
- catch {
1031
- // ignore
1032
- }
1033
- if (typeof cb === 'function')
1034
- return cb(...args);
1035
- return undefined;
1036
- };
1037
- };
1038
- const next = { ...(opts ?? {}) };
1039
- next.success = wrapCb(next.success, true);
1040
- next.fail = wrapCb(next.fail, false);
1041
- return raw(next);
1042
- };
1043
- }
1044
- function installMiniProgramPageRenderMonitor(report, reportType, options) {
1045
- const g = globalThis;
1046
- if (typeof g.Page !== 'function')
1047
- return;
1048
- if (g.__beLinkClsLoggerPageWrapped__)
1049
- return;
1050
- g.__beLinkClsLoggerPageWrapped__ = true;
1051
- const rawPage = g.Page;
1052
- g.Page = (pageOptions) => {
1053
- const next = { ...(pageOptions ?? {}) };
1054
- const rawOnLoad = next.onLoad;
1055
- const rawOnReady = next.onReady;
1056
- next.onLoad = function (...args) {
1057
- try {
1058
- this.__beLinkClsLoggerPageLoadTs__ = Date.now();
1059
- }
1060
- catch {
1061
- // ignore
1062
- }
1063
- if (typeof rawOnLoad === 'function')
1064
- return rawOnLoad.apply(this, args);
1065
- return undefined;
1066
- };
1067
- next.onReady = function (...args) {
1068
- try {
1069
- const start = this.__beLinkClsLoggerPageLoadTs__;
1070
- if (typeof start === 'number' && sampleHit(options.sampleRate)) {
1071
- report(reportType, {
1072
- metric: 'page-render',
1073
- route: this?.route ? String(this.route) : '',
1074
- duration: Date.now() - start,
1075
- unit: 'ms',
1076
- });
1077
- }
1078
- }
1079
- catch {
1080
- // ignore
1081
- }
1082
- if (typeof rawOnReady === 'function')
1083
- return rawOnReady.apply(this, args);
1084
- return undefined;
1085
- };
1086
- return rawPage(next);
1087
- };
1088
- }
1089
1087
  function installMiniProgramPerformanceMonitor(report, options) {
1090
1088
  const g = globalThis;
1089
+ const ctx = g.wx || g.Taro;
1090
+ if (!ctx || typeof ctx.getPerformance !== 'function')
1091
+ return;
1091
1092
  if (g.__beLinkClsLoggerMpPerfInstalled__)
1092
1093
  return;
1093
1094
  g.__beLinkClsLoggerMpPerfInstalled__ = true;
1094
- // 路由切换耗时(用 API 回调近似)
1095
- for (const apiName of ['navigateTo', 'redirectTo', 'switchTab', 'reLaunch']) {
1096
- try {
1097
- wrapMiniProgramRouteApi(report, options.reportType, apiName, options);
1098
- }
1099
- catch {
1100
- // ignore
1101
- }
1102
- }
1103
- // 页面渲染耗时(onLoad -> onReady)
1104
- try {
1105
- installMiniProgramPageRenderMonitor(report, options.reportType, options);
1106
- }
1107
- catch {
1108
- // ignore
1109
- }
1110
- // wx.getPerformance()(若可用,尝试读取已有 entries)
1111
1095
  try {
1112
- const wxAny = globalThis.wx;
1113
- if (wxAny && typeof wxAny.getPerformance === 'function') {
1114
- const perf = wxAny.getPerformance();
1115
- if (perf && isPlainObject(perf)) {
1116
- // 不同基础库实现差异较大:尽量容错
1117
- setTimeout(() => {
1118
- try {
1119
- if (!sampleHit(options.sampleRate))
1120
- return;
1121
- const entries = typeof perf.getEntries === 'function'
1122
- ? perf.getEntries()
1123
- : typeof perf.getEntriesByType === 'function'
1124
- ? perf.getEntriesByType('navigation')
1125
- : [];
1096
+ const perf = ctx.getPerformance();
1097
+ if (!perf || typeof perf.createObserver !== 'function')
1098
+ return;
1099
+ const observer = perf.createObserver((entryList) => {
1100
+ try {
1101
+ const entries = entryList.getEntries();
1102
+ for (const entry of entries) {
1103
+ if (!sampleHit(options.sampleRate))
1104
+ continue;
1105
+ // Page Render: firstRender
1106
+ if (entry.entryType === 'render' && entry.name === 'firstRender') {
1126
1107
  report(options.reportType, {
1127
- metric: 'mp-performance',
1128
- entries: truncate$1(stringifyLogValue(entries), options.maxTextLength),
1108
+ metric: 'page-render',
1109
+ duration: entry.duration,
1110
+ pagePath: entry.path || '',
1111
+ unit: 'ms',
1129
1112
  });
1130
1113
  }
1131
- catch {
1132
- // ignore
1114
+ // Route Switch: route
1115
+ else if (entry.entryType === 'navigation' && entry.name === 'route') {
1116
+ report(options.reportType, {
1117
+ metric: 'route',
1118
+ duration: entry.duration,
1119
+ pagePath: entry.path || '',
1120
+ unit: 'ms',
1121
+ });
1122
+ }
1123
+ // App Launch: appLaunch (Cold)
1124
+ else if (entry.entryType === 'navigation' && entry.name === 'appLaunch') {
1125
+ report(options.reportType, {
1126
+ metric: 'app-launch',
1127
+ duration: entry.duration,
1128
+ launchType: 'cold',
1129
+ unit: 'ms',
1130
+ });
1133
1131
  }
1134
- }, 0);
1132
+ }
1135
1133
  }
1136
- }
1134
+ catch {
1135
+ // ignore
1136
+ }
1137
+ });
1138
+ observer.observe({ entryTypes: ['navigation', 'render'] });
1137
1139
  }
1138
1140
  catch {
1139
1141
  // ignore
@@ -1457,7 +1459,7 @@ function installBehaviorMonitor(report, envType, options = {}) {
1457
1459
  const tag = (el.tagName || '').toLowerCase();
1458
1460
  const trackId = getAttr(el, clickTrackIdAttr);
1459
1461
  // 过滤无效点击:白名单 tag + 没有 trackId
1460
- if (clickWhiteList.includes(tag) && !trackId)
1462
+ if (clickWhiteList.includes(tag) || !trackId)
1461
1463
  return;
1462
1464
  void uvStatePromise.then(({ uvId, meta }) => {
1463
1465
  if (destroyed)
@@ -1498,8 +1500,12 @@ function installBehaviorMonitor(report, envType, options = {}) {
1498
1500
  g.Page = function patchedPage(conf) {
1499
1501
  const originalOnShow = conf?.onShow;
1500
1502
  conf.onShow = function (...args) {
1501
- if (pvEnabled)
1502
- reportPv(getPagePath());
1503
+ if (pvEnabled) {
1504
+ const pagePath = getPagePath();
1505
+ if (pagePath?.length > 0) {
1506
+ reportPv(pagePath);
1507
+ }
1508
+ }
1503
1509
  return typeof originalOnShow === 'function' ? originalOnShow.apply(this, args) : undefined;
1504
1510
  };
1505
1511
  // 点击:wrap 页面 methods(bindtap 等会调用到这里的 handler)
@@ -1835,9 +1841,20 @@ function enterClsSendingGuard() {
1835
1841
  g.__beLinkClsLoggerSendingCount__ = cur > 0 ? cur - 1 : 0;
1836
1842
  };
1837
1843
  }
1838
- class ClsLogger {
1844
+ /**
1845
+ * CLS Logger 核心基类
1846
+ * - 负责所有上报、队列、监控逻辑
1847
+ * - 不包含具体的 SDK 加载实现(由子类负责)
1848
+ * - 这样可以把 web/mini 的 SDK 依赖彻底解耦到子入口
1849
+ */
1850
+ class ClsLoggerCore {
1839
1851
  constructor() {
1852
+ this.sdk = null;
1853
+ this.sdkPromise = null;
1854
+ this.sdkOverride = null;
1855
+ this.sdkLoaderOverride = null;
1840
1856
  this.client = null;
1857
+ this.clientPromise = null;
1841
1858
  this.topicId = null;
1842
1859
  this.endpoint = 'ap-shanghai.cls.tencentcs.com';
1843
1860
  this.retryTimes = 10;
@@ -1868,6 +1885,20 @@ class ClsLogger {
1868
1885
  this.behaviorMonitorStarted = false;
1869
1886
  this.behaviorMonitorCleanup = null;
1870
1887
  }
1888
+ /**
1889
+ * 子类可按需重写(默认检测 wx)
1890
+ */
1891
+ detectEnvType() {
1892
+ const g = globalThis;
1893
+ // 微信、支付宝、字节跳动、UniApp 等小程序环境通常都有特定全局变量
1894
+ if ((g.wx && typeof g.wx.getSystemInfoSync === 'function') ||
1895
+ (g.my && typeof g.my.getSystemInfoSync === 'function') ||
1896
+ (g.tt && typeof g.tt.getSystemInfoSync === 'function') ||
1897
+ (g.uni && typeof g.uni.getSystemInfoSync === 'function')) {
1898
+ return 'miniprogram';
1899
+ }
1900
+ return 'browser';
1901
+ }
1871
1902
  init(options) {
1872
1903
  this.initTs = Date.now();
1873
1904
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId ?? null;
@@ -1879,15 +1910,33 @@ class ClsLogger {
1879
1910
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
1880
1911
  return;
1881
1912
  }
1913
+ const nextEnvType = options.envType ?? this.detectEnvType();
1914
+ // envType/endpoint/retryTimes 变化时:重置 client(以及可能的 sdk)
1915
+ const envChanged = nextEnvType !== this.envType;
1916
+ const endpointChanged = endpoint !== this.endpoint;
1917
+ const retryChanged = retryTimes !== this.retryTimes;
1918
+ if (envChanged || endpointChanged || retryChanged) {
1919
+ this.client = null;
1920
+ this.clientPromise = null;
1921
+ }
1922
+ if (envChanged) {
1923
+ this.sdk = null;
1924
+ this.sdkPromise = null;
1925
+ }
1882
1926
  this.topicId = topicId;
1883
1927
  this.endpoint = endpoint;
1884
1928
  this.retryTimes = retryTimes;
1885
1929
  this.source = source;
1930
+ this.userId = options.userId ?? this.userId;
1931
+ this.userName = options.userName ?? this.userName;
1886
1932
  this.projectId = options.projectId ?? this.projectId;
1887
1933
  this.projectName = options.projectName ?? this.projectName;
1888
1934
  this.appId = options.appId ?? this.appId;
1889
1935
  this.appVersion = options.appVersion ?? this.appVersion;
1890
- this.envType = options.envType ?? this.detectEnvType();
1936
+ this.envType = nextEnvType;
1937
+ // 可选:外部注入 SDK(优先级:sdkLoader > sdk)
1938
+ this.sdkLoaderOverride = options.sdkLoader ?? this.sdkLoaderOverride;
1939
+ this.sdkOverride = options.sdk ?? this.sdkOverride;
1891
1940
  this.userGenerateBaseFields = options.generateBaseFields ?? this.userGenerateBaseFields;
1892
1941
  this.autoGenerateBaseFields = createAutoDeviceInfoBaseFields(this.envType, options.deviceInfo);
1893
1942
  this.storageKey = options.storageKey ?? this.storageKey;
@@ -1897,8 +1946,10 @@ class ClsLogger {
1897
1946
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
1898
1947
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
1899
1948
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
1900
- // 预热 client
1901
- this.getInstance();
1949
+ // 预热(避免首条日志触发 import/初始化开销)
1950
+ void this.getInstance().catch(() => {
1951
+ // ignore
1952
+ });
1902
1953
  // 启动时尝试发送失败缓存
1903
1954
  this.flushFailed();
1904
1955
  // 初始化后立即启动请求监听
@@ -1923,7 +1974,7 @@ class ClsLogger {
1923
1974
  try {
1924
1975
  const userRaw = this.userGenerateBaseFields ? this.userGenerateBaseFields() : undefined;
1925
1976
  if (userRaw && isPlainObject(userRaw))
1926
- user = normalizeFlatFields(userRaw, 'generateBaseFields');
1977
+ user = normalizeFlatFields({ ...userRaw }, 'generateBaseFields');
1927
1978
  }
1928
1979
  catch {
1929
1980
  user = undefined;
@@ -2002,20 +2053,29 @@ class ClsLogger {
2002
2053
  this.behaviorMonitorCleanup = null;
2003
2054
  this.behaviorMonitorStarted = false;
2004
2055
  }
2005
- getInstance() {
2056
+ /**
2057
+ * 获取 CLS client(按环境懒加载 SDK)
2058
+ */
2059
+ async getInstance() {
2006
2060
  if (this.client)
2007
2061
  return this.client;
2008
- this.client = new AsyncClient({
2009
- endpoint: this.endpoint,
2010
- retry_times: this.retryTimes,
2062
+ if (this.clientPromise)
2063
+ return this.clientPromise;
2064
+ this.clientPromise = this.loadSdk()
2065
+ .then(({ AsyncClient }) => {
2066
+ const client = new AsyncClient({
2067
+ endpoint: this.endpoint,
2068
+ retry_times: this.retryTimes,
2069
+ });
2070
+ this.client = client;
2071
+ return client;
2072
+ })
2073
+ .catch((err) => {
2074
+ // 失败后允许下次重试
2075
+ this.clientPromise = null;
2076
+ throw err;
2011
2077
  });
2012
- return this.client;
2013
- }
2014
- detectEnvType() {
2015
- const wxAny = globalThis.wx;
2016
- if (wxAny && typeof wxAny.getSystemInfoSync === 'function')
2017
- return 'miniprogram';
2018
- return 'browser';
2078
+ return this.clientPromise;
2019
2079
  }
2020
2080
  /**
2021
2081
  * 直接上报:埋点入参必须是一维(扁平)Object
@@ -2041,22 +2101,33 @@ class ClsLogger {
2041
2101
  appVersion: this.appVersion || undefined,
2042
2102
  ...normalizedFields,
2043
2103
  });
2044
- const client = this.getInstance();
2045
- const logGroup = new LogGroup('127.0.0.1');
2104
+ // 同步 API:内部异步发送,避免把网络异常冒泡到业务(尤其小程序)
2105
+ void this.putAsync(finalFields).catch(() => {
2106
+ // ignore
2107
+ });
2108
+ }
2109
+ async putAsync(finalFields) {
2110
+ if (!this.topicId)
2111
+ return;
2112
+ const sdk = await this.loadSdk();
2113
+ const client = await this.getInstance();
2114
+ const logGroup = new sdk.LogGroup('127.0.0.1');
2046
2115
  logGroup.setSource(this.source);
2047
- const log = new Log(Date.now());
2116
+ const log = new sdk.Log(Date.now());
2048
2117
  for (const key of Object.keys(finalFields)) {
2049
2118
  log.addContent(key, stringifyLogValue(finalFields[key]));
2050
2119
  }
2051
2120
  logGroup.addLog(log);
2052
- const request = new PutLogsRequest(this.topicId, logGroup);
2121
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2053
2122
  const exit = enterClsSendingGuard();
2123
+ let p;
2054
2124
  try {
2055
- client.PutLogs(request);
2125
+ p = client.PutLogs(request);
2056
2126
  }
2057
2127
  finally {
2058
2128
  exit();
2059
2129
  }
2130
+ await p;
2060
2131
  }
2061
2132
  /**
2062
2133
  * 直接上报:把 data 序列化后放入指定 key(默认 “日志内容”)
@@ -2115,11 +2186,19 @@ class ClsLogger {
2115
2186
  console.warn('ClsLogger.putBatch:未初始化 topic_id');
2116
2187
  return;
2117
2188
  }
2118
- const client = this.getInstance();
2119
- const logGroup = new LogGroup('127.0.0.1');
2189
+ void this.putBatchAsync(queue).catch(() => {
2190
+ // ignore
2191
+ });
2192
+ }
2193
+ async putBatchAsync(queue) {
2194
+ if (!this.topicId)
2195
+ return;
2196
+ const sdk = await this.loadSdk();
2197
+ const client = await this.getInstance();
2198
+ const logGroup = new sdk.LogGroup('127.0.0.1');
2120
2199
  logGroup.setSource(this.source);
2121
2200
  for (const item of queue) {
2122
- const log = new Log(item.time);
2201
+ const log = new sdk.Log(item.time);
2123
2202
  const data = item.data ?? {};
2124
2203
  for (const key of Object.keys(data)) {
2125
2204
  log.addContent(key, stringifyLogValue(data[key]));
@@ -2128,14 +2207,16 @@ class ClsLogger {
2128
2207
  }
2129
2208
  if (logGroup.getLogs().length === 0)
2130
2209
  return;
2131
- const request = new PutLogsRequest(this.topicId, logGroup);
2210
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2132
2211
  const exit = enterClsSendingGuard();
2212
+ let p;
2133
2213
  try {
2134
- client.PutLogs(request);
2214
+ p = client.PutLogs(request);
2135
2215
  }
2136
2216
  finally {
2137
2217
  exit();
2138
2218
  }
2219
+ await p;
2139
2220
  }
2140
2221
  /**
2141
2222
  * 参考《一、概述》:统一上报入口(内存队列 + 批量发送)
@@ -2248,12 +2329,13 @@ class ClsLogger {
2248
2329
  async sendReportLogs(logs) {
2249
2330
  if (!this.topicId)
2250
2331
  return;
2251
- const client = this.getInstance();
2252
- const logGroup = new LogGroup('127.0.0.1');
2332
+ const sdk = await this.loadSdk();
2333
+ const client = await this.getInstance();
2334
+ const logGroup = new sdk.LogGroup('127.0.0.1');
2253
2335
  logGroup.setSource(this.source);
2254
2336
  for (const item of logs) {
2255
2337
  const fields = this.buildReportFields(item);
2256
- const log = new Log(fields.timestamp);
2338
+ const log = new sdk.Log(fields.timestamp);
2257
2339
  for (const key of Object.keys(fields)) {
2258
2340
  if (key === 'timestamp')
2259
2341
  continue;
@@ -2261,7 +2343,7 @@ class ClsLogger {
2261
2343
  }
2262
2344
  logGroup.addLog(log);
2263
2345
  }
2264
- const request = new PutLogsRequest(this.topicId, logGroup);
2346
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2265
2347
  // 只在“发起网络请求”的同步阶段打标记,避免 requestMonitor 监控 CLS 上报请求导致递归
2266
2348
  const exit = enterClsSendingGuard();
2267
2349
  let p;
@@ -2337,6 +2419,162 @@ class ClsLogger {
2337
2419
  }
2338
2420
  }
2339
2421
 
2422
+ function readGlobal(key) {
2423
+ try {
2424
+ const g = globalThis;
2425
+ return g[key] ?? null;
2426
+ }
2427
+ catch {
2428
+ return null;
2429
+ }
2430
+ }
2431
+ function tryRequire(moduleName) {
2432
+ try {
2433
+ // 说明:
2434
+ // - ESM 构建(exports.import/module)里通常不存在模块作用域的 require
2435
+ // - 一些小程序运行时/构建链路会把 require 挂到 globalThis 上
2436
+ // 因此这里同时探测“模块作用域 require”与 “globalThis.require”
2437
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
2438
+ const localReq = (typeof require === 'function' ? require : null);
2439
+ const globalReq = readGlobal('require');
2440
+ const candidates = [localReq, globalReq].filter((fn) => typeof fn === 'function');
2441
+ for (const fn of candidates) {
2442
+ try {
2443
+ return fn(moduleName);
2444
+ }
2445
+ catch {
2446
+ // continue
2447
+ }
2448
+ }
2449
+ return null;
2450
+ }
2451
+ catch {
2452
+ return null;
2453
+ }
2454
+ }
2455
+
2456
+ /**
2457
+ * 兼容版 ClsLogger(默认入口)
2458
+ * - 保留了自动识别环境 + 动态 import 的逻辑
2459
+ * - 如果业务想瘦身,建议改用 @be-link/cls-logger/web 或 /mini
2460
+ */
2461
+ class ClsLogger extends ClsLoggerCore {
2462
+ async loadSdk() {
2463
+ if (this.sdk)
2464
+ return this.sdk;
2465
+ if (this.sdkPromise)
2466
+ return this.sdkPromise;
2467
+ const normalizeSdk = (m) => {
2468
+ const mod = (m?.default && m.default.AsyncClient ? m.default : m);
2469
+ if (mod?.AsyncClient && mod?.Log && mod?.LogGroup && mod?.PutLogsRequest) {
2470
+ return {
2471
+ AsyncClient: mod.AsyncClient,
2472
+ Log: mod.Log,
2473
+ LogGroup: mod.LogGroup,
2474
+ PutLogsRequest: mod.PutLogsRequest,
2475
+ };
2476
+ }
2477
+ return null;
2478
+ };
2479
+ // 1) 外部注入的 loader(最高优先级)
2480
+ if (this.sdkLoaderOverride) {
2481
+ try {
2482
+ const loaded = await this.sdkLoaderOverride();
2483
+ const sdk = normalizeSdk(loaded);
2484
+ if (sdk) {
2485
+ this.sdk = sdk;
2486
+ return sdk;
2487
+ }
2488
+ }
2489
+ catch {
2490
+ // ignore and fallback
2491
+ }
2492
+ }
2493
+ // 2) 外部直接注入的 sdk
2494
+ if (this.sdkOverride) {
2495
+ const sdk = normalizeSdk(this.sdkOverride);
2496
+ if (sdk) {
2497
+ this.sdk = sdk;
2498
+ return sdk;
2499
+ }
2500
+ }
2501
+ const isMini = this.envType === 'miniprogram';
2502
+ // 3) 尝试 require / 全局变量
2503
+ // Mini Program: 尝试直接 require
2504
+ if (isMini) {
2505
+ // 显式使用字面量 require,帮助打包工具识别依赖(尤其是 CJS 构建模式下)
2506
+ try {
2507
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
2508
+ const req = typeof require === 'function' ? require('tencentcloud-cls-sdk-js-mini') : null;
2509
+ const reqSdk = normalizeSdk(req);
2510
+ if (reqSdk) {
2511
+ this.sdk = reqSdk;
2512
+ return reqSdk;
2513
+ }
2514
+ }
2515
+ catch {
2516
+ // ignore
2517
+ }
2518
+ // 尝试使用 tryRequire(作为补充)
2519
+ const reqMod = tryRequire('tencentcloud-cls-sdk-js-mini');
2520
+ const reqSdk = normalizeSdk(reqMod);
2521
+ if (reqSdk) {
2522
+ this.sdk = reqSdk;
2523
+ return reqSdk;
2524
+ }
2525
+ }
2526
+ else {
2527
+ // Web: 优先读全局变量 (UMD)
2528
+ const g = readGlobal('tencentcloudClsSdkJsWeb');
2529
+ const sdk = normalizeSdk(g);
2530
+ if (sdk) {
2531
+ this.sdk = sdk;
2532
+ return sdk;
2533
+ }
2534
+ // Web: 尝试 require
2535
+ const reqMod = tryRequire('tencentcloud-cls-sdk-js-web');
2536
+ const reqSdk = normalizeSdk(reqMod);
2537
+ if (reqSdk) {
2538
+ this.sdk = reqSdk;
2539
+ return reqSdk;
2540
+ }
2541
+ }
2542
+ // 4) 动态 import
2543
+ // 使用字符串字面量,确保 Bundler 能正确识别并进行 Code Splitting
2544
+ if (isMini) {
2545
+ this.sdkPromise = import('tencentcloud-cls-sdk-js-mini')
2546
+ .then((m) => {
2547
+ const sdk = normalizeSdk(m);
2548
+ if (!sdk)
2549
+ throw new Error(`ClsLogger.loadSdk: invalid sdk module for mini`);
2550
+ this.sdk = sdk;
2551
+ return sdk;
2552
+ })
2553
+ .catch((err) => {
2554
+ this.sdkPromise = null;
2555
+ console.error('[ClsLogger] Failed to load mini sdk:', err);
2556
+ throw err;
2557
+ });
2558
+ }
2559
+ else {
2560
+ this.sdkPromise = import('tencentcloud-cls-sdk-js-web')
2561
+ .then((m) => {
2562
+ const sdk = normalizeSdk(m);
2563
+ if (!sdk)
2564
+ throw new Error(`ClsLogger.loadSdk: invalid sdk module for web`);
2565
+ this.sdk = sdk;
2566
+ return sdk;
2567
+ })
2568
+ .catch((err) => {
2569
+ this.sdkPromise = null;
2570
+ console.error('[ClsLogger] Failed to load web sdk:', err);
2571
+ throw err;
2572
+ });
2573
+ }
2574
+ return this.sdkPromise;
2575
+ }
2576
+ }
2577
+
2340
2578
  const clsLogger = new ClsLogger();
2341
2579
 
2342
2580
  export { ClsLogger, clsLogger, clsLogger as default };