@be-link/cls-logger 1.0.1-beta.1 → 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]';
@@ -540,6 +540,8 @@
540
540
  }
541
541
 
542
542
  const DEFAULT_MAX_TEXT = 4000;
543
+ const DEFAULT_DEDUPE_WINDOW_MS = 3000;
544
+ const DEFAULT_DEDUPE_MAX_KEYS = 200;
543
545
  function truncate$2(s, maxLen) {
544
546
  if (!s)
545
547
  return s;
@@ -565,7 +567,17 @@
565
567
  function normalizeErrorLike(err, maxTextLength) {
566
568
  if (err && typeof err === 'object') {
567
569
  const anyErr = err;
568
- 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);
569
581
  const name = truncate$2(String(anyErr.name ?? ''), 200);
570
582
  const stack = truncate$2(String(anyErr.stack ?? ''), maxTextLength);
571
583
  return { message, name, stack };
@@ -573,6 +585,54 @@
573
585
  const message = truncate$2(stringifyLogValue(err), maxTextLength);
574
586
  return { message, name: '', stack: '' };
575
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
+ }
576
636
  function installBrowserErrorMonitor(report, options) {
577
637
  if (typeof window === 'undefined')
578
638
  return;
@@ -580,6 +640,7 @@
580
640
  if (w.__beLinkClsLoggerErrorInstalled__)
581
641
  return;
582
642
  w.__beLinkClsLoggerErrorInstalled__ = true;
643
+ const shouldReport = createDedupeGuard(options);
583
644
  window.addEventListener('error', (event) => {
584
645
  try {
585
646
  if (!sampleHit$1(options.sampleRate))
@@ -602,6 +663,8 @@
602
663
  if (e.stack)
603
664
  payload.stack = e.stack;
604
665
  }
666
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
667
+ return;
605
668
  report(options.reportType, payload);
606
669
  return;
607
670
  }
@@ -613,6 +676,8 @@
613
676
  payload.source = 'resource.error';
614
677
  payload.tagName = tagName;
615
678
  payload.resourceUrl = truncate$2(url, 2000);
679
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
680
+ return;
616
681
  report(options.reportType, payload);
617
682
  }
618
683
  }
@@ -633,6 +698,8 @@
633
698
  errorName: e.name,
634
699
  stack: e.stack,
635
700
  };
701
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
702
+ return;
636
703
  report(options.reportType, payload);
637
704
  }
