@obtrace/browser 2.0.1 → 2.2.0

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/README.md CHANGED
@@ -14,17 +14,19 @@ bun add @obtrace/sdk-browser
14
14
 
15
15
  ### Simplified setup
16
16
 
17
- The API key resolves `tenant_id`, `project_id`, `app_id`, and `env` automatically on the server side, so only three fields are needed:
17
+ The API key resolves `tenant_id`, `project_id`, `app_id`, and `env` automatically on the server side. `serviceName` must match the connected app name in the project, or an explicit alias configured for that app.
18
18
 
19
19
  ```ts
20
20
  import { initBrowserSDK } from "@obtrace/sdk-browser/browser";
21
21
 
22
22
  const sdk = initBrowserSDK({
23
23
  apiKey: "obt_live_...",
24
- serviceName: "web-app",
24
+ serviceName: "core",
25
25
  });
26
26
  ```
27
27
 
28
+ If the project app is `core` and the dashboard defines `web` as an alias, `serviceName: "web"` is accepted and normalized to `core` on ingest. Arbitrary names are rejected.
29
+
28
30
  ### Full configuration
29
31
 
30
32
  For advanced use cases you can override the resolved values explicitly:
@@ -34,10 +36,10 @@ import { initBrowserSDK, SemanticMetrics } from "@obtrace/sdk-browser/browser";
34
36
 
35
37
  const sdk = initBrowserSDK({
36
38
  apiKey: "<API_KEY>",
37
- serviceName: "web-app",
39
+ serviceName: "core",
38
40
  tenantId: "tenant-prod",
39
41
  projectId: "project-prod",
40
- appId: "web",
42
+ appId: "core",
41
43
  env: "prod"
42
44
  });
43
45
 
@@ -1,3 +1,4 @@
1
+ import { SpanStatusCode } from "@opentelemetry/api";
1
2
  import { addBreadcrumb } from "./breadcrumbs";
