@maple-dev/browser 0.12.0 → 0.13.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.
@@ -1,5 +1,5 @@
1
- import { B as mayPersistIdentifier, C as otlpKeepaliveRoom, E as postToIngest, G as scrubUrl, H as setConsent, I as configurePrivacy, L as consentAllowedSince, O as sdkHint, P as setActiveTraceIdProvider, S as ingestHeaders, U as setVisitorTracking, V as onConsentChange, _ as rotateSession, a as recordTraceId, b as OTLP_UNLOAD_TAIL_BYTES, c as getActiveSink, d as startEventSink, f as claimReplayMode, i as readSessionSink, n as getObservedTraceIds, o as startSessionLifecycle, p as getSession, r as publishSessionSink, s as clearPendingEvents, t as clearSessionSink, u as queuePending, v as postSessionMetaRow, y as isLikelyBot, z as hasConsent } from "./sink-DbEuphC-.mjs";
2
- import { ProxyTracerProvider, SpanKind, SpanStatusCode, TraceFlags, context, createContextKey, createTraceState, isSpanContextValid, propagation, trace } from "@opentelemetry/api";
1
+ import { A as postToIngest, C as OTLP_UNLOAD_TAIL_BYTES, D as otlpKeepaliveRoom, E as ingestHeaders, G as mayPersistIdentifier, H as consentAllowedSince, J as setVisitorTracking, K as onConsentChange, L as truncateText, M as sdkHint, S as isLikelyBot, V as configurePrivacy, W as hasConsent, X as scrubUrl, a as recordTraceId, c as getActiveSink, d as startEventSink, f as claimReplayMode, i as readSessionSink, n as getObservedTraceIds, o as startSessionLifecycle, p as getSession, q as setConsent, r as publishSessionSink, s as clearPendingEvents, t as clearSessionSink, u as queuePending, x as postSessionMetaRow, y as rotateSession, z as setActiveTraceIdProvider } from "./sink-Ji0J42ul.mjs";
2
+ import { INVALID_SPAN_CONTEXT, ProxyTracerProvider, SpanKind, SpanStatusCode, TraceFlags, context, createContextKey, createTraceState, isSpanContextValid, propagation, trace } from "@opentelemetry/api";
3
3
  import { BatchSpanProcessor, SamplingDecision } from "@opentelemetry/sdk-trace-base";
4
4
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
5
5
  import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
@@ -7,7 +7,112 @@ import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xm
7
7
  import { JsonTraceSerializer } from "@opentelemetry/otlp-transformer";
8
8
  import { resourceFromAttributes } from "@opentelemetry/resources";
9
9
  import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