638
705
  catch {
@@ -645,6 +712,7 @@
645
712
  if (g.__beLinkClsLoggerMpErrorInstalled__)
646
713
  return;
647
714
  g.__beLinkClsLoggerMpErrorInstalled__ = true;
715
+ const shouldReport = createDedupeGuard(options);
648
716
  const wxAny = globalThis.wx;
649
717
  // wx.* 事件(兼容在 App 已经创建后的场景)
650
718
  try {
@@ -653,10 +721,16 @@
653
721
  try {
654
722
  if (!sampleHit$1(options.sampleRate))
655
723
  return;
656
- report(options.reportType, {
724
+ const e = normalizeErrorLike(msg, options.maxTextLength);
725
+ const payload = {
657
726
  source: 'wx.onError',
658
- message: truncate$2(String(msg ?? ''), options.maxTextLength),
659
- });
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);
660
734
  }
661
735
  catch {
662
736
  // ignore
@@ -674,12 +748,15 @@
674
748
  if (!sampleHit$1(options.sampleRate))
675
749
  return;
676
750
  const e = normalizeErrorLike(res?.reason, options.maxTextLength);
677
- report(options.reportType, {
751
+ const payload = {
678
752
  source: 'wx.onUnhandledRejection',
679
753
  message: e.message,
680
754
  errorName: e.name,
681
755
  stack: e.stack,
682
- });
756
+ };
757
+ if (!shouldReport(buildErrorKey(options.reportType, payload)))
758
+ return;
759
+ report(options.reportType, payload);
683
760
  }
684
761
  catch {
685
762
  // ignore
@@ -701,10 +778,15 @@
701
778
  next.onError = function (...args) {
702
779
  try {
703
780
  if (sampleHit$1(options.sampleRate)) {
704
- report(options.reportType, {
781
+ const e = normalizeErrorLike(args?.[0], options.maxTextLength);
782
+ const payload = {
705
783
  source: 'App.onError',
706
- message: truncate$2(String(args?.[0] ?? ''), options.maxTextLength),
707
- });
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);
708
790
  }
709
791
  }
710
792
  catch {
@@ -720,12 +802,14 @@
720
802
  if (sampleHit$1(options.sampleRate)) {
721
803
  const reason = args?.[0]?.reason ?? args?.[0];
722
804
  const e = normalizeErrorLike(reason, options.maxTextLength);
723
- report(options.reportType, {
805
+ const payload = {
724
806
  source: 'App.onUnhandledRejection',
725
807
  message: e.message,
726
808
  errorName: e.name,
727
809
  stack: e.stack,
728
- });
810
+ };
811
+ if (shouldReport(buildErrorKey(options.reportType, payload)))
812
+ report(options.reportType, payload);
729
813
  }
730
814
  }
731
815
  catch {
@@ -754,6 +838,8 @@
754
838
  sampleRate: raw.sampleRate ?? 1,
755
839
  captureResourceError: raw.captureResourceError ?? true,
756
840
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT,
841
+ dedupeWindowMs: raw.dedupeWindowMs ?? DEFAULT_DEDUPE_WINDOW_MS,
842
+ dedupeMaxKeys: raw.dedupeMaxKeys ?? DEFAULT_DEDUPE_MAX_KEYS,
757
843
  };
758
844
  if (isMiniProgramEnv()) {
759
845
  installMiniProgramErrorMonitor(report, options);
@@ -1004,140 +1090,58 @@
1004
1090
  }
1005
1091
  }
1006
1092
  }
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
1093
  function installMiniProgramPerformanceMonitor(report, options) {
1094
1094
  const g = globalThis;
1095
+ const ctx = g.wx || g.Taro;
1096
+ if (!ctx || typeof ctx.getPerformance !== 'function')
1097
+ return;
1095
1098
  if (g.__beLinkClsLoggerMpPerfInstalled__)
1096
1099
  return;
1097
1100
  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
1101
  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
- : [];
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') {
1130
1113
  report(options.reportType, {
1131
- metric: 'mp-performance',
1132
- entries: truncate$1(stringifyLogValue(entries), options.maxTextLength),
1114
+ metric: 'page-render',
1115
+ duration: entry.duration,
1116
+ pagePath: entry.path || '',
1117
+ unit: 'ms',
1133
1118
  });
1134
1119
  }
1135
- catch {
1136
- // 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
+ });
1128
+ }
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
1137
  }
1138
- }, 0);
1138
+ }
1139
1139
  }
1140
- }
1140
+ catch {
1141
+ // ignore
1142
+ }
1143
+ });
1144
+ observer.observe({ entryTypes: ['navigation', 'render'] });
1141
1145
  }
