@maple-dev/browser 0.12.0 → 0.14.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,13 +1,118 @@
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";
3
- import { BatchSpanProcessor, SamplingDecision } from "@opentelemetry/sdk-trace-base";
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
+ import { BatchSpanProcessor, SamplingDecision, StackContextManager, TracerProvider } from "@opentelemetry/sdk-trace";
4
+ import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from "@opentelemetry/core";
4
5
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
5
6
  import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
6
7
  import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xml-http-request";
7
8
  import { JsonTraceSerializer } from "@opentelemetry/otlp-transformer";
8
9
  import { resourceFromAttributes } from "@opentelemetry/resources";
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 };
@@ -725,7 +907,10 @@ var HttpStatusExporter = class {
725
907
  this.captureStatus = captureStatus;
726
908
  }
727
909
  apply(span) {
728
- const read = (key) => span.attributes[key];
910
+ const read = (key) => {
911
+ const value = span.attributes[key];
912
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : void 0;
913
+ };
729
914
  const status = responseStatus(read);
730
915
  if (span.kind === SpanKind.CLIENT && status !== void 0 && inStatusRanges(status, this.captureStatus)) {
731
916
  if (span.events.some((event) => event.name === "exception")) return span;
@@ -754,9 +939,13 @@ var HttpStatusExporter = class {
754
939
  const MAX_HELD = 20;
755
940
  let stash;
756
941
  let held = [];
942
+ /** Attach the deferred chunk's stash, or detach it on shutdown, dropping what is held: it belongs to that session. */
757
943
  function attachSpanStash(next) {
758
944
  stash = next;
759
- if (!next) return;
945
+ if (!next) {
946
+ held = [];
947
+ return;
948
+ }
760
949
  const pending = held;
761
950
  held = [];
762
951
  for (const spans of pending) next(spans);
@@ -895,13 +1084,14 @@ function newestFirst(items, serializer) {
895
1084
  * newest items first, in a body small enough for keepalive, then the rest,
896
1085
  * which keeps keepalive while it fits and leaves `olderReserve` bytes of room
897
1086
  * for what is flushed after it. Each goes through `inner` on its own, so only
898
- * the part that failed is queued.
1087
+ * the part that failed is queued. `leaving` covers a pipeline whose exit flush
1088
+ * exports after `flushUnloading` has returned.
899
1089
  */
900
- function newestFirstOnExit(inner, serializer, olderReserve = 0) {
1090
+ function newestFirstOnExit(inner, serializer, olderReserve = 0, leaving = () => false) {
901
1091
  return {
902
1092
  export(items, callback) {
903
1093
  const hidden = globalThis.document?.visibilityState === "hidden";
904
- const parts = unloading || hidden ? newestFirst(items, serializer) : [items];
1094
+ const parts = unloading || hidden || leaving() ? newestFirst(items, serializer) : [items];
905
1095
  let pending = parts.length;
906
1096
  let failed;
907
1097
  for (const [index, part] of parts.entries()) {
@@ -923,7 +1113,7 @@ function newestFirstOnExit(inner, serializer, olderReserve = 0) {
923
1113
  }
924
1114
  //#endregion
925
1115
  //#region src/version.ts
926
- const SDK_VERSION = "0.12.0";
1116
+ const SDK_VERSION = "0.14.0";
927
1117
  /** The `x-maple-sdk` value this build sends. */
928
1118
  const SDK_NAME = "maple-browser";
929
1119
  //#endregion
@@ -941,7 +1131,7 @@ var TraceIdCollector = class {
941
1131
  this.getUserId = getUserId;
942
1132
  }
943
1133
  onStart(span) {
944
- for (const key of URL_ATTRIBUTES) {
1134
+ if (span.instrumentationScope?.name !== "maple-browser") for (const key of URL_ATTRIBUTES) {
945
1135
  const value = span.attributes[key];
946
1136
  if (typeof value === "string") span.setAttribute(key, scrubUrl(value));
947
1137
  }
@@ -1015,12 +1205,15 @@ function liveMapleTracer(name, version) {
1015
1205
  function resourceAttributes(config) {
1016
1206
  const attributes = {
1017
1207
  [ATTR_SERVICE_NAME]: config.serviceName,
1208
+ [ATTR_TELEMETRY_SDK_NAME]: "@maple-dev/browser",
1209
+ [ATTR_TELEMETRY_SDK_LANGUAGE]: TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS,
1210
+ [ATTR_TELEMETRY_SDK_VERSION]: SDK_VERSION,
1018
1211
  "maple.sdk.type": "browser"
1019
1212
  };
1020
1213
  if (config.serviceNamespace) attributes["service.namespace"] = config.serviceNamespace;
1021
1214
  if (config.serviceVersion) {
1022
1215
  attributes[ATTR_SERVICE_VERSION] = config.serviceVersion;
1023
- if (/^[0-9a-f]{7,40}$/i.test(config.serviceVersion)) attributes["vcs.ref.head.revision"] = config.serviceVersion;
1216
+ if (isCommitSha(config.serviceVersion)) attributes["vcs.ref.head.revision"] = config.serviceVersion;
1024
1217
  }
1025
1218
  if (config.environment) {
1026
1219
  attributes["deployment.environment"] = config.environment;
@@ -1029,6 +1222,13 @@ function resourceAttributes(config) {
1029
1222
  return attributes;
1030
1223
  }
1031
1224
  /**
1225
+ * A full 40-hex SHA, or an abbreviated one (7 to 39 hex) with at least one
1226
+ * letter: an all-digit version like a date build number is not a commit.
1227
+ */
1228
+ function isCommitSha(value) {
1229
+ return /^[0-9a-f]{40}$/i.test(value) || /^[0-9a-f]{7,39}$/i.test(value) && /[a-f]/i.test(value);
1230
+ }
1231
+ /**
1032
1232
  * Set up browser OTel tracing exporting to Maple's ingest. When
1033
1233
  * `tracingInstrumentFetch` is true, fetch() calls are auto-instrumented and
1034
1234
  * their trace ids feed the session. Disable it when an external tracer (e.g.
@@ -1048,18 +1248,6 @@ function setupTracing(config) {
1048
1248
  sdk: sdkHint(SDK_NAME, SDK_VERSION)
1049
1249
  }), JsonTraceSerializer);
1050
1250
  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
1251
  let connectionLosses = 0;
1064
1252
  let lastLossReason = "";
1065
1253
  const lossesAtStart = /* @__PURE__ */ new WeakMap();
@@ -1069,10 +1257,44 @@ function setupTracing(config) {
1069
1257
  };
1070
1258
  const connectionLost = (span) => {
1071
1259
  if (typeof navigator !== "undefined" && navigator.onLine === false) return "offline";
1072
- if (typeof document !== "undefined" && document.visibilityState === "hidden") return "page_hidden";
1073
1260
  const atStart = lossesAtStart.get(span);
1074
1261
  return atStart !== void 0 && atStart !== connectionLosses ? lastLossReason : void 0;
1075
1262
  };
1263
+ const lostXhrSpans = /* @__PURE__ */ new WeakSet();
1264
+ const provider = new TracerProvider({
1265
+ resource: resourceFromAttributes(resourceAttributes(config)),
1266
+ sampler: new SessionSampler(config.tracingSampleRate),
1267
+ spanProcessors: [
1268
+ new TraceIdCollector(() => config.identity?.id),
1269
+ {
1270
+ onStart: (span) => {
1271
+ lossesAtStart.set(span, connectionLosses);
1272
+ },
1273
+ onEnding: (span) => {
1274
+ if (!lostXhrSpans.has(span)) return;
1275
+ span.status.code = SpanStatusCode.UNSET;
1276
+ delete span.status.message;
1277
+ delete span.attributes["error.type"];
1278
+ },
1279
+ onEnd: () => {},
1280
+ forceFlush: () => Promise.resolve(),
1281
+ shutdown: () => Promise.resolve()
1282
+ },
1283
+ new BatchSpanProcessor({
1284
+ exporter,
1285
+ scheduledDelayMillis: EXPORT_INTERVAL_MS
1286
+ })
1287
+ ]
1288
+ });
1289
+ trace.setGlobalTracerProvider(provider);
1290
+ propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }));
1291
+ context.setGlobalContextManager(new StackContextManager().enable());
1292
+ mapleProvider = provider;
1293
+ const globalProvider = trace.getTracerProvider();
1294
+ const ownsGlobals = globalProvider === provider || globalProvider instanceof ProxyTracerProvider && globalProvider.getDelegate() === provider;
1295
+ const onExit = () => {
1296
+ provider.forceFlush().catch(() => {});
1297
+ };
1076
1298
  const onOffline = () => noteLost("offline");
1077
1299
  const onVisibilityChange = () => {
1078
1300
  if (document.visibilityState !== "hidden") return;
@@ -1105,9 +1327,6 @@ function setupTracing(config) {
1105
1327
  const headers = config.captureHeaders;
1106
1328
  const instrumentations = [...config.tracingInstrumentFetch ? [new FetchInstrumentation({
1107
1329
  ...requestOptions,
1108
- requestHook: (span) => {
1109
- lossesAtStart.set(span, connectionLosses);
1110
- },
1111
1330
  applyCustomAttributesOnSpan: (span, request, result) => {
1112
1331
  noteSettled(span);
1113
1332
  const sent = request instanceof Request ? request.headers : new Headers(request.headers);
@@ -1129,6 +1348,11 @@ function setupTracing(config) {
1129
1348
  ...requestOptions,
1130
1349
  applyCustomAttributesOnSpan: (span, xhr) => {
1131
1350
  noteSettled(span);
1351
+ const lost = xhr.status === 0 ? connectionLost(span) : void 0;
1352
+ if (lost !== void 0) {
1353
+ span.setAttribute("maple.connection_lost", lost);
1354
+ lostXhrSpans.add(span);
1355
+ }
1132
1356
  if (headers.response.length === 0) return;
1133
1357
  const exposed = responseHeaders(xhr.getAllResponseHeaders());
1134
1358
  setHeaderAttributes(span, "response", headers.response, (name) => exposed.get(name));
@@ -1146,12 +1370,12 @@ function setupTracing(config) {
1146
1370
  }
1147
1371
  unregisterInstrumentations?.();
1148
1372
  if (mapleProvider === provider) mapleProvider = void 0;
1149
- await provider.shutdown();
1150
1373
  if (ownsGlobals) {
1151
1374
  trace.disable();
1152
1375
  context.disable();
1153
1376
  propagation.disable();
1154
1377
  }
1378
+ await provider.shutdown();
1155
1379
  };
1156
1380
  }
1157
1381
  /** `AbortSignal.timeout()` aborts with, and rejects the fetch with, a `TimeoutError` DOMException. */
@@ -1178,8 +1402,7 @@ const alreadyReported = wasReported;
1178
1402
  * claiming it then would swallow the same error reported again once tracing is
1179
1403
  * live. Without consent the span is never exported, so the error is not claimed either.
1180
1404
  */
1181
- function recordFailure(span, error) {
1182
- const normalized = asError(error);
1405
+ function recordFailure(span, error, normalized = asError(error)) {
1183
1406
  if (!alreadyReported(error)) {
1184
1407
  const exported = span.isRecording() && hasConsent();
1185
1408
  if (exported) markReported(error);
@@ -1193,33 +1416,71 @@ function recordFailure(span, error) {
1193
1416
  }
1194
1417
  /**
1195
1418
  * Record `error` on a one-off span, exported whatever the session's trace
1196
- * sampling, unless the app's error filters drop it.
1419
+ * sampling, unless the app's error filters drop it. Without tracing there is no
1420
+ * span, but a buffered replay still keeps itself for the error.
1197
1421
  */
1198
- function recordException(error, options, source, filename, filtered = false) {
1422
+ function recordException(error, options, record) {
1199
1423
  if (!hasConsent()) return;
1200
- if (!filtered && !shouldCapture(asError(error), {
1201
- source,
1424
+ const normalized = record.normalized ?? asError(error);
1425
+ if (!record.filtered && !shouldCapture(normalized, {
1426
+ source: record.source,
1202
1427
  originalError: error
1203
- }, filename)) return;
1428
+ }, record.filename)) return;
1204
1429
  const tracer = liveMapleTracer(SDK_NAME, SDK_VERSION);
1205
- if (!tracer) return;
1430
+ if (!tracer) {
1431
+ notifyErrorRecorded(INVALID_SPAN_CONTEXT);
1432
+ return;
1433
+ }
1206
1434
  const span = tracer.startSpan(options.name ?? "exception", {
1207
1435
  kind: SpanKind.INTERNAL,
1436
+ startTime: record.startTime,
1208
1437
  attributes: {
1209
1438
  ...typeof location !== "undefined" ? { "url.full": scrubUrl(location.href) } : void 0,
1210
1439
  ...options.attributes
1211
1440
  }
1212
1441
  }, keepContext(context.active()));
1213
- recordFailure(span, error);
1442
+ recordFailure(span, error, normalized);
1214
1443
  span.end();
1215
1444
  }
1445
+ /** Held until `init()` starts capture; beyond this, the oldest are dropped. */
1446
+ const MAX_PENDING = 30;
1447
+ let accepting = false;
1448
+ let pending = [];
1216
1449
  /**
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.
1450
+ * Record an error that no span was watching. Safe before `init()`: the call is
1451
+ * held (capped) and recorded when it runs. Reporting the same error object
1452
+ * twice records it once.
1219
1453
  */
1220
1454
  function captureException(error, options = {}) {
1455
+ if (typeof window === "undefined") return;
1456
+ if (!accepting && !liveMapleTracer("maple-browser", "0.14.0")) {
1457
+ pending.push({
1458
+ error,
1459
+ options,
1460
+ at: Date.now()
1461
+ });
1462
+ if (pending.length > MAX_PENDING) pending.shift();
1463
+ return;
1464
+ }
1221
1465
  if (alreadyReported(error)) return;
1222
- recordException(error, options, "captureException");
1466
+ recordException(error, options, { source: "captureException" });
1467
+ }
1468
+ /** Called by `init()` once tracing (if any) is up: records what was held, then records directly. */
1469
+ function openExceptionCapture() {
1470
+ accepting = true;
1471
+ const held = pending;
1472
+ pending = [];
1473
+ for (const { error, options, at } of held) {
1474
+ if (alreadyReported(error)) continue;
1475
+ recordException(error, options, {
1476
+ source: "captureException",
1477
+ startTime: at
1478
+ });
1479
+ }
1480
+ }
1481
+ /** Called by `shutdown()`: later calls are held for the next `init()`. */
1482
+ function closeExceptionCapture() {
1483
+ accepting = false;
1223
1484
  }
1224
1485
  /** Whether the app's error filters keep `error`, for failures recorded on an existing span. */
1225
1486
  function passesErrorFilters(error) {
@@ -1231,33 +1492,47 @@ function passesErrorFilters(error) {
1231
1492
  /** `captureException` for an error `passesErrorFilters` already let through: `beforeCapture` runs once. */
1232
1493
  function captureFilteredException(error, options = {}) {
1233
1494
  if (alreadyReported(error)) return;
1234
- recordException(error, options, "captureException", void 0, true);
1495
+ recordException(error, options, {
1496
+ source: "captureException",
1497
+ filtered: true
1498
+ });
1235
1499
  }
1236
1500
  /**
1237
1501
  * Register global handlers for uncaught errors and unhandled rejections.
1238
- * Returns a teardown that removes them.
1502
+ * Returns a teardown that removes them. With `record: false` (tracing or
1503
+ * `captureErrors` off) they record nothing and only tell the replay an error happened.
1239
1504
  */
1240
- function setupErrorCapture() {
1505
+ function setupErrorCapture(record = true) {
1241
1506
  if (typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {};
1507
+ const handle = (error, options, recordOptions) => {
1508
+ if (record) recordException(error, options, recordOptions);
1509
+ else notifyUnrecorded(error, recordOptions);
1510
+ };
1242
1511
  const onError = (event) => {
1243
- const error = event.error ?? (event.message && event.filename ? new Error(event.message) : void 0);
1512
+ const error = event.error ?? (event.message && event.filename ? event.message : void 0);
1244
1513
  if (error === void 0 || alreadyReported(error)) return;
1245
- recordException(error, {
1514
+ const thrownError = event.error instanceof Error;
1515
+ const filename = event.filename ? scrubUrl(event.filename) : void 0;
1516
+ handle(error, {
1246
1517
  name: "browser.uncaught_error",
1247
1518
  attributes: {
1248
1519
  "maple.exception.source": "window.onerror",
1249
- ...event.filename ? { "code.file.path": event.filename } : void 0,
1520
+ ...filename ? { "code.file.path": filename } : void 0,
1250
1521
  ...event.lineno ? { "code.line.number": event.lineno } : void 0,
1251
1522
  ...event.colno ? { "code.column.number": event.colno } : void 0
1252
1523
  }
1253
- }, "window.onerror", event.error instanceof Error ? void 0 : event.filename || void 0);
1524
+ }, {
1525
+ source: "window.onerror",
1526
+ filename: thrownError ? void 0 : event.filename || void 0,
1527
+ normalized: thrownError ? void 0 : withThrowSite(asError(error), filename, event)
1528
+ });
1254
1529
  };
1255
1530
  const onUnhandledRejection = (event) => {
1256
1531
  if (alreadyReported(event.reason)) return;
1257
- recordException(event.reason, {
1532
+ handle(event.reason, {
1258
1533
  name: "browser.unhandled_rejection",
1259
1534
  attributes: { "maple.exception.source": "unhandledrejection" }
1260
- }, "unhandledrejection");
1535
+ }, { source: "unhandledrejection" });
1261
1536
  };
1262
1537
  window.addEventListener("error", onError);
1263
1538
  window.addEventListener("unhandledrejection", onUnhandledRejection);
@@ -1266,6 +1541,35 @@ function setupErrorCapture() {
1266
1541
  window.removeEventListener("unhandledrejection", onUnhandledRejection);
1267
1542
  };
1268
1543
  }
1544
+ /** An error the app wants no span for still keeps a buffered replay, if the filters keep it. */
1545
+ function notifyUnrecorded(error, record) {
1546
+ if (!hasConsent()) return;
1547
+ const normalized = record.normalized ?? asError(error);
1548
+ if (!shouldCapture(normalized, {
1549
+ source: record.source,
1550
+ originalError: error
1551
+ }, record.filename)) return;
1552
+ notifyErrorRecorded(INVALID_SPAN_CONTEXT);
1553
+ }
1554
+ /**
1555
+ * A thrown non-Error has no frames of its own, only the event's location. One
1556
+ * V8-style frame there makes it groupable by where it was thrown.
1557
+ */
1558
+ function withThrowSite(error, filename, event) {
1559
+ if (!filename || error.stack !== void 0) return error;
1560
+ const position = [
1561
+ filename,
1562
+ event.lineno || void 0,
1563
+ event.colno || void 0
1564
+ ].filter((part) => part !== void 0).join(":");
1565
+ const header = error.message ? `${error.name}: ${error.message}` : error.name;
1566
+ Object.defineProperty(error, "stack", {
1567
+ value: `${header}\n at ${position}`,
1568
+ writable: true,
1569
+ configurable: true
1570
+ });
1571
+ return error;
1572
+ }
1269
1573
  //#endregion
1270
1574
  //#region src/config.ts
1271
1575
  /**
@@ -1275,7 +1579,14 @@ function setupErrorCapture() {
1275
1579
  function resolveIdentity(config) {
1276
1580
  return normalizeIdentity(config.user ?? config.userId);
1277
1581
  }
1582
+ let warnedServiceName = false;
1278
1583
  function resolveConfig(config) {
1584
+ if (typeof config.serviceName !== "string" || config.serviceName.trim() === "") {
1585
+ if (!warnedServiceName) {
1586
+ warnedServiceName = true;
1587
+ console.warn("[maple] init() needs a `serviceName`; telemetry is reported without one. Pass the name of your app.");
1588
+ }
1589
+ }
1279
1590
  const endpoint = resolveIngestEndpoint({
1280
1591
  endpoints: [config.endpoint],
1281
1592
  regions: [config.region]
@@ -1290,7 +1601,7 @@ function resolveConfig(config) {
1290
1601
  return {
1291
1602
  ...resolveSignalOptions(config),
1292
1603
  ingestKey: config.ingestKey,
1293
- serviceName: config.serviceName,
1604
+ serviceName: typeof config.serviceName === "string" ? config.serviceName : "",
1294
1605
  endpoint,
1295
1606
  serviceNamespace: config.serviceNamespace,
1296
1607
  serviceVersion: config.serviceVersion,
@@ -1520,6 +1831,9 @@ function parseTraceparent(value) {
1520
1831
  /** `x-maple-sdk` value for every request this build makes to ingest. */
1521
1832
  const SDK_HINT = sdkHint(SDK_NAME, SDK_VERSION);
1522
1833
  let active;
1834
+ /** What the live handle was made from, to tell a repeated `init()` from a reconfiguration. */
1835
+ let activeRawConfig;
1836
+ let warnedReinit = false;
1523
1837
  let activeConfig;
1524
1838
  /**
1525
1839
  * An `identify()` made before `init()`, applied when it runs. Auth callbacks
@@ -1529,12 +1843,42 @@ let activeConfig;
1529
1843
  */
1530
1844
  let pendingIdentity;
1531
1845
  /**
1846
+ * The flushing half of every `shutdown()` still running. A new `init()` starts
1847
+ * its session and deferred signals behind it: the session sink, replay and log
1848
+ * pipeline are page-wide, and the old teardown would otherwise stop the new ones.
1849
+ */
1850
+ let teardown;
1851
+ /** The longest a new `init()` waits for the previous handle's final flush. */
1852
+ const TEARDOWN_WAIT_MS = 3e3;
1853
+ /** Run one teardown step; a failure must not leave the rest of the SDK half torn down. */
1854
+ async function attempt(step) {
1855
+ try {
1856
+ await step();
1857
+ } catch {}
1858
+ }
1859
+ /** Same settings, by value: a re-render passing a fresh but equal object is not a reconfiguration. */
1860
+ function sameConfig(a, b) {
1861
+ if (a === b) return true;
1862
+ try {
1863
+ return JSON.stringify(a) === JSON.stringify(b);
1864
+ } catch {
1865
+ return false;
1866
+ }
1867
+ }
1868
+ /**
1532
1869
  * Initialize Maple browser telemetry. With consent gating enabled the returned
1533
1870
  * handle remains live while denied: granting starts capture, revoking detaches
1534
- * it without flushing, and a later grant starts cleanly again.
1871
+ * it without flushing, and a later grant starts cleanly again. While a handle
1872
+ * is live, `init()` returns it and ignores the new config.
1535
1873
  */
1536
1874
  function init(rawConfig) {
1537
- if (active) return active;
1875
+ if (active) {
1876
+ if (activeRawConfig && !warnedReinit && !sameConfig(activeRawConfig, rawConfig)) {
1877
+ warnedReinit = true;
1878
+ 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.");
1879
+ }
1880
+ return active;
1881
+ }
1538
1882
  if (typeof window === "undefined") return {
1539
1883
  sessionId: "",
1540
1884
  shutdown: () => Promise.resolve()
@@ -1543,11 +1887,15 @@ function init(rawConfig) {
1543
1887
  if (pendingIdentity) config.identity = normalizeIdentity(pendingIdentity.input);
1544
1888
  pendingIdentity = void 0;
1545
1889
  activeConfig = config;
1546
- configurePrivacy(config);
1890
+ activeRawConfig = rawConfig;
1891
+ const removeSanitizer = configurePrivacy(config);
1547
1892
  configureErrorFilters(config.errorFilters);
1548
1893
  if (!hasConsent()) clearPendingEvents();
1549
1894
  setActiveTraceIdProvider(() => trace.getActiveSpan()?.spanContext().traceId);
1550
1895
  const replayEligible = config.replayEnabled && !isLikelyBot(navigator.userAgent);
1896
+ const recordErrors = config.tracingEnabled && config.tracingCaptureErrors;
1897
+ const behind = teardown && Promise.race([teardown, new Promise((resolve) => setTimeout(resolve, TEARDOWN_WAIT_MS))]);
1898
+ let ready = behind === void 0;
1551
1899
  let runtime;
1552
1900
  let stopped = false;
1553
1901
  let rotateOnNextStart = false;
@@ -1556,8 +1904,14 @@ function init(rawConfig) {
1556
1904
  let deferredPending;
1557
1905
  let stopDeferred;
1558
1906
  let generation = 0;
1907
+ const startTelemetry = () => {
1908
+ if (stopped || !hasConsent()) return;
1909
+ if (config.tracingEnabled && !shutdownTracing) shutdownTracing = setupTracing(config);
1910
+ if (!stopErrorCapture && (recordErrors || replayEligible && config.replayOnErrorSampleRate > 0)) stopErrorCapture = setupErrorCapture(recordErrors);
1911
+ };
1559
1912
  const startRuntime = () => {
1560
- if (stopped || runtime || !hasConsent()) return;
1913
+ startTelemetry();
1914
+ if (stopped || !ready || runtime || !hasConsent()) return;
1561
1915
  setVisitorTracking(config.persistVisitorId && mayPersistIdentifier());
1562
1916
  const session = (rotateOnNextStart ? rotateSession() : void 0) ?? getSession();
1563
1917
  rotateOnNextStart = false;
@@ -1569,13 +1923,11 @@ function init(rawConfig) {
1569
1923
  sdk: SDK_HINT,
1570
1924
  maskAllInputs: config.maskAllInputs,
1571
1925
  maskAllText: config.maskAllText,
1572
- getIdentity: () => activeConfig?.identity
1926
+ getIdentity: () => config.identity
1573
1927
  }, session.id);
1574
- if (config.tracingEnabled && !shutdownTracing) shutdownTracing = setupTracing(config);
1575
- if (config.tracingEnabled && config.tracingCaptureErrors && !stopErrorCapture) stopErrorCapture = setupErrorCapture();
1576
1928
  if (!deferredPending) {
1577
- setLogIdentity(() => activeConfig?.identity?.id);
1578
- deferredPending = import("./deferred-BDKAWiKZ.mjs").then(({ startDeferred }) => {
1929
+ setLogIdentity(() => config.identity?.id);
1930
+ deferredPending = import("./deferred-BcyLmMyV.mjs").then(({ startDeferred }) => {
1579
1931
  stopDeferred = startDeferred(config);
1580
1932
  }).catch(() => {});
1581
1933
  }
@@ -1586,7 +1938,7 @@ function init(rawConfig) {
1586
1938
  serviceName: config.serviceName,
1587
1939
  environment: config.environment,
1588
1940
  serviceVersion: config.serviceVersion,
1589
- getIdentity: () => activeConfig?.identity,
1941
+ getIdentity: () => config.identity,
1590
1942
  captureUserEmail: config.captureUserEmail
1591
1943
  };
1592
1944
  const startMetadata = () => startMetadataSession({
@@ -1609,7 +1961,7 @@ function init(rawConfig) {
1609
1961
  runtime = next;
1610
1962
  const ownGeneration = ++generation;
1611
1963
  const stale = () => stopped || !hasConsent() || generation !== ownGeneration || runtime !== next;
1612
- next.replayPending = import("./replay-session-Cg3U1RS0.mjs").then(({ startReplaySession }) => {
1964
+ next.replayPending = import("./replay-session-DFPRE4Ee.mjs").then(({ startReplaySession }) => {
1613
1965
  if (stale()) return;
1614
1966
  next.replay = startReplaySession({
1615
1967
  ...shared,
@@ -1634,11 +1986,12 @@ function init(rawConfig) {
1634
1986
  const currentSessionId = previous.replay?.sessionId ?? previous.metadata?.sessionId;
1635
1987
  const liveSink = getActiveSink();
1636
1988
  const sink = liveSink && currentSessionId && liveSink.sessionId === currentSessionId ? liveSink : previous.sink;
1637
- if (flush) await sink.flush(true);
1989
+ const flushed = flush ? sink.flush(true) : void 0;
1638
1990
  sink.stop();
1639
1991
  if (sink !== previous.sink) previous.sink.stop();
1640
1992
  clearSessionSink(currentSessionId ?? previous.initialSessionId);
1641
- await Promise.all([
1993
+ await flushed;
1994
+ await Promise.allSettled([
1642
1995
  replayShutdown,
1643
1996
  metadataShutdown,
1644
1997
  previous.replayPending
@@ -1648,7 +2001,12 @@ function init(rawConfig) {
1648
2001
  runtime?.replay?.trigger();
1649
2002
  });
1650
2003
  startRuntime();
1651
- const stopConsentListener = config.requireConsent ? onConsentChange((allowed) => {
2004
+ openExceptionCapture();
2005
+ if (!ready) behind?.then(() => {
2006
+ ready = true;
2007
+ startRuntime();
2008
+ });
2009
+ const stopConsentListener = onConsentChange((allowed) => {
1652
2010
  if (allowed) {
1653
2011
  startRuntime();
1654
2012
  return;
@@ -1658,29 +2016,45 @@ function init(rawConfig) {
1658
2016
  clearPendingChunk();
1659
2017
  setVisitorTracking(false);
1660
2018
  stopRuntime(false);
1661
- }) : () => {};
2019
+ });
1662
2020
  const handle = {
1663
2021
  get sessionId() {
1664
2022
  return runtime?.replay?.sessionId ?? runtime?.metadata?.sessionId ?? runtime?.initialSessionId ?? "";
1665
2023
  },
1666
- shutdown: async () => {
1667
- if (stopped) return;
2024
+ shutdown: () => {
2025
+ if (stopped) return Promise.resolve();
1668
2026
  stopped = true;
2027
+ if (active === handle) {
2028
+ active = void 0;
2029
+ activeRawConfig = void 0;
2030
+ activeConfig = void 0;
2031
+ }
1669
2032
  stopConsentListener();
1670
2033
  stopReplayTrigger();
1671
- await stopRuntime(true);
2034
+ closeExceptionCapture();
1672
2035
  stopErrorCapture?.();
1673
2036
  stopErrorCapture = void 0;
1674
2037
  configureErrorFilters(void 0);
2038
+ removeSanitizer();
2039
+ setActiveTraceIdProvider(() => void 0);
1675
2040
  resetNavigation();
1676
- await deferredPending;
1677
- await shutdownTracing?.();
2041
+ const runtimeStopped = attempt(() => stopRuntime(true));
2042
+ const tracingStopped = attempt(() => shutdownTracing?.());
1678
2043
  shutdownTracing = void 0;
1679
- await stopDeferred?.();
1680
- stopDeferred = void 0;
1681
- setActiveTraceIdProvider(() => void 0);
1682
- active = void 0;
1683
- activeConfig = void 0;
2044
+ const flushed = (async () => {
2045
+ await attempt(() => runtimeStopped);
2046
+ await attempt(() => deferredPending);
2047
+ await attempt(() => tracingStopped);
2048
+ await attempt(() => stopDeferred?.());
2049
+ stopDeferred = void 0;
2050
+ if (active === void 0) attachSpanStash(void 0);
2051
+ })();
2052
+ const all = Promise.all([behind, flushed]).then(() => {});
2053
+ teardown = all;
2054
+ all.finally(() => {
2055
+ if (teardown === all) teardown = void 0;
2056
+ });
2057
+ return all;
1684
2058
  }
1685
2059
  };
1686
2060
  active = handle;
@@ -1763,4 +2137,4 @@ const MapleBrowser = {
1763
2137
  logger
1764
2138
  };
1765
2139
  //#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 };
2140
+ 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 };