10
- import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
10
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_TELEMETRY_SDK_LANGUAGE, ATTR_TELEMETRY_SDK_NAME, ATTR_TELEMETRY_SDK_VERSION, TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS } from "@opentelemetry/semantic-conventions";
11
+ //#region ../browser-session/src/events/props.ts
12
+ const MAX_PROPS = 32;
13
+ const MAX_PROP_KEY_LENGTH = 64;
14
+ const MAX_PROP_VALUE_LENGTH = 1024;
15
+ const MAX_TOTAL_PROP_BYTES = 8192;
16
+ /** A surrogate pair, or a lone half of one. No lookbehind: older Safari cannot parse it. */
17
+ const SURROGATES = /[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g;
18
+ /**
19
+ * `text` with every lone surrogate replaced by U+FFFD. Strict JSON parsers
20
+ * (ingest's among them) reject the escape for one, and the whole batch with it.
21
+ */
22
+ function wellFormed(text) {
23
+ return text.replace(SURROGATES, (match) => match.length === 2 ? match : "�");
24
+ }
25
+ /** Cut to `max` UTF-16 units, never leaving half a surrogate pair behind. */
26
+ function capText(text, max) {
27
+ return wellFormed(truncateText(text, max));
28
+ }
29
+ /**
30
+ * Coerce one property value to the string the warehouse column holds.
31
+ *
32
+ * `null`/`undefined`/functions/symbols are dropped rather than stringified:
33
+ * `"undefined"` as a stored value is worse than an absent key.
34
+ */
35
+ function coerce(value) {
36
+ if (value === null || value === void 0) return void 0;
37
+ switch (typeof value) {
38
+ case "string": return capText(value, MAX_PROP_VALUE_LENGTH);
39
+ case "number":
40
+ case "boolean":
41
+ case "bigint": return String(value);
42
+ case "function":
43
+ case "symbol": return;
44
+ }
45
+ try {
46
+ if (value instanceof Date) return value.toISOString();
47
+ const json = JSON.stringify(value);
48
+ return json === void 0 ? void 0 : capText(json, MAX_PROP_VALUE_LENGTH);
49
+ } catch {
50
+ return;
51
+ }
52
+ }
53
+ /**
54
+ * Whether `value` is a property bag. Arrays and primitives are not: a string
55
+ * would otherwise become `{0: "h", 1: "i"}`. `Array.isArray` throws on a
56
+ * revoked Proxy, hence the guard.
57
+ */
58
+ function isPropertyBag(value) {
59
+ try {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ /** Own enumerable string keys of `bag`, or none when a Proxy's `ownKeys` trap throws. */
66
+ function bagKeys(bag) {
67
+ try {
68
+ return Object.keys(bag);
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+ /**
74
+ * `parse(bag[key])`, or `undefined` when the getter (or Proxy `get` trap) throws.
75
+ * BOUNDARY: `parse` receives a host-app value straight off an untyped bag.
76
+ */
77
+ function readEntry(bag, key, parse) {
78
+ try {
79
+ return parse(bag[key]);
80
+ } catch {
81
+ return;
82
+ }
83
+ }
84
+ /**
85
+ * Store `value` as an own data property. Plain assignment of `"__proto__"`
86
+ * would try to swap the prototype and silently drop the key instead.
87
+ */
88
+ function defineEntry(out, key, value) {
89
+ Object.defineProperty(out, key, {
90
+ configurable: true,
91
+ enumerable: true,
92
+ value,
93
+ writable: true
94
+ });
95
+ }
96
+ /** Coerce and cap a `track()` property bag to the `Map(String, String)` the warehouse stores. */
97
+ function coerceTrackProps(props) {
98
+ const out = {};
99
+ if (!isPropertyBag(props)) return out;
100
+ let count = 0;
101
+ let bytes = 0;
102
+ for (const rawKey of bagKeys(props)) {
103
+ if (count >= MAX_PROPS) break;
104
+ const value = readEntry(props, rawKey, coerce);
105
+ if (value === void 0) continue;
106
+ const key = capText(rawKey, MAX_PROP_KEY_LENGTH);
107
+ if (!key || Object.prototype.hasOwnProperty.call(out, key)) continue;
108
+ bytes += key.length + value.length;
109
+ if (bytes > MAX_TOTAL_PROP_BYTES) break;
110
+ defineEntry(out, key, value);
111
+ count++;
112
+ }
113
+ return out;
114
+ }
115
+ //#endregion
11
116
  //#region ../browser-session/src/identity/identity.ts
12
117
  /**
13
118
  * Trait caps. The warehouse column is `Map(String, String)` — plain keys
@@ -19,35 +124,48 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic
19
124
  const MAX_TRAITS = 24;
20
125
  const MAX_TRAIT_KEY_LENGTH = 64;
21
126
  const MAX_TRAIT_VALUE_LENGTH = 256;
127
+ /** Ids, emails and names; matches the gateway's per-id byte cap in UTF-16 units. */
128
+ const MAX_FIELD_LENGTH = 256;
22
129
  let warnedAboutTraitKeys = false;
23
130
  /** Heuristic: a trait *key* that looks like an id is almost always a mistake. */
24
131
  function looksLikeId(key) {
25
132
  return /^[0-9a-f]{8,}$/i.test(key) || /^(usr|user|org|cus|acct)_/i.test(key);
26
133
  }
27
134
  function coerceTraitValue(value) {
28
- if (value === null || value === void 0) return void 0;
29
- return (typeof value === "string" ? value : String(value)).slice(0, MAX_TRAIT_VALUE_LENGTH);
135
+ switch (typeof value) {
136
+ case "string": return capText(value, MAX_TRAIT_VALUE_LENGTH);
137
+ case "number":
138
+ case "boolean":
139
+ case "bigint": return String(value);
140
+ default: return;
141
+ }
30
142
  }
31
143
  function normalizeTraits(traits) {
32
- if (!traits) return {};
33
144
  const out = {};
34
- for (const [rawKey, rawValue] of Object.entries(traits)) {
35
- if (Object.keys(out).length >= MAX_TRAITS) break;
36
- const value = coerceTraitValue(rawValue);
145
+ if (!isPropertyBag(traits)) return out;
146
+ let count = 0;
147
+ for (const rawKey of bagKeys(traits)) {
148
+ if (count >= MAX_TRAITS) break;
149
+ const value = readEntry(traits, rawKey, coerceTraitValue);
37
150
  if (value === void 0) continue;
38
- const key = rawKey.slice(0, MAX_TRAIT_KEY_LENGTH);
39
- if (!key) continue;
151
+ const key = capText(rawKey, MAX_TRAIT_KEY_LENGTH);
152
+ if (!key || Object.prototype.hasOwnProperty.call(out, key)) continue;
40
153
  if (!warnedAboutTraitKeys && looksLikeId(key)) {
41
154
  warnedAboutTraitKeys = true;
42
155
  console.warn(`[maple] identity trait key "${key}" looks like an id. A key per user widens every session row's trait map; put ids in the value, or in id/groupId, not the key.`);
43
156
  }
44
- out[key] = value;
157
+ defineEntry(out, key, value);
158
+ count++;
45
159
  }
46
160
  return out;
47
161
  }
48
- function trimmed(value) {
49
- const text = value?.trim();
50
- return text ? text : void 0;
162
+ /**
163
+ * A trimmed, capped string field. Numeric ids (`{ id: 42 }`) are common enough in
164
+ * untyped callers to keep; anything else that is not a string is ignored.
165
+ */
166
+ function field(value) {
167
+ const trimmed = (typeof value === "string" ? value : typeof value === "number" && Number.isFinite(value) || typeof value === "bigint" ? String(value) : void 0)?.trim();
168
+ return trimmed ? capText(trimmed, MAX_FIELD_LENGTH) : void 0;
51
169
  }
52
170
  /**
53
171
  * Normalize whatever a host app passed into the shape the metadata row wants.
@@ -58,21 +176,20 @@ function trimmed(value) {
58
176
  * already did.
59
177
  */
60
178
  function normalizeIdentity(input) {
61
- if (input === null || input === void 0) return void 0;
62
- if (typeof input === "string") {
63
- const id = trimmed(input);
179
+ if (!isPropertyBag(input)) {
180
+ const id = field(input);
64
181
  return id ? {
65
182
  id,
66
183
  traits: {}
67
184
  } : void 0;
68
185
  }
69
186
  const resolved = {
70
- id: trimmed(input.id),
71
- email: trimmed(input.email),
72
- username: trimmed(input.username),
73
- groupId: trimmed(input.groupId),
74
- groupName: trimmed(input.groupName),
75
- traits: normalizeTraits(input.traits)
187
+ id: readEntry(input, "id", field),
188
+ email: readEntry(input, "email", field),
189
+ username: readEntry(input, "username", field),
190
+ groupId: readEntry(input, "groupId", field),
191
+ groupName: readEntry(input, "groupName", field),
192
+ traits: readEntry(input, "traits", normalizeTraits) ?? {}
76
193
  };
77
194
  return resolved.id || resolved.email || resolved.username || resolved.groupId || resolved.groupName || Object.keys(resolved.traits).length > 0 ? resolved : void 0;
78
195
  }
@@ -109,54 +226,9 @@ function clearPendingChunk() {
109
226
  }
110
227
  }
111
228
  //#endregion
112
- //#region ../browser-session/src/events/props.ts
113
- const MAX_PROPS = 32;
114
- const MAX_PROP_KEY_LENGTH = 64;
115
- const MAX_PROP_VALUE_LENGTH = 1024;
116
- const MAX_TOTAL_PROP_BYTES = 8192;
117
- /**
118
- * Coerce one property value to the string the warehouse column holds.
119
- *
120
- * `null`/`undefined`/functions/symbols are dropped rather than stringified —
121
- * `"undefined"` as a stored value is worse than an absent key.
122
- */
123
- function coerce(value) {
124
- if (value === null || value === void 0) return void 0;
125
- switch (typeof value) {
126
- case "string": return value.slice(0, MAX_PROP_VALUE_LENGTH);
127
- case "number":
128
- case "boolean":
129
- case "bigint": return String(value);
130
- case "function":
131
- case "symbol": return;
132
- }
133
- try {
134
- if (value instanceof Date) return value.toISOString();
135
- return JSON.stringify(value)?.slice(0, MAX_PROP_VALUE_LENGTH);
136
- } catch {
137
- return;
138
- }
139
- }
140
- /** Coerce and cap a `track()` property bag to the `Map(String, String)` the warehouse stores. */
141
- function coerceTrackProps(props) {
142
- if (!props) return {};
143
- const out = {};
144
- let bytes = 0;
145
- for (const [rawKey, rawValue] of Object.entries(props)) {
146
- if (Object.keys(out).length >= MAX_PROPS) break;
147
- const value = coerce(rawValue);
148
- if (value === void 0) continue;
149
- const key = rawKey.slice(0, MAX_PROP_KEY_LENGTH);
150
- if (!key) continue;
151
- bytes += key.length + value.length;
152
- if (bytes > MAX_TOTAL_PROP_BYTES) break;
153
- out[key] = value;
154
- }
155
- return out;
156
- }
157
- //#endregion
158
229
  //#region ../browser-session/src/events/track.ts
159
230
  let warnedAboutName = false;
231
+ let warnedAboutReservedName = false;
160
232
  /**
161
233
  * Record a custom product event against the current session.
162
234
  *
@@ -176,9 +248,17 @@ function track(name, props) {
176
248
  }
177
249
  return;
178
250
  }
251
+ const trimmedName = name.trim();
252
+ if (trimmedName.startsWith("$")) {
253
+ if (!warnedAboutReservedName) {
254
+ warnedAboutReservedName = true;
255
+ console.warn(`[maple] track() event names starting with "$" are reserved; "${trimmedName}" was ignored.`);
256
+ }
257
+ return;
258
+ }
179
259
  const ev = {
180
260
  type: "custom",
181
- message: name.trim().slice(0, 128),
261
+ message: capText(trimmedName, 128),
182
262
  attrs: coerceTrackProps(props),
183
263
  timestamp: Date.now(),
184
264
  url: typeof location !== "undefined" ? location.href : void 0
@@ -265,41 +345,133 @@ function warnIfKeylessMapleIngest(options) {
265
345
  //#region ../sdk-core/src/errors.ts
266
346
  /** Deep enough for real wrapping chains, bounded against a cause cycle or a huge aggregate. */
267
347
  const MAX_LINKED = 5;
348
+ /** A thrown plain object is summarised by its first few primitive fields, never walked. */
349
+ const MAX_SUMMARY_FIELDS = 8;
350
+ const MAX_SUMMARY_VALUE = 80;
351
+ const MAX_SUMMARY = 300;
268
352
  const isRecord = (value) => typeof value === "object" && value !== null;
353
+ /** A property read that a throwing getter or Proxy trap cannot turn into a second error. */
354
+ function read(value, key) {
355
+ try {
356
+ return value[key];
357
+ } catch {
358
+ return;
359
+ }
360
+ }
361
+ function isErrorInstance(value) {
362
+ try {
363
+ return value instanceof Error;
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
269
368
  /** Error-shaped: an `Error`, or a copy with its fields (Effect's pretty errors, a structured clone). */
270
- const isErrorLike = (value) => value instanceof Error || isRecord(value) && typeof value.name === "string" && typeof value.message === "string" && (value.stack === void 0 || typeof value.stack === "string");
369
+ function isErrorLike(value) {
370
+ if (isErrorInstance(value)) return true;
371
+ if (!isRecord(value)) return false;
372
+ const stack = read(value, "stack");
373
+ return typeof read(value, "name") === "string" && typeof read(value, "message") === "string" && (stack === void 0 || typeof stack === "string");
374
+ }
375
+ /** Cut at a code point boundary, so a clipped value never ends in half a surrogate pair. */
376
+ function clip(text, max) {
377
+ if (text.length <= max) return text;
378
+ const end = /[\uD800-\uDBFF]/.test(text.charAt(max - 1)) ? max - 1 : max;
379
+ return `${text.slice(0, end)}…`;
380
+ }
381
+ function tagOf(value) {
382
+ try {
383
+ return Object.prototype.toString.call(value);
384
+ } catch {
385
+ return "[object Object]";
386
+ }
387
+ }
388
+ /** `{"code":1,"reason":"busy"}`: what a thrown plain object says about itself, bounded. */
389
+ function summarize(value) {
390
+ let keys;
391
+ try {
392
+ keys = Object.keys(value);
393
+ } catch {
394
+ return tagOf(value);
395
+ }
396
+ const fields = [];
397
+ for (const key of keys) {
398
+ if (fields.length >= MAX_SUMMARY_FIELDS) break;
399
+ let field;
400
+ try {
401
+ field = value[key];
402
+ } catch {
403
+ continue;
404
+ }
405
+ const rendered = typeof field === "string" ? JSON.stringify(clip(field, MAX_SUMMARY_VALUE)) : typeof field === "number" || typeof field === "boolean" || field === null ? String(field) : typeof field === "bigint" ? `${field}n` : void 0;
406
+ if (rendered !== void 0) fields.push(`${JSON.stringify(clip(key, MAX_SUMMARY_VALUE))}:${rendered}`);
407
+ }
408
+ return fields.length > 0 ? clip(`{${fields.join(",")}}`, MAX_SUMMARY) : tagOf(value);
409
+ }
410
+ /**
411
+ * An `Error` made here for a value thrown elsewhere. Its own stack would point
412
+ * into the SDK, which is not where anything was thrown, and would group every
413
+ * such throw into one issue, so it carries the source's stack or none.
414
+ */
415
+ function madeError(message, name, stack) {
416
+ const error = new Error(message);
417
+ if (name !== void 0 && name !== "") error.name = name;
418
+ try {
419
+ Object.defineProperty(error, "stack", {
420
+ value: stack,
421
+ writable: true,
422
+ configurable: true
423
+ });
424
+ } catch {}
425
+ return error;
426
+ }
427
+ function stringOf(value) {
428
+ try {
429
+ return String(value);
430
+ } catch {
431
+ return "Unknown error";
432
+ }
433
+ }
271
434
  /**
272
435
  * BOUNDARY: a thrown value is unparsed by definition, JavaScript can throw anything.
273
436
  * Narrow it into an `Error` without ever throwing from the error path.
274
437
  */
275
438
  function asError(value) {
276
- if (value instanceof Error) return value;
277
- if (typeof value === "string") return new Error(value);
278
- if (isRecord(value) && typeof value.message === "string") return new Error(value.message);
279
- try {
280
- return new Error(String(value));
281
- } catch {
282
- return /* @__PURE__ */ new Error("Unknown error");
439
+ if (isErrorInstance(value)) {
440
+ const name = read(value, "name");
441
+ const message = read(value, "message");
442
+ const stack = read(value, "stack");
443
+ if (typeof name === "string" && typeof message === "string" && (stack === void 0 || typeof stack === "string")) return value;
444
+ return madeError(typeof message === "string" ? message : "", typeof name === "string" ? name : void 0, typeof stack === "string" ? stack : void 0);
445
+ }
446
+ if (typeof value === "string") return madeError(value);
447
+ if (isRecord(value)) {
448
+ const message = read(value, "message");
449
+ if (typeof message === "string") {
450
+ const name = read(value, "name");
451
+ const stack = read(value, "stack");
452
+ return madeError(message, typeof name === "string" ? name : void 0, typeof stack === "string" ? stack : void 0);
453
+ }
454
+ return madeError(summarize(value));
283
455
  }
456
+ return madeError(stringOf(value));
284
457
  }
285
458
  /** What an error links to: its `cause`, and its `errors` list (duck-typed, so copies qualify). */
286
459
  const linksOf = (value) => {
287
- if (!isErrorLike(value)) return [];
460
+ if (!isRecord(value) || !isErrorLike(value)) return [];
288
461
  const next = [];
289
462
  try {
290
- if ("errors" in value && Array.isArray(value.errors)) next.push(...value.errors);
291
- if (value.cause !== void 0) next.push(value.cause);
463
+ const errors = read(value, "errors");
464
+ if (Array.isArray(errors)) next.push(...errors.slice(0, MAX_LINKED));
292
465
  } catch {}
466
+ const cause = read(value, "cause");
467
+ if (cause !== void 0) next.push(cause);
293
468
  return next;
294
469
  };
295
470
  /** A null-prototype object or a throwing `toString` must not break the error path. */
296
471
  function render(value) {
297
- if (isRecord(value) && typeof value.message === "string") return value.message;
298
- try {
299
- return String(value);
300
- } catch {
301
- return Object.prototype.toString.call(value);
302
- }
472
+ if (!isRecord(value)) return stringOf(value);
473
+ const message = read(value, "message");
474
+ return typeof message === "string" ? message : summarize(value);
303
475
  }
304
476
  function linkedErrors(error) {
305
477
  const linked = [];
@@ -313,23 +485,31 @@ function linkedErrors(error) {
313
485
  }
314
486
  return linked;
315
487
  }
488
+ function stringField(value, key) {
489
+ const field = read(value, key);
490
+ return typeof field === "string" ? field : "";
491
+ }
316
492
  /** V8 starts a stack with a `Name: message` header; the other engines start at the first frame. */
317
493
  function framesOf(error) {
318
- const stack = typeof error.stack === "string" ? error.stack : "";
319
- const header = error.message ? `${error.name}: ${error.message}` : error.name;
494
+ const stack = stringField(error, "stack");
495
+ const name = stringField(error, "name");
496
+ const message = stringField(error, "message");
497
+ const header = message ? `${name}: ${message}` : name;
320
498
  return stack.startsWith(header) ? stack.slice(header.length).replace(/^\n/, "") : stack;
321
499
  }
322
500
  function describe(value) {
323
- if (!isErrorLike(value)) return `Caused by: ${render(value)}`;
501
+ if (!isRecord(value) || !isErrorLike(value)) return `Caused by: ${render(value)}`;
324
502
  const frames = framesOf(value);
325
- const header = `Caused by: ${value.name}: ${value.message}`;
503
+ const header = `Caused by: ${stringField(value, "name")}: ${stringField(value, "message")}`;
326
504
  return frames ? `${header}\n${frames}` : header;
327
505
  }
328
506
  /** The stack trace to record for `error`, with its linked errors appended. */
329
507
  function stackWithCauses(error) {
508
+ const own = read(error, "stack");
509
+ const stack = typeof own === "string" ? own : void 0;
330
510
  const linked = linkedErrors(error);
331
- if (linked.length === 0) return error.stack;
332
- return [error.stack ?? `${error.name}: ${error.message}`, ...linked.map(describe)].join("\n");
511
+ if (linked.length === 0) return stack;
512
+ return [stack ?? `${stringField(error, "name")}: ${stringField(error, "message")}`, ...linked.map(describe)].join("\n");
333
513
  }
334
514
  //#endregion
335
515
  //#region ../sdk-core/src/error-filters.ts
@@ -475,6 +655,8 @@ function sessionRoll(sessionId) {
475
655
  return (hash >>> 0) / 2 ** 32;
476
656
  }
477
657
  const MAX_56 = 2 ** 56;
658
+ /** The largest double below 2^56: `2 ** 56 - 1` rounds up to 2^56, which needs a 15th hex digit. */
659
+ const TOP_56 = MAX_56 - 8;
478
660
  /** 56 bits as the 14-hex-digit form `ot=th`/`ot=rv` use; `th` drops trailing zeros. */
479
661
  const hex56 = (value) => value.toString(16).padStart(14, "0");
480
662
  /**
@@ -482,14 +664,14 @@ const hex56 = (value) => value.toString(16).padStart(14, "0");
482
664
  * hex, trailing zeros dropped. Ingest reads it back as the span's weight.
483
665
  */
484
666
  function rejectionThreshold(probability) {
485
- return hex56(Math.round((1 - probability) * MAX_56)).replace(/0+$/, "") || "0";
667
+ return hex56(Math.min(TOP_56, Math.round((1 - probability) * MAX_56))).replace(/0+$/, "") || "0";
486
668
  }
487
669
  /**
488
670
  * The session's randomness as the W3C `ot=rv:` value. The decision is `rv >= th`,
489
671
  * so a downstream consistent-probability sampler reaches the same answer.
490
672
  */
491
673
  function randomnessValue(roll) {
492
- return Math.min(MAX_56 - 1, Math.floor((1 - roll) * MAX_56));
674
+ return Math.min(TOP_56, Math.floor((1 - roll) * MAX_56));
493
675
  }
494
676
  const KEEP$1 = { sampled: true };
495
677
  const DROP = { sampled: false };
@@ -754,9 +936,13 @@ var HttpStatusExporter = class {
754
936
  const MAX_HELD = 20;
755
937
  let stash;
756
938
  let held = [];
939
+ /** Attach the deferred chunk's stash, or detach it on shutdown, dropping what is held: it belongs to that session. */
757
940
  function attachSpanStash(next) {
758
941
  stash = next;
759
- if (!next) return;
942
+ if (!next) {
943
+ held = [];
944
+ return;
945
+ }
760
946
  const pending = held;
761
947
  held = [];
762
948
  for (const spans of pending) next(spans);
@@ -895,13 +1081,14 @@ function newestFirst(items, serializer) {
895
1081
  * newest items first, in a body small enough for keepalive, then the rest,
896
1082
  * which keeps keepalive while it fits and leaves `olderReserve` bytes of room
897
1083
  * for what is flushed after it. Each goes through `inner` on its own, so only
898
- * the part that failed is queued.
1084
+ * the part that failed is queued. `leaving` covers a pipeline whose exit flush
1085
+ * exports after `flushUnloading` has returned.
899
1086
  */
900
- function newestFirstOnExit(inner, serializer, olderReserve = 0) {
1087
+ function newestFirstOnExit(inner, serializer, olderReserve = 0, leaving = () => false) {
901
1088
  return {
902
1089
  export(items, callback) {
903
1090
  const hidden = globalThis.document?.visibilityState === "hidden";
904
- const parts = unloading || hidden ? newestFirst(items, serializer) : [items];
1091
+ const parts = unloading || hidden || leaving() ? newestFirst(items, serializer) : [items];
905
1092
  let pending = parts.length;
906
1093
  let failed;
907
1094
  for (const [index, part] of parts.entries()) {
@@ -923,7 +1110,7 @@ function newestFirstOnExit(inner, serializer, olderReserve = 0) {
923
1110
  }
924
1111
  //#endregion
925
1112
  //#region src/version.ts
926
- const SDK_VERSION = "0.12.0";
1113
+ const SDK_VERSION = "0.13.0";
927
1114
  /** The `x-maple-sdk` value this build sends. */
928
1115
  const SDK_NAME = "maple-browser";
929
1116
  //#endregion
@@ -941,7 +1128,7 @@ var TraceIdCollector = class {
941
1128
  this.getUserId = getUserId;
942
1129
  }
943
1130
  onStart(span) {
944
- for (const key of URL_ATTRIBUTES) {
1131
+ if (span.instrumentationScope?.name !== "maple-browser") for (const key of URL_ATTRIBUTES) {
945
1132
  const value = span.attributes[key];
946
1133
  if (typeof value === "string") span.setAttribute(key, scrubUrl(value));
947
1134
  }
@@ -1015,12 +1202,15 @@ function liveMapleTracer(name, version) {
1015
1202
  function resourceAttributes(config) {
1016
1203
  const attributes = {
1017
1204
  [ATTR_SERVICE_NAME]: config.serviceName,
1205
+ [ATTR_TELEMETRY_SDK_NAME]: "@maple-dev/browser",
1206
+ [ATTR_TELEMETRY_SDK_LANGUAGE]: TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS,
1207
+ [ATTR_TELEMETRY_SDK_VERSION]: SDK_VERSION,
1018
1208
  "maple.sdk.type": "browser"
1019
1209
  };
1020
1210
  if (config.serviceNamespace) attributes["service.namespace"] = config.serviceNamespace;
1021
1211
  if (config.serviceVersion) {
1022
1212
  attributes[ATTR_SERVICE_VERSION] = config.serviceVersion;
1023
- if (/^[0-9a-f]{7,40}$/i.test(config.serviceVersion)) attributes["vcs.ref.head.revision"] = config.serviceVersion;
1213
+ if (isCommitSha(config.serviceVersion)) attributes["vcs.ref.head.revision"] = config.serviceVersion;
1024
1214
  }
1025
1215
  if (config.environment) {
1026
1216
  attributes["deployment.environment"] = config.environment;
@@ -1029,6 +1219,13 @@ function resourceAttributes(config) {
1029
1219
  return attributes;
1030
1220
  }
1031
1221
  /**
1222
+ * A full 40-hex SHA, or an abbreviated one (7 to 39 hex) with at least one
1223
+ * letter: an all-digit version like a date build number is not a commit.
1224
+ */
1225
+ function isCommitSha(value) {
1226
+ return /^[0-9a-f]{40}$/i.test(value) || /^[0-9a-f]{7,39}$/i.test(value) && /[a-f]/i.test(value);
1227
+ }
1228
+ /**
1032
1229
  * Set up browser OTel tracing exporting to Maple's ingest. When
1033
1230
  * `tracingInstrumentFetch` is true, fetch() calls are auto-instrumented and
1034
1231
  * their trace ids feed the session. Disable it when an external tracer (e.g.
@@ -1048,18 +1245,6 @@ function setupTracing(config) {
1048
1245
  sdk: sdkHint(SDK_NAME, SDK_VERSION)
1049
1246
  }), JsonTraceSerializer);
1050
1247
  const exporter = new ConsentSpanExporter(new HttpStatusExporter(newestFirstOnExit(config.offlineQueue ? new OfflineSpanExporter(otlp) : otlp, JsonTraceSerializer, EXIT_LOGS_RESERVE_BYTES), config.errorFilters.captureHttpStatus));
1051
- const provider = new WebTracerProvider({
1052
- resource: resourceFromAttributes(resourceAttributes(config)),
1053
- sampler: new SessionSampler(config.tracingSampleRate),
1054
- spanProcessors: [new TraceIdCollector(() => config.identity?.id), new BatchSpanProcessor(exporter, { scheduledDelayMillis: EXPORT_INTERVAL_MS })]
1055
- });
1056
- provider.register();
1057
- mapleProvider = provider;
1058
- const globalProvider = trace.getTracerProvider();
1059
- const ownsGlobals = globalProvider === provider || globalProvider instanceof ProxyTracerProvider && globalProvider.getDelegate() === provider;
1060
- const onExit = () => {
1061
- provider.forceFlush().catch(() => {});
1062
- };
1063
1248
  let connectionLosses = 0;
1064
1249
  let lastLossReason = "";
1065
1250
  const lossesAtStart = /* @__PURE__ */ new WeakMap();
@@ -1069,10 +1254,39 @@ function setupTracing(config) {
1069
1254
  };
1070
1255
  const connectionLost = (span) => {
1071
1256
  if (typeof navigator !== "undefined" && navigator.onLine === false) return "offline";
1072
- if (typeof document !== "undefined" && document.visibilityState === "hidden") return "page_hidden";
1073
1257
  const atStart = lossesAtStart.get(span);
1074
1258
  return atStart !== void 0 && atStart !== connectionLosses ? lastLossReason : void 0;
1075
1259
  };
1260
+ const lostXhrSpans = /* @__PURE__ */ new WeakSet();
1261
+ const provider = new WebTracerProvider({
1262
+ resource: resourceFromAttributes(resourceAttributes(config)),
1263
+ sampler: new SessionSampler(config.tracingSampleRate),
1264
+ spanProcessors: [
1265
+ new TraceIdCollector(() => config.identity?.id),
1266
+ {
1267
+ onStart: (span) => {
1268
+ lossesAtStart.set(span, connectionLosses);
1269
+ },
1270
+ onEnding: (span) => {
1271
+ if (!lostXhrSpans.has(span)) return;
1272
+ span.status.code = SpanStatusCode.UNSET;
1273
+ delete span.status.message;
1274
+ delete span.attributes["error.type"];
1275
+ },
1276
+ onEnd: () => {},
1277
+ forceFlush: () => Promise.resolve(),
1278
+ shutdown: () => Promise.resolve()
1279
+ },
1280
+ new BatchSpanProcessor(exporter, { scheduledDelayMillis: EXPORT_INTERVAL_MS })
1281
+ ]
1282
+ });
1283
+ provider.register();
1284
+ mapleProvider = provider;
1285
+ const globalProvider = trace.getTracerProvider();
1286
+ const ownsGlobals = globalProvider === provider || globalProvider instanceof ProxyTracerProvider && globalProvider.getDelegate() === provider;
1287
+ const onExit = () => {
1288
+ provider.forceFlush().catch(() => {});
1289
+ };
1076
1290
  const onOffline = () => noteLost("offline");
1077
1291
  const onVisibilityChange = () => {
1078
1292
  if (document.visibilityState !== "hidden") return;
@@ -1105,9 +1319,6 @@ function setupTracing(config) {
1105
1319
  const headers = config.captureHeaders;
1106
1320
  const instrumentations = [...config.tracingInstrumentFetch ? [new FetchInstrumentation({
1107
1321
  ...requestOptions,
1108
- requestHook: (span) => {
1109
- lossesAtStart.set(span, connectionLosses);
1110
- },
1111
1322
  applyCustomAttributesOnSpan: (span, request, result) => {
1112
1323
  noteSettled(span);
1113
1324
  const sent = request instanceof Request ? request.headers : new Headers(request.headers);
@@ -1129,6 +1340,11 @@ function setupTracing(config) {
1129
1340
  ...requestOptions,
1130
1341
  applyCustomAttributesOnSpan: (span, xhr) => {
1131
1342
  noteSettled(span);
1343
+ const lost = xhr.status === 0 ? connectionLost(span) : void 0;
1344
+ if (lost !== void 0) {
1345
+ span.setAttribute("maple.connection_lost", lost);
1346
+ lostXhrSpans.add(span);
1347
+ }
1132
1348
  if (headers.response.length === 0) return;
1133
1349
  const exposed = responseHeaders(xhr.getAllResponseHeaders());
1134
1350
  setHeaderAttributes(span, "response", headers.response, (name) => exposed.get(name));
@@ -1146,12 +1362,12 @@ function setupTracing(config) {
1146
1362
  }
1147
1363
  unregisterInstrumentations?.();
1148
1364
  if (mapleProvider === provider) mapleProvider = void 0;
1149
- await provider.shutdown();
1150
1365
  if (ownsGlobals) {
1151
1366
  trace.disable();
1152
1367
  context.disable();
1153
1368
  propagation.disable();
1154
1369
  }
1370
+ await provider.shutdown();
1155
1371
  };
1156
1372
  }
1157
1373
  /** `AbortSignal.timeout()` aborts with, and rejects the fetch with, a `TimeoutError` DOMException. */
@@ -1178,8 +1394,7 @@ const alreadyReported = wasReported;
1178
1394
  * claiming it then would swallow the same error reported again once tracing is
1179
1395
  * live. Without consent the span is never exported, so the error is not claimed either.
1180
1396
  */
1181
- function recordFailure(span, error) {
1182
- const normalized = asError(error);
1397
+ function recordFailure(span, error, normalized = asError(error)) {
1183
1398
  if (!alreadyReported(error)) {
1184
1399
  const exported = span.isRecording() && hasConsent();
1185
1400
  if (exported) markReported(error);
@@ -1193,33 +1408,71 @@ function recordFailure(span, error) {
1193
1408
  }
1194
1409
  /**
1195
1410
  * Record `error` on a one-off span, exported whatever the session's trace
1196
- * sampling, unless the app's error filters drop it.
1411
+ * sampling, unless the app's error filters drop it. Without tracing there is no
1412
+ * span, but a buffered replay still keeps itself for the error.
1197
1413
  */
1198
- function recordException(error, options, source, filename, filtered = false) {
1414
+ function recordException(error, options, record) {
1199
1415
  if (!hasConsent()) return;
1200
- if (!filtered && !shouldCapture(asError(error), {
1201
- source,
1416
+ const normalized = record.normalized ?? asError(error);
1417
+ if (!record.filtered && !shouldCapture(normalized, {
1418
+ source: record.source,
1202
1419
  originalError: error
1203
- }, filename)) return;
1420
+ }, record.filename)) return;
1204
1421
  const tracer = liveMapleTracer(SDK_NAME, SDK_VERSION);
1205
- if (!tracer) return;
1422
+ if (!tracer) {
1423
+ notifyErrorRecorded(INVALID_SPAN_CONTEXT);
1424
+ return;
1425
+ }
1206
1426
  const span = tracer.startSpan(options.name ?? "exception", {
1207
1427
  kind: SpanKind.INTERNAL,
1428
+ startTime: record.startTime,
1208
1429
  attributes: {
1209
1430
  ...typeof location !== "undefined" ? { "url.full": scrubUrl(location.href) } : void 0,
1210
1431
  ...options.attributes
1211
1432
  }
1212
1433
  }, keepContext(context.active()));
1213
- recordFailure(span, error);
1434
+ recordFailure(span, error, normalized);
1214
1435
  span.end();
1215
1436
  }
1437
+ /** Held until `init()` starts capture; beyond this, the oldest are dropped. */
1438
+ const MAX_PENDING = 30;
1439
+ let accepting = false;
1440
+ let pending = [];
1216
1441
  /**
1217
- * Record an error that no span was watching. Safe before `init()`, where it
1218
- * does nothing. Reporting the same error object twice records it once.
1442
+ * Record an error that no span was watching. Safe before `init()`: the call is
1443
+ * held (capped) and recorded when it runs. Reporting the same error object
1444
+ * twice records it once.
1219
1445
  */
1220
1446
  function captureException(error, options = {}) {
1447
+ if (typeof window === "undefined") return;
1448
+ if (!accepting && !liveMapleTracer("maple-browser", "0.13.0")) {
1449
+ pending.push({
1450
+ error,
1451
+ options,
1452
+ at: Date.now()
1453
+ });
1454
+ if (pending.length > MAX_PENDING) pending.shift();
1455
+ return;
1456
+ }
1221
1457
  if (alreadyReported(error)) return;
1222
- recordException(error, options, "captureException");
1458
+ recordException(error, options, { source: "captureException" });
1459
+ }
1460
+ /** Called by `init()` once tracing (if any) is up: records what was held, then records directly. */
1461
+ function openExceptionCapture() {
1462
+ accepting = true;
1463
+ const held = pending;
1464
+ pending = [];
1465
+ for (const { error, options, at } of held) {
1466
+ if (alreadyReported(error)) continue;
1467
+ recordException(error, options, {
1468
+ source: "captureException",
1469
+ startTime: at
1470
+ });
1471
+ }
1472
+ }
1473
+ /** Called by `shutdown()`: later calls are held for the next `init()`. */
1474
+ function closeExceptionCapture() {
1475
+ accepting = false;
1223
1476
  }
1224
1477
  /** Whether the app's error filters keep `error`, for failures recorded on an existing span. */
1225
1478
  function passesErrorFilters(error) {
@@ -1231,33 +1484,47 @@ function passesErrorFilters(error) {
1231
1484
  /** `captureException` for an error `passesErrorFilters` already let through: `beforeCapture` runs once. */
1232
1485
  function captureFilteredException(error, options = {}) {
1233
1486
  if (alreadyReported(error)) return;
1234
- recordException(error, options, "captureException", void 0, true);
1487
+ recordException(error, options, {
1488
+ source: "captureException",
1489
+ filtered: true
1490
+ });
1235
1491
  }
1236
1492
  /**
1237
1493
  * Register global handlers for uncaught errors and unhandled rejections.
1238
- * Returns a teardown that removes them.
1494
+ * Returns a teardown that removes them. With `record: false` (tracing or
1495
+ * `captureErrors` off) they record nothing and only tell the replay an error happened.
1239
1496
  */
1240
- function setupErrorCapture() {
1497
+ function setupErrorCapture(record = true) {
1241
1498
  if (typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {};
1499
+ const handle = (error, options, recordOptions) => {
1500
+ if (record) recordException(error, options, recordOptions);
1501
+ else notifyUnrecorded(error, recordOptions);
1502
+ };
1242
1503
  const onError = (event) => {
1243
- const error = event.error ?? (event.message && event.filename ? new Error(event.message) : void 0);
1504
+ const error = event.error ?? (event.message && event.filename ? event.message : void 0);
1244
1505
  if (error === void 0 || alreadyReported(error)) return;
1245
- recordException(error, {
1506
+ const thrownError = event.error instanceof Error;
1507
+ const filename = event.filename ? scrubUrl(event.filename) : void 0;
1508
+ handle(error, {
1246
1509
  name: "browser.uncaught_error",
1247
1510
  attributes: {
1248
1511
  "maple.exception.source": "window.onerror",
1249
- ...event.filename ? { "code.file.path": event.filename } : void 0,
1512
+ ...filename ? { "code.file.path": filename } : void 0,
1250
1513
  ...event.lineno ? { "code.line.number": event.lineno } : void 0,
1251
1514
  ...event.colno ? { "code.column.number": event.colno } : void 0
1252
1515
  }
1253
- }, "window.onerror", event.error instanceof Error ? void 0 : event.filename || void 0);
1516
+ }, {
1517
+ source: "window.onerror",
1518
+ filename: thrownError ? void 0 : event.filename || void 0,
1519
+ normalized: thrownError ? void 0 : withThrowSite(asError(error), filename, event)
1520
+ });
1254
1521
  };
1255
1522
  const onUnhandledRejection = (event) => {
1256
1523
  if (alreadyReported(event.reason)) return;
1257
- recordException(event.reason, {
1524
+ handle(event.reason, {
1258
1525
  name: "browser.unhandled_rejection",
1259
1526
  attributes: { "maple.exception.source": "unhandledrejection" }
1260
- }, "unhandledrejection");
1527
+ }, { source: "unhandledrejection" });
1261
1528
  };
1262
1529
  window.addEventListener("error", onError);
1263
1530
  window.addEventListener("unhandledrejection", onUnhandledRejection);
@@ -1266,6 +1533,35 @@ function setupErrorCapture() {
1266
1533
  window.removeEventListener("unhandledrejection", onUnhandledRejection);
1267
1534
  };
1268
1535
  }
1536
+ /** An error the app wants no span for still keeps a buffered replay, if the filters keep it. */
1537
+ function notifyUnrecorded(error, record) {
1538
+ if (!hasConsent()) return;
1539
+ const normalized = record.normalized ?? asError(error);
1540
+ if (!shouldCapture(normalized, {
1541
+ source: record.source,
1542
+ originalError: error
1543
+ }, record.filename)) return;
1544
+ notifyErrorRecorded(INVALID_SPAN_CONTEXT);
1545
+ }
1546
+ /**
1547
+ * A thrown non-Error has no frames of its own, only the event's location. One
1548
+ * V8-style frame there makes it groupable by where it was thrown.
1549
+ */
1550
+ function withThrowSite(error, filename, event) {
1551
+ if (!filename || error.stack !== void 0) return error;
1552
+ const position = [
1553
+ filename,
1554
+ event.lineno || void 0,
1555
+ event.colno || void 0
1556
+ ].filter((part) => part !== void 0).join(":");
1557
+ const header = error.message ? `${error.name}: ${error.message}` : error.name;
1558
+ Object.defineProperty(error, "stack", {
1559
+ value: `${header}\n at ${position}`,
1560
+ writable: true,
1561
+ configurable: true
1562
+ });
1563
+ return error;
1564
+ }
1269
1565
  //#endregion
1270
1566
  //#region src/config.ts
1271
1567
  /**
@@ -1275,7 +1571,14 @@ function setupErrorCapture() {
1275
1571
  function resolveIdentity(config) {
1276
1572
  return normalizeIdentity(config.user ?? config.userId);
1277
1573
  }
1574
+ let warnedServiceName = false;
1278
1575
  function resolveConfig(config) {
1576
+ if (typeof config.serviceName !== "string" || config.serviceName.trim() === "") {
1577
+ if (!warnedServiceName) {
1578
+ warnedServiceName = true;
1579
+ console.warn("[maple] init() needs a `serviceName`; telemetry is reported without one. Pass the name of your app.");
1580
+ }
1581
+ }
1279
1582
  const endpoint = resolveIngestEndpoint({
1280
1583
  endpoints: [config.endpoint],
1281
1584
  regions: [config.region]
@@ -1290,7 +1593,7 @@ function resolveConfig(config) {
1290
1593
  return {
1291
1594
  ...resolveSignalOptions(config),
1292
1595
  ingestKey: config.ingestKey,
1293
- serviceName: config.serviceName,
1596
+ serviceName: typeof config.serviceName === "string" ? config.serviceName : "",
1294
1597
  endpoint,
1295
1598
  serviceNamespace: config.serviceNamespace,
1296
1599
  serviceVersion: config.serviceVersion,
@@ -1520,6 +1823,9 @@ function parseTraceparent(value) {
1520
1823
  /** `x-maple-sdk` value for every request this build makes to ingest. */
1521
1824
  const SDK_HINT = sdkHint(SDK_NAME, SDK_VERSION);
1522
1825
  let active;
1826
+ /** What the live handle was made from, to tell a repeated `init()` from a reconfiguration. */
1827
+ let activeRawConfig;
1828
+ let warnedReinit = false;
1523
1829
  let activeConfig;
1524
1830
  /**
1525
1831
  * An `identify()` made before `init()`, applied when it runs. Auth callbacks
@@ -1529,12 +1835,42 @@ let activeConfig;
1529
1835
  */
1530
1836
  let pendingIdentity;
1531
1837
  /**
1838
+ * The flushing half of every `shutdown()` still running. A new `init()` starts
1839
+ * its session and deferred signals behind it: the session sink, replay and log
1840
+ * pipeline are page-wide, and the old teardown would otherwise stop the new ones.
1841
+ */
1842
+ let teardown;
1843
+ /** The longest a new `init()` waits for the previous handle's final flush. */
1844
+ const TEARDOWN_WAIT_MS = 3e3;
1845
+ /** Run one teardown step; a failure must not leave the rest of the SDK half torn down. */
1846
+ async function attempt(step) {
1847
+ try {
1848
+ await step();
1849
+ } catch {}
1850
+ }
1851
+ /** Same settings, by value: a re-render passing a fresh but equal object is not a reconfiguration. */
1852
+ function sameConfig(a, b) {
1853
+ if (a === b) return true;
1854
+ try {
1855
+ return JSON.stringify(a) === JSON.stringify(b);
1856
+ } catch {
1857
+ return false;
1858
+ }
1859
+ }
1860
+ /**
1532
1861
  * Initialize Maple browser telemetry. With consent gating enabled the returned
1533
1862
  * handle remains live while denied: granting starts capture, revoking detaches
1534
- * it without flushing, and a later grant starts cleanly again.
1863
+ * it without flushing, and a later grant starts cleanly again. While a handle
1864
+ * is live, `init()` returns it and ignores the new config.
1535
1865
  */
1536
1866
  function init(rawConfig) {
1537
- if (active) return active;
1867
+ if (active) {
1868
+ if (activeRawConfig && !warnedReinit && !sameConfig(activeRawConfig, rawConfig)) {
1869
+ warnedReinit = true;
1870
+ console.warn("[maple] init() was called again with a different config while the SDK is running; the new config is ignored. Call shutdown() on the first handle before re-initializing.");
1871
+ }
1872
+ return active;
1873
+ }
1538
1874
  if (typeof window === "undefined") return {
1539
1875
  sessionId: "",
1540
1876
  shutdown: () => Promise.resolve()
@@ -1543,11 +1879,15 @@ function init(rawConfig) {
1543
1879
  if (pendingIdentity) config.identity = normalizeIdentity(pendingIdentity.input);
1544
1880
  pendingIdentity = void 0;
1545
1881
  activeConfig = config;
1546
- configurePrivacy(config);
1882
+ activeRawConfig = rawConfig;
1883
+ const removeSanitizer = configurePrivacy(config);
1547
1884
  configureErrorFilters(config.errorFilters);
1548
1885
  if (!hasConsent()) clearPendingEvents();
1549
1886
  setActiveTraceIdProvider(() => trace.getActiveSpan()?.spanContext().traceId);
1550
1887
  const replayEligible = config.replayEnabled && !isLikelyBot(navigator.userAgent);
1888
+ const recordErrors = config.tracingEnabled && config.tracingCaptureErrors;
1889
+ const behind = teardown && Promise.race([teardown, new Promise((resolve) => setTimeout(resolve, TEARDOWN_WAIT_MS))]);
1890
+ let ready = behind === void 0;
1551
1891
  let runtime;
1552
1892
  let stopped = false;
1553
1893
  let rotateOnNextStart = false;
@@ -1556,8 +1896,14 @@ function init(rawConfig) {
1556
1896
  let deferredPending;
1557
1897
  let stopDeferred;
1558
1898
  let generation = 0;
1899
+ const startTelemetry = () => {
1900
+ if (stopped || !hasConsent()) return;
1901
+ if (config.tracingEnabled && !shutdownTracing) shutdownTracing = setupTracing(config);
1902
+ if (!stopErrorCapture && (recordErrors || replayEligible && config.replayOnErrorSampleRate > 0)) stopErrorCapture = setupErrorCapture(recordErrors);
1903
+ };
1559
1904
  const startRuntime = () => {
1560
- if (stopped || runtime || !hasConsent()) return;
1905
+ startTelemetry();
1906
+ if (stopped || !ready || runtime || !hasConsent()) return;
1561
1907
  setVisitorTracking(config.persistVisitorId && mayPersistIdentifier());
1562
1908
  const session = (rotateOnNextStart ? rotateSession() : void 0) ?? getSession();
1563
1909
  rotateOnNextStart = false;
@@ -1569,13 +1915,11 @@ function init(rawConfig) {
1569
1915
  sdk: SDK_HINT,
1570
1916
  maskAllInputs: config.maskAllInputs,
1571
1917
  maskAllText: config.maskAllText,
1572
- getIdentity: () => activeConfig?.identity
1918
+ getIdentity: () => config.identity
1573
1919
  }, session.id);
1574
- if (config.tracingEnabled && !shutdownTracing) shutdownTracing = setupTracing(config);
1575
- if (config.tracingEnabled && config.tracingCaptureErrors && !stopErrorCapture) stopErrorCapture = setupErrorCapture();
1576
1920
  if (!deferredPending) {
1577
- setLogIdentity(() => activeConfig?.identity?.id);
1578
- deferredPending = import("./deferred-BDKAWiKZ.mjs").then(({ startDeferred }) => {
1921
+ setLogIdentity(() => config.identity?.id);
1922
+ deferredPending = import("./deferred-Dr2AmxSU.mjs").then(({ startDeferred }) => {
1579
1923
  stopDeferred = startDeferred(config);
1580
1924
  }).catch(() => {});
1581
1925
  }
@@ -1586,7 +1930,7 @@ function init(rawConfig) {
1586
1930
  serviceName: config.serviceName,
1587
1931
  environment: config.environment,
1588
1932
  serviceVersion: config.serviceVersion,
1589
- getIdentity: () => activeConfig?.identity,
1933
+ getIdentity: () => config.identity,
1590
1934
  captureUserEmail: config.captureUserEmail
1591
1935
  };
1592
1936
  const startMetadata = () => startMetadataSession({
@@ -1609,7 +1953,7 @@ function init(rawConfig) {
1609
1953
  runtime = next;
1610
1954
  const ownGeneration = ++generation;
1611
1955
  const stale = () => stopped || !hasConsent() || generation !== ownGeneration || runtime !== next;
1612
- next.replayPending = import("./replay-session-Cg3U1RS0.mjs").then(({ startReplaySession }) => {
1956
+ next.replayPending = import("./replay-session-BuUOwHnI.mjs").then(({ startReplaySession }) => {
1613
1957
  if (stale()) return;
1614
1958
  next.replay = startReplaySession({
1615
1959
  ...shared,
@@ -1634,11 +1978,12 @@ function init(rawConfig) {
1634
1978
  const currentSessionId = previous.replay?.sessionId ?? previous.metadata?.sessionId;
1635
1979
  const liveSink = getActiveSink();
1636
1980
  const sink = liveSink && currentSessionId && liveSink.sessionId === currentSessionId ? liveSink : previous.sink;
1637
- if (flush) await sink.flush(true);
1981
+ const flushed = flush ? sink.flush(true) : void 0;
1638
1982
  sink.stop();
1639
1983
  if (sink !== previous.sink) previous.sink.stop();
1640
1984
  clearSessionSink(currentSessionId ?? previous.initialSessionId);
1641
- await Promise.all([
1985
+ await flushed;
1986
+ await Promise.allSettled([
1642
1987
  replayShutdown,
1643
1988
  metadataShutdown,
1644
1989
  previous.replayPending
@@ -1648,7 +1993,12 @@ function init(rawConfig) {
1648
1993
  runtime?.replay?.trigger();
1649
1994
  });
1650
1995
  startRuntime();
1651
- const stopConsentListener = config.requireConsent ? onConsentChange((allowed) => {
1996
+ openExceptionCapture();
1997
+ if (!ready) behind?.then(() => {
1998
+ ready = true;
1999
+ startRuntime();
2000
+ });
2001
+ const stopConsentListener = onConsentChange((allowed) => {
1652
2002
  if (allowed) {
1653
2003
  startRuntime();
1654
2004
  return;
@@ -1658,29 +2008,45 @@ function init(rawConfig) {
1658
2008
  clearPendingChunk();
1659
2009
  setVisitorTracking(false);
1660
2010
  stopRuntime(false);
1661
- }) : () => {};
2011
+ });
1662
2012
  const handle = {
1663
2013
  get sessionId() {
1664
2014
  return runtime?.replay?.sessionId ?? runtime?.metadata?.sessionId ?? runtime?.initialSessionId ?? "";
1665
2015
  },
1666
- shutdown: async () => {
1667
- if (stopped) return;
2016
+ shutdown: () => {
2017
+ if (stopped) return Promise.resolve();
1668
2018
  stopped = true;
2019
+ if (active === handle) {
2020
+ active = void 0;
2021
+ activeRawConfig = void 0;
2022
+ activeConfig = void 0;
2023
+ }
1669
2024
  stopConsentListener();
1670
2025
  stopReplayTrigger();
1671
- await stopRuntime(true);
2026
+ closeExceptionCapture();
1672
2027
  stopErrorCapture?.();
1673
2028
  stopErrorCapture = void 0;
1674
2029
  configureErrorFilters(void 0);
2030
+ removeSanitizer();
2031
+ setActiveTraceIdProvider(() => void 0);
1675
2032
  resetNavigation();
1676
- await deferredPending;
1677
- await shutdownTracing?.();
2033
+ const runtimeStopped = attempt(() => stopRuntime(true));
2034
+ const tracingStopped = attempt(() => shutdownTracing?.());
1678
2035
  shutdownTracing = void 0;
1679
- await stopDeferred?.();
1680
- stopDeferred = void 0;
1681
- setActiveTraceIdProvider(() => void 0);
1682
- active = void 0;
1683
- activeConfig = void 0;
2036
+ const flushed = (async () => {
2037
+ await attempt(() => runtimeStopped);
2038
+ await attempt(() => deferredPending);
2039
+ await attempt(() => tracingStopped);
2040
+ await attempt(() => stopDeferred?.());
2041
+ stopDeferred = void 0;
2042
+ if (active === void 0) attachSpanStash(void 0);
2043
+ })();
2044
+ const all = Promise.all([behind, flushed]).then(() => {});
2045
+ teardown = all;
2046
+ all.finally(() => {
2047
+ if (teardown === all) teardown = void 0;
2048
+ });
2049
+ return all;
1684
2050
  }
1685
2051
  };
1686
2052
  active = handle;
@@ -1763,4 +2129,4 @@ const MapleBrowser = {
1763
2129
  logger
1764
2130
  };
1765
2131
  //#endregion
1766
- export { severityOf as _, detachLogSink as a, resourceAttributes as c, OtlpExporter as d, newestFirstOnExit as f, Severity as g, onErrorRecorded as h, attachLogSink as i, SDK_NAME as l, claimPageSignal as m, navigationSpanAt as n, emitLog as o, attachSpanStash as p, onDocumentPageload as r, liveMapleTracer as s, MapleBrowser as t, SDK_VERSION as u, PENDING_CHUNK_KEY as v, clearPendingChunk as y };
2132
+ export { Severity as _, attachLogSink as a, clearPendingChunk as b, liveMapleTracer as c, SDK_VERSION as d, OtlpExporter as f, onErrorRecorded as g, claimPageSignal as h, onDocumentPageload as i, resourceAttributes as l, attachSpanStash as m, interruptNavigation as n, detachLogSink as o, newestFirstOnExit as p, navigationSpanAt as r, emitLog as s, MapleBrowser as t, SDK_NAME as u, severityOf as v, PENDING_CHUNK_KEY as y };