2
3
  const LEVEL_MAP = {
3
4
  debug: "debug",
@@ -20,32 +21,72 @@ export function installConsoleCapture(tracer, sessionId) {
20
21
  const level = LEVEL_MAP[method];
21
22
  console[method] = (...args) => {
22
23
  original(...args);
23
- let message;
24
- let attrs = {};
25
- if (args.length === 1 && typeof args[0] === "object" && args[0] !== null && !Array.isArray(args[0])) {
26
- const obj = args[0];
27
- message = String(obj.msg || obj.message || JSON.stringify(obj));
28
- for (const [k, v] of Object.entries(obj)) {
29
- if (k === "msg" || k === "message")
30
- continue;
31
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
32
- attrs[k] = v;
24
+ try {
25
+ let message;
26
+ let attrs = {};
27
+ const safeStringify = (v) => {
28
+ try {
29
+ return JSON.stringify(v);
30
+ }
31
+ catch {
32
+ return String(v);
33
+ }
34
+ };
35
+ const firstArg = args[0];
36
+ const isErrorObj = firstArg instanceof Error;
37
+ if (isErrorObj) {
38
+ const err = firstArg;
39
+ message = `${err.name}: ${err.message}`;
40
+ if (err.stack)
41
+ attrs["error.stack"] = err.stack.slice(0, 4096);
42
+ attrs["error.type"] = err.name;
43
+ for (let i = 1; i < args.length; i++) {
44
+ const extra = args[i];
45
+ if (typeof extra === "string" && extra.includes("\n at ")) {
46
+ attrs["error.component_stack"] = extra.slice(0, 4096);
47
+ }
33
48
  }
34
49
  }
50
+ else if (args.length === 1 && typeof firstArg === "object" && firstArg !== null && !Array.isArray(firstArg)) {
51
+ const obj = firstArg;
52
+ message = String(obj.msg || obj.message || safeStringify(obj));
53
+ for (const [k, v] of Object.entries(obj)) {
54
+ if (k === "msg" || k === "message")
55
+ continue;
56
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
57
+ attrs[k] = v;
58
+ }
59
+ }
60
+ }
61
+ else {
62
+ message = args.map(a => typeof a === "string" ? a : safeStringify(a)).join(" ");
63
+ if (method === "error") {
64
+ for (const a of args) {
65
+ if (typeof a === "string" && a.includes("\n at ")) {
66
+ attrs["error.stack"] = a.slice(0, 4096);
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ addBreadcrumb({ timestamp: Date.now(), category: `console.${method}`, message, level, data: attrs });
73
+ const spanName = (method === "error" && (isErrorObj || attrs["error.stack"])) ? "browser.error" : "browser.console";
74
+ const span = tracer.startSpan(spanName, {
75
+ attributes: {
76
+ "log.severity": level.toUpperCase(),
77
+ "log.message": message.slice(0, 1024),
78
+ "session.id": sessionId,
79
+ ...attrs,
80
+ },
81
+ });
82
+ if (method === "error") {
83
+ span.setStatus({ code: SpanStatusCode.ERROR, message: message.slice(0, 1024) });
84
+ if (isErrorObj)
85
+ span.recordException(firstArg);
86
+ }
87
+ span.end();
35
88
  }
36
- else {
37
- message = args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
38
- }
39
- addBreadcrumb({ timestamp: Date.now(), category: `console.${method}`, message, level, data: attrs });
40
- const span = tracer.startSpan("browser.console", {
41
- attributes: {
42
- "log.severity": level.toUpperCase(),
43
- "log.message": message.slice(0, 1024),
44
- "session.id": sessionId,
45
- ...attrs,
46
- },
47
- });
48
- span.end();
89
+ catch { }
49
90
  };
50
91
  }
51
92
  return () => {
@@ -5,42 +5,62 @@ export function installBrowserErrorHooks(tracer, sessionId) {
5
5
  return () => undefined;
6
6
  }
7
7
  const onError = (ev) => {
8
- addBreadcrumb({ timestamp: Date.now(), category: "error", message: ev.message || "window.error", level: "error" });
9
- const breadcrumbs = getBreadcrumbs();
10
- const span = tracer.startSpan("browser.error", {
11
- attributes: {
12
- "error.message": ev.message || "window.error",
13
- "error.file": ev.filename || "",
14
- "error.line": ev.lineno || 0,
15
- "error.column": ev.colno || 0,
16
- "breadcrumbs.count": breadcrumbs.length,
17
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
18
- ...(sessionId ? { "session.id": sessionId } : {}),
19
- },
20
- });
21
- span.setStatus({ code: SpanStatusCode.ERROR, message: ev.message });
22
- if (ev.error instanceof Error) {
23
- span.recordException(ev.error);
8
+ const message = ev.message || "window.error";
9
+ addBreadcrumb({ timestamp: Date.now(), category: "error", message, level: "error" });
10
+ try {
11
+ const breadcrumbs = getBreadcrumbs();
12
+ const stack = ev.error instanceof Error ? ev.error.stack || "" : "";
13
+ const span = tracer.startSpan("browser.error", {
14
+ attributes: {
15
+ "error.message": message,
16
+ "error.file": ev.filename || "",
17
+ "error.line": ev.lineno || 0,
18
+ "error.column": ev.colno || 0,
19
+ "error.stack": stack.slice(0, 4096),
20
+ "error.type": ev.error?.constructor?.name || "Error",
21
+ "breadcrumbs.count": breadcrumbs.length,
22
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
23
+ ...(sessionId ? { "session.id": sessionId } : {}),
24
+ },
25
+ });
26
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
27
+ if (ev.error instanceof Error) {
28
+ span.recordException(ev.error);
29
+ }
30
+ span.end();
24
31
  }
25
- span.end();
32
+ catch { }
26
33
  };
27
34
  const onRejection = (ev) => {
28
- const reason = typeof ev.reason === "string" ? ev.reason : JSON.stringify(ev.reason ?? {});
29
- addBreadcrumb({ timestamp: Date.now(), category: "error", message: reason, level: "error" });
30
- const breadcrumbs = getBreadcrumbs();
31
- const span = tracer.startSpan("browser.unhandledrejection", {
32
- attributes: {
33
- "error.message": reason,
34
- "breadcrumbs.count": breadcrumbs.length,
35
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
36
- ...(sessionId ? { "session.id": sessionId } : {}),
37
- },
38
- });
39
- span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
35
+ let reason;
36
+ let stack = "";
40
37
  if (ev.reason instanceof Error) {
41
- span.recordException(ev.reason);
38
+ reason = `${ev.reason.name}: ${ev.reason.message}`;
39
+ stack = ev.reason.stack || "";
40
+ }
41
+ else {
42
+ reason = typeof ev.reason === "string" ? ev.reason : JSON.stringify(ev.reason ?? {});
43
+ }
44
+ addBreadcrumb({ timestamp: Date.now(), category: "error", message: reason, level: "error" });
45
+ try {
46
+ const breadcrumbs = getBreadcrumbs();
47
+ const span = tracer.startSpan("browser.unhandledrejection", {
48
+ attributes: {
49
+ "error.message": reason,
50
+ "error.stack": stack.slice(0, 4096),
51
+ "error.type": ev.reason?.constructor?.name || "UnhandledRejection",
52
+ "breadcrumbs.count": breadcrumbs.length,
53
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
54
+ ...(sessionId ? { "session.id": sessionId } : {}),
55
+ },
56
+ });
57
+ span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
58
+ if (ev.reason instanceof Error) {
59
+ span.recordException(ev.reason);
60
+ }
61
+ span.end();
42
62
  }
43
- span.end();
63
+ catch { }
44
64
  };
45
65
  window.addEventListener("error", onError);
46
66
  window.addEventListener("unhandledrejection", onRejection);
@@ -157,7 +157,12 @@ export function initBrowserSDK(config) {
157
157
  const sampleRate = config.tracesSampleRate ?? 1;
158
158
  const replaySampleRate = config.replaySampleRate ?? 1;
159
159
  const shouldReplay = Math.random() < replaySampleRate;
160
- const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate });
160
+ const replay = new BrowserReplayBuffer({
161
+ maxChunkBytes: config.replay?.maxChunkBytes ?? 480_000,
162
+ flushIntervalMs: config.replay?.flushIntervalMs ?? 5000,
163
+ sessionStorageKey: config.replay?.sessionStorageKey ?? "obtrace_session_id",
164
+ });
165
+ const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate, sessionId: replay.sessionId });
161
166
  const tracer = otel.tracer;
162
167
  const meter = otel.meter;
163
168
  const client = new ObtraceClient({
@@ -173,11 +178,6 @@ export function initBrowserSDK(config) {
173
178
  vitals: { enabled: true, reportAllChanges: false, ...config.vitals },
174
179
  propagation: { enabled: true, ...config.propagation },
175
180
  });
176
- const replay = new BrowserReplayBuffer({
177
- maxChunkBytes: config.replay?.maxChunkBytes ?? 480_000,
178
- flushIntervalMs: config.replay?.flushIntervalMs ?? 5000,
179
- sessionStorageKey: config.replay?.sessionStorageKey ?? "obtrace_session_id",
180
- });
181
181
  const recipeSteps = [];
182
182
  const cleanups = [];
183
183
  const entry = { client, sessionId: replay.sessionId, replay, config, recipeSteps, otel };
@@ -245,9 +245,16 @@ export function initBrowserSDK(config) {
245
245
  pendingBeaconBlob = null;
246
246
  }
247
247
  };
248
- const onVisibility = () => { if (document.visibilityState === "hidden")
249
- flushReplay(); };
250
- const onBeforeUnload = () => sendViaBeacon();
248
+ const onVisibility = () => {
249
+ if (document.visibilityState === "hidden") {
250
+ otel.forceFlush();
251
+ flushReplay();
252
+ }
253
+ };
254
+ const onBeforeUnload = () => {
255
+ otel.forceFlush();
256
+ sendViaBeacon();
257
+ };
251
258
  if (typeof document !== "undefined") {
252
259
  document.addEventListener("visibilitychange", onVisibility);
253
260
  cleanups.push(() => document.removeEventListener("visibilitychange", onVisibility));
@@ -27784,6 +27784,7 @@ function setupOtelWeb(config) {
27784
27784
  ...config.projectId ? { "obtrace.project_id": config.projectId } : {},
27785
27785
  ...config.appId ? { "obtrace.app_id": config.appId } : {},
27786
27786
  ...config.env ? { "obtrace.env": config.env } : {},
27787
+ ...config.sessionId ? { "session.id": config.sessionId } : {},
27787
27788
  "runtime.name": "browser"
27788
27789
  });
27789
27790
  const traceExporter = new OTLPTraceExporter({
@@ -27840,13 +27841,24 @@ function setupOtelWeb(config) {
27840
27841
  tracerProvider,
27841
27842
  instrumentations
27842
27843
  });
27843
- const tracer = trace.getTracer("@obtrace/sdk-browser", "1.1.0");
27844
- const meter = metrics.getMeter("@obtrace/sdk-browser", "1.1.0");
27844
+ const tracer = trace.getTracer("@obtrace/sdk-browser", "2.1.0");
27845
+ const meter = metrics.getMeter("@obtrace/sdk-browser", "2.1.0");
27846
+ const forceFlush = async () => {
27847
+ try {
27848
+ await tracerProvider.forceFlush();
27849
+ } catch {
27850
+ }
27851
+ try {
27852
+ await meterProvider.forceFlush();
27853
+ } catch {
27854
+ }
27855
+ };
27845
27856
  const shutdown = async () => {
27857
+ await forceFlush();
27846
27858
  await tracerProvider.shutdown();
27847
27859
  await meterProvider.shutdown();
27848
27860
  };
27849
- return { tracer, meter, shutdown };
27861
+ return { tracer, meter, shutdown, forceFlush };
27850
27862
  }
27851
27863
 
27852
27864
  // src/browser/breadcrumbs.ts
@@ -27893,42 +27905,61 @@ function installBrowserErrorHooks(tracer, sessionId) {
27893
27905
  return () => void 0;
27894
27906
  }
27895
27907
  const onError = (ev) => {
27896
- addBreadcrumb({ timestamp: Date.now(), category: "error", message: ev.message || "window.error", level: "error" });
27897
- const breadcrumbs = getBreadcrumbs();
27898
- const span = tracer.startSpan("browser.error", {
27899
- attributes: {
27900
- "error.message": ev.message || "window.error",
27901
- "error.file": ev.filename || "",
27902
- "error.line": ev.lineno || 0,
27903
- "error.column": ev.colno || 0,
27904
- "breadcrumbs.count": breadcrumbs.length,
27905
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
27906
- ...sessionId ? { "session.id": sessionId } : {}
27908
+ const message = ev.message || "window.error";
27909
+ addBreadcrumb({ timestamp: Date.now(), category: "error", message, level: "error" });
27910
+ try {
27911
+ const breadcrumbs = getBreadcrumbs();
27912
+ const stack = ev.error instanceof Error ? ev.error.stack || "" : "";
27913
+ const span = tracer.startSpan("browser.error", {
27914
+ attributes: {
27915
+ "error.message": message,
27916
+ "error.file": ev.filename || "",
27917
+ "error.line": ev.lineno || 0,
27918
+ "error.column": ev.colno || 0,
27919
+ "error.stack": stack.slice(0, 4096),
27920
+ "error.type": ev.error?.constructor?.name || "Error",
27921
+ "breadcrumbs.count": breadcrumbs.length,
27922
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
27923
+ ...sessionId ? { "session.id": sessionId } : {}
27924
+ }
27925
+ });
27926
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
27927
+ if (ev.error instanceof Error) {
27928
+ span.recordException(ev.error);
27907
27929
  }
27908
- });
27909
- span.setStatus({ code: SpanStatusCode.ERROR, message: ev.message });
27910
- if (ev.error instanceof Error) {
27911
- span.recordException(ev.error);
27930
+ span.end();
27931
+ } catch {
27912
27932
  }
27913
- span.end();
27914
27933
  };
27915
27934
  const onRejection = (ev) => {
27916
- const reason = typeof ev.reason === "string" ? ev.reason : JSON.stringify(ev.reason ?? {});
27935
+ let reason;
27936
+ let stack = "";
27937
+ if (ev.reason instanceof Error) {
27938
+ reason = `${ev.reason.name}: ${ev.reason.message}`;
27939
+ stack = ev.reason.stack || "";
27940
+ } else {
27941
+ reason = typeof ev.reason === "string" ? ev.reason : JSON.stringify(ev.reason ?? {});
27942
+ }
27917
27943
  addBreadcrumb({ timestamp: Date.now(), category: "error", message: reason, level: "error" });
27918
- const breadcrumbs = getBreadcrumbs();
27919
- const span = tracer.startSpan("browser.unhandledrejection", {
27920
- attributes: {
27921
- "error.message": reason,
27922
- "breadcrumbs.count": breadcrumbs.length,
27923
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
27924
- ...sessionId ? { "session.id": sessionId } : {}
27944
+ try {
27945
+ const breadcrumbs = getBreadcrumbs();
27946
+ const span = tracer.startSpan("browser.unhandledrejection", {
27947
+ attributes: {
27948
+ "error.message": reason,
27949
+ "error.stack": stack.slice(0, 4096),
27950
+ "error.type": ev.reason?.constructor?.name || "UnhandledRejection",
27951
+ "breadcrumbs.count": breadcrumbs.length,
27952
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
27953
+ ...sessionId ? { "session.id": sessionId } : {}
27954
+ }
27955
+ });
27956
+ span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
27957
+ if (ev.reason instanceof Error) {
27958
+ span.recordException(ev.reason);
27925
27959
  }
27926
- });
27927
- span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
27928
- if (ev.reason instanceof Error) {
27929
- span.recordException(ev.reason);
27960
+ span.end();
27961
+ } catch {
27930
27962
  }
27931
- span.end();
27932
27963
  };
27933
27964
  window.addEventListener("error", onError);
27934
27965
  window.addEventListener("unhandledrejection", onRejection);
@@ -28218,30 +28249,66 @@ function installConsoleCapture(tracer, sessionId) {
28218
28249
  const level = LEVEL_MAP[method];
28219
28250
  console[method] = (...args) => {
28220
28251
  original(...args);
28221
- let message;
28222
- let attrs = {};
28223
- if (args.length === 1 && typeof args[0] === "object" && args[0] !== null && !Array.isArray(args[0])) {
28224
- const obj = args[0];
28225
- message = String(obj.msg || obj.message || JSON.stringify(obj));
28226
- for (const [k, v] of Object.entries(obj)) {
28227
- if (k === "msg" || k === "message") continue;
28228
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
28229
- attrs[k] = v;
28252
+ try {
28253
+ let message;
28254
+ let attrs = {};
28255
+ const safeStringify = (v) => {
28256
+ try {
28257
+ return JSON.stringify(v);
28258
+ } catch {
28259
+ return String(v);
28260
+ }
28261
+ };
28262
+ const firstArg = args[0];
28263
+ const isErrorObj = firstArg instanceof Error;
28264
+ if (isErrorObj) {
28265
+ const err = firstArg;
28266
+ message = `${err.name}: ${err.message}`;
28267
+ if (err.stack) attrs["error.stack"] = err.stack.slice(0, 4096);
28268
+ attrs["error.type"] = err.name;
28269
+ for (let i = 1; i < args.length; i++) {
28270
+ const extra = args[i];
28271
+ if (typeof extra === "string" && extra.includes("\n at ")) {
28272
+ attrs["error.component_stack"] = extra.slice(0, 4096);
28273
+ }
28274
+ }
28275
+ } else if (args.length === 1 && typeof firstArg === "object" && firstArg !== null && !Array.isArray(firstArg)) {
28276
+ const obj = firstArg;
28277
+ message = String(obj.msg || obj.message || safeStringify(obj));
28278
+ for (const [k, v] of Object.entries(obj)) {
28279
+ if (k === "msg" || k === "message") continue;
28280
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
28281
+ attrs[k] = v;
28282
+ }
28283
+ }
28284
+ } else {
28285
+ message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
28286
+ if (method === "error") {
28287
+ for (const a of args) {
28288
+ if (typeof a === "string" && a.includes("\n at ")) {
28289
+ attrs["error.stack"] = a.slice(0, 4096);
28290
+ break;
28291
+ }
28292
+ }
28230
28293
  }
28231
28294
  }
28232
- } else {
28233
- message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
28234
- }
28235
- addBreadcrumb({ timestamp: Date.now(), category: `console.${method}`, message, level, data: attrs });
28236
- const span = tracer.startSpan("browser.console", {
28237
- attributes: {
28238
- "log.severity": level.toUpperCase(),
28239
- "log.message": message.slice(0, 1024),
28240
- "session.id": sessionId,
28241
- ...attrs
28295
+ addBreadcrumb({ timestamp: Date.now(), category: `console.${method}`, message, level, data: attrs });
28296
+ const spanName = method === "error" && (isErrorObj || attrs["error.stack"]) ? "browser.error" : "browser.console";
28297
+ const span = tracer.startSpan(spanName, {
28298
+ attributes: {
28299
+ "log.severity": level.toUpperCase(),
28300
+ "log.message": message.slice(0, 1024),
28301
+ "session.id": sessionId,
28302
+ ...attrs
28303
+ }
28304
+ });
28305
+ if (method === "error") {
28306
+ span.setStatus({ code: SpanStatusCode.ERROR, message: message.slice(0, 1024) });
28307
+ if (isErrorObj) span.recordException(firstArg);
28242
28308
  }
28243
- });
28244
- span.end();
28309
+ span.end();
28310
+ } catch {
28311
+ }
28245
28312
  };
28246
28313
  }
28247
28314
  return () => {
@@ -28525,7 +28592,12 @@ function initBrowserSDK(config) {
28525
28592
  const sampleRate = config.tracesSampleRate ?? 1;
28526
28593
  const replaySampleRate = config.replaySampleRate ?? 1;
28527
28594
  const shouldReplay = Math.random() < replaySampleRate;
28528
- const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate });
28595
+ const replay = new BrowserReplayBuffer({
28596
+ maxChunkBytes: config.replay?.maxChunkBytes ?? 48e4,
28597
+ flushIntervalMs: config.replay?.flushIntervalMs ?? 5e3,
28598
+ sessionStorageKey: config.replay?.sessionStorageKey ?? "obtrace_session_id"
28599
+ });
28600
+ const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate, sessionId: replay.sessionId });
28529
28601
  const tracer = otel.tracer;
28530
28602
  const meter = otel.meter;
28531
28603
  const client = new ObtraceClient({
@@ -28541,11 +28613,6 @@ function initBrowserSDK(config) {
28541
28613
  vitals: { enabled: true, reportAllChanges: false, ...config.vitals },
28542
28614
  propagation: { enabled: true, ...config.propagation }
28543
28615
  });
28544
- const replay = new BrowserReplayBuffer({
28545
- maxChunkBytes: config.replay?.maxChunkBytes ?? 48e4,
28546
- flushIntervalMs: config.replay?.flushIntervalMs ?? 5e3,
28547
- sessionStorageKey: config.replay?.sessionStorageKey ?? "obtrace_session_id"
28548
- });
28549
28616
  const recipeSteps = [];
28550
28617
  const cleanups = [];
28551
28618
  const entry = { client, sessionId: replay.sessionId, replay, config, recipeSteps, otel };
@@ -28610,9 +28677,15 @@ function initBrowserSDK(config) {
28610
28677
  }
28611
28678
  };
28612
28679
  const onVisibility = () => {
28613
- if (document.visibilityState === "hidden") flushReplay();
28680
+ if (document.visibilityState === "hidden") {
28681
+ otel.forceFlush();
28682
+ flushReplay();
28683
+ }
28684
+ };
28685
+ const onBeforeUnload = () => {
28686
+ otel.forceFlush();
28687
+ sendViaBeacon();
28614
28688
  };
28615
- const onBeforeUnload = () => sendViaBeacon();
28616
28689
  if (typeof document !== "undefined") {
28617
28690
  document.addEventListener("visibilitychange", onVisibility);
28618
28691
  cleanups.push(() => document.removeEventListener("visibilitychange", onVisibility));