1142
1146
  catch {
1143
1147
  // ignore
@@ -1461,7 +1465,7 @@
1461
1465
  const tag = (el.tagName || '').toLowerCase();
1462
1466
  const trackId = getAttr(el, clickTrackIdAttr);
1463
1467
  // 过滤无效点击:白名单 tag + 没有 trackId
1464
- if (clickWhiteList.includes(tag) && !trackId)
1468
+ if (clickWhiteList.includes(tag) || !trackId)
1465
1469
  return;
1466
1470
  void uvStatePromise.then(({ uvId, meta }) => {
1467
1471
  if (destroyed)
@@ -1502,8 +1506,12 @@
1502
1506
  g.Page = function patchedPage(conf) {
1503
1507
  const originalOnShow = conf?.onShow;
1504
1508
  conf.onShow = function (...args) {
1505
- if (pvEnabled)
1506
- reportPv(getPagePath());
1509
+ if (pvEnabled) {
1510
+ const pagePath = getPagePath();
1511
+ if (pagePath?.length > 0) {
1512
+ reportPv(pagePath);
1513
+ }
1514
+ }
1507
1515
  return typeof originalOnShow === 'function' ? originalOnShow.apply(this, args) : undefined;
1508
1516
  };
1509
1517
  // 点击:wrap 页面 methods(bindtap 等会调用到这里的 handler)
@@ -1839,9 +1847,20 @@
1839
1847
  g.__beLinkClsLoggerSendingCount__ = cur > 0 ? cur - 1 : 0;
1840
1848
  };
1841
1849
  }
1842
- class ClsLogger {
1850
+ /**
1851
+ * CLS Logger 核心基类
1852
+ * - 负责所有上报、队列、监控逻辑
1853
+ * - 不包含具体的 SDK 加载实现(由子类负责)
1854
+ * - 这样可以把 web/mini 的 SDK 依赖彻底解耦到子入口
1855
+ */
1856
+ class ClsLoggerCore {
1843
1857
  constructor() {
1858
+ this.sdk = null;
1859
+ this.sdkPromise = null;
1860
+ this.sdkOverride = null;
1861
+ this.sdkLoaderOverride = null;
1844
1862
  this.client = null;
1863
+ this.clientPromise = null;
1845
1864
  this.topicId = null;
1846
1865
  this.endpoint = 'ap-shanghai.cls.tencentcs.com';
1847
1866
  this.retryTimes = 10;
@@ -1872,6 +1891,20 @@
1872
1891
  this.behaviorMonitorStarted = false;
1873
1892
  this.behaviorMonitorCleanup = null;
1874
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
+ }
1875
1908
  init(options) {
1876
1909
  this.initTs = Date.now();
1877
1910
  const topicId = options?.tencentCloud?.topicID ?? options?.topic_id ?? options?.topicID ?? this.topicId ?? null;
@@ -1883,15 +1916,33 @@
1883
1916
  console.warn('ClsLogger.init 没有传 topicID/topic_id');
1884
1917
  return;
1885
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
+ }
1886
1932
  this.topicId = topicId;
1887
1933
  this.endpoint = endpoint;
1888
1934
  this.retryTimes = retryTimes;
1889
1935
  this.source = source;
1936
+ this.userId = options.userId ?? this.userId;
1937
+ this.userName = options.userName ?? this.userName;
1890
1938
  this.projectId = options.projectId ?? this.projectId;
1891
1939
  this.projectName = options.projectName ?? this.projectName;
1892
1940
  this.appId = options.appId ?? this.appId;
1893
1941
  this.appVersion = options.appVersion ?? this.appVersion;
1894
- 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;
1895
1946
  this.userGenerateBaseFields = options.generateBaseFields ?? this.userGenerateBaseFields;
1896
1947
  this.autoGenerateBaseFields = createAutoDeviceInfoBaseFields(this.envType, options.deviceInfo);
1897
1948
  this.storageKey = options.storageKey ?? this.storageKey;
@@ -1901,8 +1952,10 @@
1901
1952
  this.startupDelayMs = options.batch?.startupDelayMs ?? this.startupDelayMs;
1902
1953
  this.failedCacheKey = options.failedCacheKey ?? this.failedCacheKey;
1903
1954
  this.failedCacheMax = options.failedCacheMax ?? this.failedCacheMax;
1904
- // 预热 client
1905
- this.getInstance();
1955
+ // 预热(避免首条日志触发 import/初始化开销)
1956
+ void this.getInstance().catch(() => {
1957
+ // ignore
1958
+ });
1906
1959
  // 启动时尝试发送失败缓存
1907
1960
  this.flushFailed();
1908
1961
  // 初始化后立即启动请求监听
@@ -1927,7 +1980,7 @@
1927
1980
  try {
1928
1981
  const userRaw = this.userGenerateBaseFields ? this.userGenerateBaseFields() : undefined;
1929
1982
  if (userRaw && isPlainObject(userRaw))
1930
- user = normalizeFlatFields(userRaw, 'generateBaseFields');
1983
+ user = normalizeFlatFields({ ...userRaw }, 'generateBaseFields');
1931
1984
  }
1932
1985
  catch {
1933
1986
  user = undefined;
@@ -2006,20 +2059,29 @@
2006
2059
  this.behaviorMonitorCleanup = null;
2007
2060
  this.behaviorMonitorStarted = false;
2008
2061
  }
2009
- getInstance() {
2062
+ /**
2063
+ * 获取 CLS client(按环境懒加载 SDK)
2064
+ */
2065
+ async getInstance() {
2010
2066
  if (this.client)
2011
2067
  return this.client;
2012
- this.client = new tencentcloudClsSdkJsWeb.AsyncClient({
2013
- endpoint: this.endpoint,
2014
- 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;
2015
2083
  });
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';
2084
+ return this.clientPromise;
2023
2085
  }
2024
2086
  /**
2025
2087
  * 直接上报:埋点入参必须是一维(扁平)Object
@@ -2045,22 +2107,33 @@
2045
2107
  appVersion: this.appVersion || undefined,
2046
2108
  ...normalizedFields,
2047
2109
  });
2048
- const client = this.getInstance();
2049
- 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');
2050
2121
  logGroup.setSource(this.source);
2051
- const log = new tencentcloudClsSdkJsWeb.Log(Date.now());
2122
+ const log = new sdk.Log(Date.now());
2052
2123
  for (const key of Object.keys(finalFields)) {
2053
2124
  log.addContent(key, stringifyLogValue(finalFields[key]));
2054
2125
  }
2055
2126
  logGroup.addLog(log);
2056
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2127
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2057
2128
  const exit = enterClsSendingGuard();
2129
+ let p;
2058
2130
  try {
2059
- client.PutLogs(request);
2131
+ p = client.PutLogs(request);
2060
2132
  }
2061
2133
  finally {
2062
2134
  exit();
2063
2135
  }
2136
+ await p;
2064
2137
  }
2065
2138
  /**
2066
2139
  * 直接上报:把 data 序列化后放入指定 key(默认 “日志内容”)
@@ -2119,11 +2192,19 @@
2119
2192
  console.warn('ClsLogger.putBatch:未初始化 topic_id');
2120
2193
  return;
2121
2194
  }
2122
- const client = this.getInstance();
2123
- 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');
2124
2205
  logGroup.setSource(this.source);
2125
2206
  for (const item of queue) {
2126
- const log = new tencentcloudClsSdkJsWeb.Log(item.time);
2207
+ const log = new sdk.Log(item.time);
2127
2208
  const data = item.data ?? {};
2128
2209
  for (const key of Object.keys(data)) {
2129
2210
  log.addContent(key, stringifyLogValue(data[key]));
@@ -2132,14 +2213,16 @@
2132
2213
  }
2133
2214
  if (logGroup.getLogs().length === 0)
2134
2215
  return;
2135
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2216
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2136
2217
  const exit = enterClsSendingGuard();
2218
+ let p;
2137
2219
  try {
2138
- client.PutLogs(request);
2220
+ p = client.PutLogs(request);
2139
2221
  }
2140
2222
  finally {
2141
2223
  exit();
2142
2224
  }
2225
+ await p;
2143
2226
  }
2144
2227
  /**
2145
2228
  * 参考《一、概述》:统一上报入口(内存队列 + 批量发送)
@@ -2252,12 +2335,13 @@
2252
2335
  async sendReportLogs(logs) {
2253
2336
  if (!this.topicId)
2254
2337
  return;
2255
- const client = this.getInstance();
2256
- 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');
2257
2341
  logGroup.setSource(this.source);
2258
2342
  for (const item of logs) {
2259
2343
  const fields = this.buildReportFields(item);
2260
- const log = new tencentcloudClsSdkJsWeb.Log(fields.timestamp);
2344
+ const log = new sdk.Log(fields.timestamp);
2261
2345
  for (const key of Object.keys(fields)) {
2262
2346
  if (key === 'timestamp')
2263
2347
  continue;
@@ -2265,7 +2349,7 @@
2265
2349
  }
2266
2350
  logGroup.addLog(log);
2267
2351
  }
2268
- const request = new tencentcloudClsSdkJsWeb.PutLogsRequest(this.topicId, logGroup);
2352
+ const request = new sdk.PutLogsRequest(this.topicId, logGroup);
2269
2353
  // 只在“发起网络请求”的同步阶段打标记,避免 requestMonitor 监控 CLS 上报请求导致递归
2270
2354
  const exit = enterClsSendingGuard();
2271
2355
  let p;
@@ -2341,6 +2425,146 @@
2341
2425
  }
2342
2426
  }
2343
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
+
2344
2568
  const clsLogger = new ClsLogger();
2345
2569
 
2346
2570
  exports.ClsLogger = ClsLogger;