@softprobe/web-record 0.1.6 → 0.1.8

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.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,SAAS,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,wBAAwB,EACxB,SAAS,GACV,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC"}
package/dist/otlp.d.ts CHANGED
@@ -27,10 +27,31 @@ export type BuildRecordingSpanInput = {
27
27
  serviceName?: string;
28
28
  environment?: string;
29
29
  };
30
+ export type BuildClientRequestSpanInput = {
31
+ traceId: string;
32
+ spanId: string;
33
+ sessionId: string;
34
+ method: string;
35
+ scheme: string;
36
+ serverAddress: string;
37
+ path: string;
38
+ statusCode: number;
39
+ failed: boolean;
40
+ requestBody?: string;
41
+ responseBody?: string;
42
+ startTimeUnixNano: string;
43
+ endTimeUnixNano: string;
44
+ serviceName?: string;
45
+ environment?: string;
46
+ };
30
47
  /** Build an OTLP JSON ExportTraceServiceRequest for one recording flush. */
31
48
  export declare function buildRecordingExport(input: BuildRecordingSpanInput): {
32
49
  resourceSpans: unknown[];
33
50
  };
51
+ /** Build an OTLP client span for an explicitly selected browser API request. */
52
+ export declare function buildClientRequestExport(input: BuildClientRequestSpanInput): {
53
+ resourceSpans: unknown[];
54
+ };
34
55
  export declare function randomHexId(bytes: number): string;
35
56
  export declare function msToUnixNano(ms: number): string;
36
57
  export { SPAN_NAME, EVENT_NAME, EVENTS_ATTR };
@@ -1 +1 @@
1
- {"version":3,"file":"otlp.d.ts","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,QAAA,MAAM,SAAS,4BAA4B,CAAC;AAC5C,QAAA,MAAM,UAAU,uBAAuB,CAAC;AACxC,QAAA,MAAM,WAAW,wBAAwB,CAAC;AAE1C,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,WAAW,CAAC;AAEpD,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAyBF,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB,GAAG;IACpE,aAAa,EAAE,OAAO,EAAE,CAAC;CAC1B,CAmDA;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"otlp.d.ts","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,QAAA,MAAM,SAAS,4BAA4B,CAAC;AAC5C,QAAA,MAAM,UAAU,uBAAuB,CAAC;AACxC,QAAA,MAAM,WAAW,wBAAwB,CAAC;AAE1C,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,WAAW,CAAC;AAEpD,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAyBF,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB,GAAG;IACpE,aAAa,EAAE,OAAO,EAAE,CAAC;CAC1B,CAmDA;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,2BAA2B,GAAG;IAC5E,aAAa,EAAE,OAAO,EAAE,CAAC;CAC1B,CA8CA;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC"}
package/dist/otlp.js CHANGED
@@ -69,6 +69,54 @@ export function buildRecordingExport(input) {
69
69
  ],
70
70
  };
71
71
  }
