@maple-dev/browser 0.11.1 → 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.
- package/README.md +23 -9
- package/dist/console-iNhlufZV.mjs +183 -0
- package/dist/{deferred-B8pFPgZ_.mjs → deferred-Dr2AmxSU.mjs} +32 -5
- package/dist/index.d.mts +12 -5
- package/dist/index.mjs +1 -1
- package/dist/react.d.mts +7 -4
- package/dist/react.mjs +43 -5
- package/dist/replay-session-BuUOwHnI.mjs +906 -0
- package/dist/{sink-DbEuphC-.mjs → sink-Ji0J42ul.mjs} +848 -188
- package/dist/{src-CCfykEMo.mjs → src-JUynR8Jc.mjs} +535 -169
- package/package.json +9 -6
- package/dist/console-Bkd3hBGp.mjs +0 -73
- package/dist/replay-session-D0zjvZIV.mjs +0 -596
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
157
|
+
defineEntry(out, key, value);
|
|
158
|
+
count++;
|
|
45
159
|
}
|
|
46
160
|
return out;
|
|
47
161
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
62
|
-
|
|
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:
|
|
71
|
-
email:
|
|
72
|
-
username:
|
|
73
|
-
groupId:
|
|
74
|
-
groupName:
|
|
75
|
-
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:
|
|
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
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
291
|
-
if (
|
|
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)
|
|
298
|
-
|
|
299
|
-
|
|
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 =
|
|
319
|
-
const
|
|
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
|
|
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
|
|
332
|
-
return [
|
|
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(
|
|
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)
|
|
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.
|
|
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 (
|
|
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,
|
|
1414
|
+
function recordException(error, options, record) {
|
|
1199
1415
|
if (!hasConsent()) return;
|
|
1200
|
-
|
|
1201
|
-
|
|
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)
|
|
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()
|
|
1218
|
-
*
|
|
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,
|
|
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 ?
|
|
1504
|
+
const error = event.error ?? (event.message && event.filename ? event.message : void 0);
|
|
1244
1505
|
if (error === void 0 || alreadyReported(error)) return;
|
|
1245
|
-
|
|
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
|
-
...
|
|
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
|
-
},
|
|
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
|
-
|
|
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,
|
|
@@ -1405,7 +1708,7 @@ function navigationSpanAt(epochMs) {
|
|
|
1405
1708
|
}
|
|
1406
1709
|
/** End the open navigation as interrupted: something other than its route finishing ended it. */
|
|
1407
1710
|
function interruptNavigation() {
|
|
1408
|
-
navigation?.span.setAttribute("
|
|
1711
|
+
navigation?.span.setAttribute("maple.navigation.interrupted", true);
|
|
1409
1712
|
navigation?.span.end();
|
|
1410
1713
|
navigation = void 0;
|
|
1411
1714
|
}
|
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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(() =>
|
|
1578
|
-
deferredPending = import("./deferred-
|
|
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: () =>
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
2033
|
+
const runtimeStopped = attempt(() => stopRuntime(true));
|
|
2034
|
+
const tracingStopped = attempt(() => shutdownTracing?.());
|
|
1678
2035
|
shutdownTracing = void 0;
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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 {
|
|
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 };
|