72
+ /** Build an OTLP client span for an explicitly selected browser API request. */
73
+ export function buildClientRequestExport(input) {
74
+ const attrs = [
75
+ stringAttr("sp.session.id", input.sessionId),
76
+ stringAttr("http.request.method", input.method),
77
+ stringAttr("url.scheme", input.scheme),
78
+ stringAttr("server.address", input.serverAddress),
79
+ stringAttr("url.path", input.path),
80
+ intAttr("http.response.status_code", input.statusCode),
81
+ ...(input.requestBody === undefined
82
+ ? []
83
+ : [stringAttr("http.request.body.content", input.requestBody)]),
84
+ ...(input.responseBody === undefined
85
+ ? []
86
+ : [stringAttr("http.response.body.content", input.responseBody)]),
87
+ ];
88
+ const resourceAttrs = [];
89
+ if (input.serviceName)
90
+ resourceAttrs.push(stringAttr("service.name", input.serviceName));
91
+ if (input.environment) {
92
+ resourceAttrs.push(stringAttr("deployment.environment.name", input.environment));
93
+ }
94
+ return {
95
+ resourceSpans: [
96
+ {
97
+ resource: { attributes: resourceAttrs },
98
+ scopeSpans: [
99
+ {
100
+ scope: { name: "softprobe.web.record", version: "0.1.0" },
101
+ spans: [
102
+ {
103
+ traceId: input.traceId,
104
+ spanId: input.spanId,
105
+ name: `HTTP ${input.method}`,
106
+ kind: 3,
107
+ startTimeUnixNano: input.startTimeUnixNano,
108
+ endTimeUnixNano: input.endTimeUnixNano,
109
+ attributes: attrs,
110
+ events: [],
111
+ status: { code: input.failed ? 2 : 1 },
112
+ },
113
+ ],
114
+ },
115
+ ],
116
+ },
117
+ ],
118
+ };
119
+ }
72
120
  export function randomHexId(bytes) {
73
121
  const buf = new Uint8Array(bytes);
74
122
  crypto.getRandomValues(buf);
package/dist/otlp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"otlp.js","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"otlp.js","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"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"}
package/dist/sdk.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type eventWithTime } from "@rrweb/types";
2
- import type { RecordingHandle, Tags, WebRecordController, WebRecordInitOptions } from "./types.js";
2
+ import type { RecordingHandle, RecordingUploadStatus, Tags, WebRecordController, WebRecordInitOptions } from "./types.js";
3
3
  type Compressible = eventWithTime & {
4
4
  data?: unknown;
5
5
  isCompressed?: boolean;
@@ -9,12 +9,17 @@ export declare class RecordSdk implements WebRecordController {
9
9
  readonly enabled: boolean;
10
10
  private _sessionId;
11
11
  private readonly otlpEndpoint;
12
- private readonly publicKey;
12
+ private readonly apiKey;
13
+ private readonly apiOrigins;
14
+ private readonly sessionIdStorageKey;
13
15
  private readonly intervalMs;
14
16
  private readonly serviceName?;
15
17
  private readonly environment?;
16
18
  private readonly userId?;
17
19
  private readonly maskAllInputs;
20
+ private readonly maskTextSelector?;
21
+ private readonly blockSelector?;
22
+ private readonly onUploadStatus?;
18
23
  private readonly recordOptions;
19
24
  private readonly replacers;
20
25
  private tags;
@@ -26,11 +31,22 @@ export declare class RecordSdk implements WebRecordController {
26
31
  private systemInfo;
27
32
  private stopHandle;
28
33
  private saving;
34
+ private recordingFlush;
35
+ private readonly pendingClientSpanExports;
36
+ /** 连续导出失败次数;成功即归零。 */
37
+ private consecutiveFailures;
38
+ /** 退避到期时间戳(ms);在此之前定时器不再尝试导出。 */
39
+ private nextAttemptAt;
40
+ /** 熔断标志:连败到上限后置位,导出与采集一起停。 */
41
+ private exportDisabled;
29
42
  private fetchImpl;
30
43
  /** Serializes setSessionId drains so overlapping navigations cannot race. */
31
44
  private sessionSwitch;
45
+ private _uploadStatus;
46
+ private readonly onPageHide;
32
47
  private constructor();
33
48
  get sessionId(): string;
49
+ get uploadStatus(): RecordingUploadStatus;
34
50
  static init(options?: WebRecordInitOptions): WebRecordController;
35
51
  record(opts?: {
36
52
  tags?: Tags;
@@ -46,7 +62,28 @@ export declare class RecordSdk implements WebRecordController {
46
62
  */
47
63
  private drainFlush;
48
64
  compressEvent(event: Compressible): Compressible;
49
- flush(): Promise<boolean>;
65
+ flush(options?: {
66
+ keepalive?: boolean;
67
+ }): Promise<boolean>;
68
+ private flushRecordingBatch;
69
+ private flushPendingClientSpanExports;
70
+ private setUploadStatus;
71
+ private createRequestSpan;
72
+ private exportClientRequestSpan;
73
+ /**
74
+ * 导出失败后的退避与熔断。
75
+ *
76
+ * <p>现场实证(2026-08-25,甲方内网):到不了配置的 collector,原实现每 5 秒把同一批
77
+ * 事件原样重发,失败 429 次没停过 —— 控制台被刷爆,主线程每轮还白做一次
78
+ * `JSON.stringify` + 压缩,页面肉眼可见地卡。三条对策:
79
+ * <ul>
80
+ * <li>连败指数退避,不再固定间隔硬打;</li>
81
+ * <li>只在连败的<b>第一条</b>打日志,后续静默;</li>
82
+ * <li>连败到上限就停掉采集并清空缓冲,再打一条写明端点和处置办法。</li>
83
+ * </ul>
84
+ * 单次失败仍然保留缓冲(可能只是网络抖动),这一点不变。
85
+ */
86
+ private noteExportFailure;
50
87
  private collectSystemInfo;
51
88
  }
52
89
  export {};
package/dist/sdk.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAiB7D,OAAO,KAAK,EACV,eAAe,EAEf,IAAI,EACJ,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAMpB,KAAK,YAAY,GAAG,aAAa,GAAG;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,qBAAa,SAAU,YAAW,mBAAmB;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA0B;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAuD;IACzE,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAe;IAChC,6EAA6E;IAC7E,OAAO,CAAC,aAAa,CAAoC;IAEzD,OAAO;IAgCP,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,MAAM,CAAC,IAAI,CAAC,OAAO,GAAE,oBAAiD,GAAG,mBAAmB;IAsC5F,MAAM,CAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,IAAI,CAAA;KAAE,GAAG,eAAe,GAAG,IAAI;IA2CtD,IAAI,IAAI,IAAI;IAIZ,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,UAAQ,GAAG,IAAI;IAI3C,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAUhC,eAAe;IAQ7B;;;;OAIG;YACW,UAAU;IAcxB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY;IAkB1C,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;YA0DjB,iBAAiB;CAqDhC"}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"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"}
package/dist/sdk.js CHANGED
@@ -1,21 +1,31 @@
1
1
  import { EventType } from "@rrweb/types";
2
2
  import { pack, record as rrwebRecord } from "rrweb";
3
3
  import { asNonEmptyString, deriveOtlpEndpoint, resolveSoftprobeConfigFromObject, MissingSoftprobeCredentialsError, } from "@softprobe/tracing/config";
4
- import { initializeHttpInterceptor, updateHttpInterceptorSessionId, } from "./http-interceptor.js";
5
- import { buildRecordingExport, msToUnixNano, randomHexId, } from "./otlp.js";
4
+ import { initializeHttpInterceptor, } from "./http-interceptor.js";
5
+ import { buildClientRequestExport, buildRecordingExport, msToUnixNano, randomHexId, } from "./otlp.js";
6
6
  const MAX_EVENTS = 500;
7
+ /** 连续导出失败多少次后彻底放弃采集(见 {@link RecordSdk.noteExportFailure})。 */
8
+ const MAX_CONSECUTIVE_EXPORT_FAILURES = 5;
9
+ /** 退避上限。不可达时不应一直定期打点。 */
10
+ const BACKOFF_MAX_MS = 5 * 60_000;
7
11
  const COOKIE_NAME = "_sp_vid";
8
12
  const COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
13
+ const SESSION_ID_STORAGE_PREFIX = "_sp_session_id:";
9
14
  export class RecordSdk {
10
15
  enabled;
11
16
  _sessionId;
12
17
  otlpEndpoint;
13
- publicKey;
18
+ apiKey;
19
+ apiOrigins;
20
+ sessionIdStorageKey;
14
21
  intervalMs;
15
22
  serviceName;
16
23
  environment;
17
24
  userId;
18
25
  maskAllInputs;
26
+ maskTextSelector;
27
+ blockSelector;
28
+ onUploadStatus;
19
29
  recordOptions;
20
30
  replacers;
21
31
  tags;
@@ -27,19 +37,43 @@ export class RecordSdk {
27
37
  systemInfo = null;
28
38
  stopHandle = null;
29
39
  saving = false;
40
+ recordingFlush = null;
41
+ pendingClientSpanExports = new Set();
42
+ /** 连续导出失败次数;成功即归零。 */
43
+ consecutiveFailures = 0;
44
+ /** 退避到期时间戳(ms);在此之前定时器不再尝试导出。 */
45
+ nextAttemptAt = 0;
46
+ /** 熔断标志:连败到上限后置位,导出与采集一起停。 */
47
+ exportDisabled = false;
30
48
  fetchImpl;
31
49
  /** Serializes setSessionId drains so overlapping navigations cannot race. */
32
50
  sessionSwitch = Promise.resolve();
51
+ _uploadStatus = "idle";
52
+ onPageHide = () => {
53
+ if (!this.enabled || (this.events.length === 0 && !this.saving && this.pendingClientSpanExports.size === 0))
54
+ return;
55
+ this.setUploadStatus("incomplete");
56
+ // A keepalive request is best-effort. If an ordinary export is already in
57
+ // flight, do not duplicate its batch; report it as incomplete until it
58
+ // resolves or fails.
59
+ if (!this.saving)
60
+ void this.flush({ keepalive: true });
61
+ };
33
62
  constructor(options) {
34
63
  this.enabled = options.enabled;
35
- this.publicKey = options.publicKey ?? "";
64
+ this.apiKey = options.apiKey ?? options.publicKey ?? "";
65
+ this.apiOrigins = [...(options.apiOrigins ?? [])];
36
66
  this.otlpEndpoint = options.otlpEndpoint;
37
- this._sessionId = asNonEmptyString(options.sessionId) ?? uuid();
67
+ this.sessionIdStorageKey = `${SESSION_ID_STORAGE_PREFIX}${options.serviceName ?? "default"}`;
68
+ this._sessionId = getOrCreateSessionId(options.sessionId, this.sessionIdStorageKey);
38
69
  this.intervalMs = Math.max(options.interval ?? 5000, 5000);
39
70
  this.serviceName = options.serviceName;
40
71
  this.environment = options.environment;
41
72
  this.userId = options.userId;
42
73
  this.maskAllInputs = options.maskAllInputs !== false;
74
+ this.maskTextSelector = options.maskTextSelector;
75
+ this.blockSelector = options.blockSelector;
76
+ this.onUploadStatus = options.onUploadStatus;
43
77
  this.recordOptions = options.recordOptions ?? {};
44
78
  this.replacers = options.replacers ?? {};
45
79
  this.tags = { ...(options.tags ?? {}) };
@@ -53,7 +87,11 @@ export class RecordSdk {
53
87
  throw new Error("fetch is not available");
54
88
  });
55
89
  if (this.enabled) {
56
- initializeHttpInterceptor(() => this._sessionId);
90
+ initializeHttpInterceptor({
91
+ apiOrigins: this.apiOrigins,
92
+ httpBodyCapture: options.httpBodyCapture,
93
+ createRequestSpan: (method, url) => this.createRequestSpan(method, url),
94
+ });
57
95
  if (!options.manual) {
58
96
  this.record();
59
97
  }
@@ -62,11 +100,14 @@ export class RecordSdk {
62
100
  get sessionId() {
63
101
  return this._sessionId;
64
102
  }
103
+ get uploadStatus() {
104
+ return this._uploadStatus;
105
+ }
65
106
  static init(options = {}) {
66
107
  const softDisable = options.softDisable !== false;
67
108
  try {
68
109
  const resolved = resolveSoftprobeConfigFromObject({
69
- publicKey: options.publicKey,
110
+ publicKey: options.apiKey ?? options.publicKey,
70
111
  baseUrl: options.baseUrl,
71
112
  otlpEndpoint: options.otlpEndpoint,
72
113
  environment: options.environment,
@@ -101,13 +142,22 @@ export class RecordSdk {
101
142
  record(opts) {
102
143
  if (!this.enabled)
103
144
  return null;
145
+ // 熔断后不再重新武装。宿主(SPA)每次路由变化都会调 record(),不挡住的话
146
+ // 每导航一次就白起一轮 rrweb 采集,下一个 tick 再自己停掉。
147
+ if (this.exportDisabled)
148
+ return null;
104
149
  if (this.stopHandle)
105
150
  return this.stopHandle;
106
151
  if (opts?.tags)
107
152
  this.setTags(opts.tags);
153
+ if (typeof window !== "undefined") {
154
+ window.addEventListener("pagehide", this.onPageHide);
155
+ }
108
156
  const stopFn = rrwebRecord({
109
157
  maskAllInputs: this.maskAllInputs,
110
158
  ...this.recordOptions,
159
+ ...(this.maskTextSelector ? { maskTextSelector: this.maskTextSelector } : {}),
160
+ ...(this.blockSelector ? { blockSelector: this.blockSelector } : {}),
111
161
  emit: (raw) => {
112
162
  const event = raw;
113
163
  if (this.events.length >= MAX_EVENTS)
@@ -120,6 +170,14 @@ export class RecordSdk {
120
170
  },
121
171
  });
122
172
  const intervalId = setInterval(() => {
173
+ // 熔断后连采集一起停:导出不出去还继续录,只是白耗主线程和内存。
174
+ if (this.exportDisabled) {
175
+ this.stop();
176
+ return;
177
+ }
178
+ // 退避未到,这轮跳过。这里刻意不打日志——日志本身就是故障放大器。
179
+ if (Date.now() < this.nextAttemptAt)
180
+ return;
123
181
  void this.flush();
124
182
  if (this.events.length) {
125
183
  const last = this.events[this.events.length - 1];
@@ -133,6 +191,9 @@ export class RecordSdk {
133
191
  this.stopHandle = {
134
192
  stop: () => {
135
193
  clearInterval(intervalId);
194
+ if (typeof window !== "undefined") {
195
+ window.removeEventListener("pagehide", this.onPageHide);
196
+ }
136
197
  stopFn?.();
137
198
  void this.flush();
138
199
  this.stopHandle = null;
@@ -159,8 +220,13 @@ export class RecordSdk {
159
220
  async switchSessionId(next) {
160
221
  await this.drainFlush();
161
222
  this._sessionId = next;
162
- if (this.enabled) {
163
- updateHttpInterceptorSessionId(() => this._sessionId);
223
+ try {
224
+ if (typeof sessionStorage !== "undefined") {
225
+ sessionStorage.setItem(this.sessionIdStorageKey, next);
226
+ }
227
+ }
228
+ catch {
229
+ // Storage can be unavailable in restricted browser contexts.
164
230
  }
165
231
  }
166
232
  /**
@@ -202,10 +268,35 @@ export class RecordSdk {
202
268
  }
203
269
  return event;
204
270
  }
205
- async flush() {
206
- if (!this.enabled || this.events.length === 0 || this.saving)
271
+ async flush(options = {}) {
272
+ if (this.exportDisabled)
273
+ return false;
274
+ if (!this.enabled)
207
275
  return true;
276
+ let recordingResult = true;
277
+ if (this.saving) {
278
+ recordingResult = await (this.recordingFlush ?? Promise.resolve(true));
279
+ }
280
+ else if (this.events.length > 0) {
281
+ const recordingFlush = this.flushRecordingBatch(options);
282
+ this.recordingFlush = recordingFlush;
283
+ try {
284
+ recordingResult = await recordingFlush;
285
+ }
286
+ finally {
287
+ if (this.recordingFlush === recordingFlush)
288
+ this.recordingFlush = null;
289
+ }
290
+ }
291
+ const clientSpansResult = await this.flushPendingClientSpanExports();
292
+ return recordingResult && clientSpansResult;
293
+ }
294
+ async flushRecordingBatch(options) {
295
+ if (this.exportDisabled || !this.enabled || this.events.length === 0)
296
+ return !this.exportDisabled;
208
297
  this.saving = true;
298
+ if (!options.keepalive)
299
+ this.setUploadStatus("pending");
209
300
  // Splice out the batch BEFORE await so events recorded while export is in
210
301
  // flight stay in `this.events` and are not dropped on success.
211
302
  const batch = this.events.splice(0, this.events.length);
@@ -239,26 +330,160 @@ export class RecordSdk {
239
330
  method: "POST",
240
331
  headers: {
241
332
  "Content-Type": "application/json",
242
- Authorization: `Bearer ${this.publicKey}`,
333
+ Authorization: `Bearer ${this.apiKey}`,
243
334
  },
244
335
  body: JSON.stringify(body),
245
336
  redirect: "follow",
337
+ ...(options.keepalive ? { keepalive: true } : {}),
246
338
  });
247
339
  if (!res.ok) {
248
340
  throw new Error(`OTLP export failed: ${res.status} ${await res.text()}`);
249
341
  }
250
342
  this.batchIndex = batchIndex + 1;
343
+ this.consecutiveFailures = 0;
344
+ this.nextAttemptAt = 0;
345
+ this.setUploadStatus("uploaded");
251
346
  return true;
252
347
  }
253
348
  catch (error) {
254
- console.error("[@softprobe/web-record] Failed to flush recording:", error);
255
349
  this.events.unshift(...batch);
350
+ this.setUploadStatus("failed");
351
+ this.noteExportFailure(error);
256
352
  return false;
257
353
  }
258
354
  finally {
259
355
  this.saving = false;
260
356
  }
261
357
  }
358
+ async flushPendingClientSpanExports() {
359
+ let succeeded = true;
360
+ while (this.pendingClientSpanExports.size > 0) {
361
+ const batch = [...this.pendingClientSpanExports];
362
+ const results = await Promise.all(batch);
363
+ succeeded = results.every(Boolean) && succeeded;
364
+ }
365
+ return succeeded;
366
+ }
367
+ setUploadStatus(status) {
368
+ this._uploadStatus = status;
369
+ try {
370
+ this.onUploadStatus?.(status);
371
+ }
372
+ catch {
373
+ // Application status rendering must not interrupt recording or export.
374
+ }
375
+ }
376
+ createRequestSpan(method, rawUrl) {
377
+ let url;
378
+ try {
379
+ url = new URL(rawUrl, typeof location !== "undefined" ? location.href : undefined);
380
+ const endpoint = new URL(this.otlpEndpoint);
381
+ // Exclude only the collector endpoint itself. An application may use a
382
+ // same-origin gateway for its own API and telemetry ingestion.
383
+ if (url.origin === endpoint.origin && url.pathname === endpoint.pathname)
384
+ return null;
385
+ }
386
+ catch {
387
+ return null;
388
+ }
389
+ const traceId = randomHexId(16);
390
+ const spanId = randomHexId(8);
391
+ const sessionId = this._sessionId;
392
+ const startMs = Date.now();
393
+ let finished = false;
394
+ let resolveCompletion;
395
+ const completion = new Promise((resolve) => { resolveCompletion = resolve; });
396
+ this.pendingClientSpanExports.add(completion);
397
+ void completion.finally(() => this.pendingClientSpanExports.delete(completion));
398
+ return {
399
+ traceparent: `00-${traceId}-${spanId}-01`,
400
+ finish: (statusCode, error, bodies) => {
401
+ if (finished)
402
+ return;
403
+ finished = true;
404
+ const endMs = Date.now();
405
+ try {
406
+ const body = buildClientRequestExport({
407
+ traceId,
408
+ spanId,
409
+ sessionId,
410
+ method,
411
+ scheme: url.protocol.slice(0, -1),
412
+ serverAddress: url.hostname,
413
+ path: url.pathname,
414
+ statusCode,
415
+ failed: Boolean(error) || statusCode >= 500,
416
+ requestBody: bodies?.requestBody,
417
+ responseBody: bodies?.responseBody,
418
+ startTimeUnixNano: msToUnixNano(startMs),
419
+ endTimeUnixNano: msToUnixNano(endMs),
420
+ serviceName: this.serviceName,
421
+ environment: this.environment,
422
+ });
423
+ void this.exportClientRequestSpan(body).then(resolveCompletion, () => resolveCompletion(false));
424
+ }
425
+ catch {
426
+ resolveCompletion(false);
427
+ }
428
+ },
429
+ };
430
+ }
431
+ async exportClientRequestSpan(body) {
432
+ this.setUploadStatus("pending");
433
+ try {
434
+ const response = await this.fetchImpl(this.otlpEndpoint, {
435
+ method: "POST",
436
+ headers: {
437
+ "Content-Type": "application/json",
438
+ Authorization: `Bearer ${this.apiKey}`,
439
+ },
440
+ body: JSON.stringify(body),
441
+ redirect: "follow",
442
+ // Client spans close with the application request and are often
443
+ // exported immediately before a full-page navigation.
444
+ keepalive: true,
445
+ });
446
+ if (!response.ok)
447
+ throw new Error(`OTLP export failed: ${response.status}`);
448
+ this.setUploadStatus("uploaded");
449
+ return true;
450
+ }
451
+ catch {
452
+ this.setUploadStatus("failed");
453
+ return false;
454
+ }
455
+ }
456
+ /**
457
+ * 导出失败后的退避与熔断。
458
+ *
459
+ * <p>现场实证(2026-08-25,甲方内网):到不了配置的 collector,原实现每 5 秒把同一批
460
+ * 事件原样重发,失败 429 次没停过 —— 控制台被刷爆,主线程每轮还白做一次
461
+ * `JSON.stringify` + 压缩,页面肉眼可见地卡。三条对策:
462
+ * <ul>
463
+ * <li>连败指数退避,不再固定间隔硬打;</li>
464
+ * <li>只在连败的<b>第一条</b>打日志,后续静默;</li>
465
+ * <li>连败到上限就停掉采集并清空缓冲,再打一条写明端点和处置办法。</li>
466
+ * </ul>
467
+ * 单次失败仍然保留缓冲(可能只是网络抖动),这一点不变。
468
+ */
469
+ noteExportFailure(error) {
470
+ this.consecutiveFailures += 1;
471
+ if (this.consecutiveFailures === 1) {
472
+ console.error("[@softprobe/web-record] Failed to flush recording:", error);
473
+ }
474
+ if (this.consecutiveFailures >= MAX_CONSECUTIVE_EXPORT_FAILURES) {
475
+ this.exportDisabled = true;
476
+ this.events.length = 0;
477
+ console.error(`[@softprobe/web-record] Giving up after ${this.consecutiveFailures} ` +
478
+ `consecutive export failures; session recording disabled. ` +
479
+ `Endpoint: ${this.otlpEndpoint}. On a closed network, point ` +
480
+ `SOFTPROBE_BASE_URL at a reachable collector or disable web recording.`);
481
+ return;
482
+ }
483
+ this.nextAttemptAt =
484
+ Date.now() +
485
+ Math.min(BACKOFF_MAX_MS, this.intervalMs * 2 ** (this.consecutiveFailures - 1));
486
+ }
262
487
  async collectSystemInfo() {
263
488
  let ua = "";
264
489
  let os = "Unknown";
@@ -314,6 +539,27 @@ function uuid() {
314
539
  (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 4)))).toString(16);
315
540
  });
316
541
  }
542
+ function getOrCreateSessionId(candidate, storageKey) {
543
+ const provided = asNonEmptyString(candidate);
544
+ try {
545
+ if (typeof sessionStorage !== "undefined") {
546
+ if (provided) {
547
+ sessionStorage.setItem(storageKey, provided);
548
+ return provided;
549
+ }
550
+ const stored = asNonEmptyString(sessionStorage.getItem(storageKey));
551
+ if (stored)
552
+ return stored;
553
+ const generated = uuid();
554
+ sessionStorage.setItem(storageKey, generated);
555
+ return generated;
556
+ }
557
+ }
558
+ catch {
559
+ // sessionStorage may throw for blocked or opaque origins; use an in-memory ID.
560
+ }
561
+ return provided ?? uuid();
562
+ }
317
563
  function getOrCreateVisitorId() {
318
564
  if (typeof document === "undefined")
319
565
  return uuid();