@maple-dev/browser 0.4.0 → 0.9.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/LICENSE +1 -1
- package/README.md +81 -2
- package/dist/index.d.mts +73 -20
- package/dist/index.mjs +307 -57
- package/dist/{replay-session-B_ACRogX.mjs → replay-session-gSlAYwqV.mjs} +81 -111
- package/dist/{sink-DHTfvFgl.mjs → sink-DHPiMgU0.mjs} +776 -134
- package/package.json +26 -10
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { trace } from "@opentelemetry/api";
|
|
1
|
+
import { A as setVisitorTracking, D as setActiveTraceIdProvider, M as scrubUrl, S as sdkHint, _ as isLikelyBot, a as recordTraceId, c as getActiveSink, d as claimReplaySample, f as getSession, g as postSessionMetaRow, h as rotateSession, i as readSessionSink, j as addUrlSanitizer, k as configureVisitorCookie, l as queuePending, n as getObservedTraceIds, o as startSessionLifecycle, r as publishSessionSink, s as clearPendingEvents, t as clearSessionSink, u as startEventSink, v as SDK_HINT_HEADER } from "./sink-DHPiMgU0.mjs";
|
|
2
|
+
import { ProxyTracerProvider, SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
3
3
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
4
|
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
5
5
|
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
|
|
@@ -7,7 +7,7 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
|
7
7
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
8
8
|
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
9
9
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
10
|
-
//#region ../browser-session/src/consent.ts
|
|
10
|
+
//#region ../browser-session/src/identity/consent.ts
|
|
11
11
|
/**
|
|
12
12
|
* Consent gating for session capture.
|
|
13
13
|
*
|
|
@@ -27,7 +27,7 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic
|
|
|
27
27
|
*/
|
|
28
28
|
/**
|
|
29
29
|
* Consent lives on `globalThis`, for the same reason the event sink does
|
|
30
|
-
* (`events-sink.ts`): an app that bundles both `@maple-dev/browser` and the
|
|
30
|
+
* (`events/events-sink.ts`): an app that bundles both `@maple-dev/browser` and the
|
|
31
31
|
* Effect SDK gets **two copies** of this module, and per-copy state would mean
|
|
32
32
|
* `setConsent(true)` on one never releases the other copy's exporters — or, the
|
|
33
33
|
* other way round, one copy capturing while the other believes consent is
|
|
@@ -76,6 +76,7 @@ function configurePrivacy(options) {
|
|
|
76
76
|
crossSubdomainCookie: options?.crossSubdomainCookie,
|
|
77
77
|
cookieDomain: options?.cookieDomain
|
|
78
78
|
});
|
|
79
|
+
if (options?.sanitizeUrl) addUrlSanitizer(options.sanitizeUrl);
|
|
79
80
|
updateEffectiveConsent(previous);
|
|
80
81
|
}
|
|
81
82
|
/** Record the user's consent decision. No-op unless `requireConsent` is set. */
|
|
@@ -120,7 +121,7 @@ function mayPersistIdentifier() {
|
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
122
123
|
//#endregion
|
|
123
|
-
//#region ../browser-session/src/identity.ts
|
|
124
|
+
//#region ../browser-session/src/identity/identity.ts
|
|
124
125
|
/**
|
|
125
126
|
* Trait caps. The warehouse column is `Map(String, String)` — plain keys
|
|
126
127
|
* precisely because they are arbitrary — so the cost of a unique key per user
|
|
@@ -151,7 +152,7 @@ function normalizeTraits(traits) {
|
|
|
151
152
|
if (!key) continue;
|
|
152
153
|
if (!warnedAboutTraitKeys && looksLikeId(key)) {
|
|
153
154
|
warnedAboutTraitKeys = true;
|
|
154
|
-
console.warn(`[maple] identity trait key "${key}" looks like an id.
|
|
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.`);
|
|
155
156
|
}
|
|
156
157
|
out[key] = value;
|
|
157
158
|
}
|
|
@@ -189,7 +190,7 @@ function normalizeIdentity(input) {
|
|
|
189
190
|
return resolved.id || resolved.email || resolved.username || resolved.groupId || resolved.groupName || Object.keys(resolved.traits).length > 0 ? resolved : void 0;
|
|
190
191
|
}
|
|
191
192
|
//#endregion
|
|
192
|
-
//#region ../browser-session/src/metadata-session.ts
|
|
193
|
+
//#region ../browser-session/src/session/metadata-session.ts
|
|
193
194
|
/**
|
|
194
195
|
* Metadata-only session lifecycle used whenever rrweb is disabled or unsampled.
|
|
195
196
|
* It is the shared lifecycle driver with no capture of its own: the distilled
|
|
@@ -200,7 +201,7 @@ function startMetadataSession(options) {
|
|
|
200
201
|
return startSessionLifecycle(options, {
|
|
201
202
|
recorded: false,
|
|
202
203
|
post: (row, keepalive) => {
|
|
203
|
-
postSessionMetaRow(options
|
|
204
|
+
postSessionMetaRow(options, row, keepalive);
|
|
204
205
|
},
|
|
205
206
|
onSuspend: ({ flush, keepalive }) => {
|
|
206
207
|
if (flush && keepalive) getActiveSink()?.flush(true);
|
|
@@ -209,18 +210,11 @@ function startMetadataSession(options) {
|
|
|
209
210
|
});
|
|
210
211
|
}
|
|
211
212
|
//#endregion
|
|
212
|
-
//#region ../browser-session/src/
|
|
213
|
-
/**
|
|
214
|
-
* Caps mirroring what the ingest gateway enforces. Applying them here too means
|
|
215
|
-
* an over-sized event is trimmed before it costs bandwidth, and the developer
|
|
216
|
-
* sees the same shape locally that the warehouse will store.
|
|
217
|
-
*/
|
|
218
|
-
const MAX_NAME_LENGTH = 128;
|
|
213
|
+
//#region ../browser-session/src/events/props.ts
|
|
219
214
|
const MAX_PROPS = 32;
|
|
220
215
|
const MAX_PROP_KEY_LENGTH = 64;
|
|
221
216
|
const MAX_PROP_VALUE_LENGTH = 1024;
|
|
222
217
|
const MAX_TOTAL_PROP_BYTES = 8192;
|
|
223
|
-
let warnedAboutName = false;
|
|
224
218
|
/**
|
|
225
219
|
* Coerce one property value to the string the warehouse column holds.
|
|
226
220
|
*
|
|
@@ -244,7 +238,8 @@ function coerce(value) {
|
|
|
244
238
|
return;
|
|
245
239
|
}
|
|
246
240
|
}
|
|
247
|
-
|
|
241
|
+
/** Coerce and cap a `track()` property bag to the `Map(String, String)` the warehouse stores. */
|
|
242
|
+
function coerceTrackProps(props) {
|
|
248
243
|
if (!props) return {};
|
|
249
244
|
const out = {};
|
|
250
245
|
let bytes = 0;
|
|
@@ -260,6 +255,9 @@ function coerceProps(props) {
|
|
|
260
255
|
}
|
|
261
256
|
return out;
|
|
262
257
|
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region ../browser-session/src/events/track.ts
|
|
260
|
+
let warnedAboutName = false;
|
|
263
261
|
/**
|
|
264
262
|
* Record a custom product event against the current session.
|
|
265
263
|
*
|
|
@@ -281,8 +279,8 @@ function track(name, props) {
|
|
|
281
279
|
}
|
|
282
280
|
const ev = {
|
|
283
281
|
type: "custom",
|
|
284
|
-
message: name.trim().slice(0,
|
|
285
|
-
attrs:
|
|
282
|
+
message: name.trim().slice(0, 128),
|
|
283
|
+
attrs: coerceTrackProps(props),
|
|
286
284
|
timestamp: Date.now(),
|
|
287
285
|
url: typeof location !== "undefined" ? location.href : void 0
|
|
288
286
|
};
|
|
@@ -291,43 +289,69 @@ function track(name, props) {
|
|
|
291
289
|
else queuePending(ev);
|
|
292
290
|
}
|
|
293
291
|
//#endregion
|
|
294
|
-
//#region src/
|
|
295
|
-
const
|
|
292
|
+
//#region ../browser-session/src/platform/region.ts
|
|
293
|
+
const MAPLE_REGIONS = ["us", "eu"];
|
|
294
|
+
const INGEST_ENDPOINTS = {
|
|
295
|
+
us: "https://ingest.maple.dev",
|
|
296
|
+
eu: "https://ingest.eu.maple.dev"
|
|
297
|
+
};
|
|
296
298
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
+
* Narrow an untyped region (an env var, or a JS caller outside the type
|
|
300
|
+
* checker) to a known one. Case and surrounding whitespace are ignored;
|
|
301
|
+
* anything else is `undefined`.
|
|
299
302
|
*/
|
|
300
|
-
function
|
|
301
|
-
|
|
303
|
+
function parseRegion(raw) {
|
|
304
|
+
if (typeof raw !== "string") return void 0;
|
|
305
|
+
const value = raw.trim().toLowerCase();
|
|
306
|
+
return MAPLE_REGIONS.find((region) => region === value);
|
|
302
307
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
308
|
+
let warnedRegions = /* @__PURE__ */ new Set();
|
|
309
|
+
/**
|
|
310
|
+
* The ingest base URL for a region. An unrecognized value falls back to the
|
|
311
|
+
* default region with a console warning rather than throwing: the ingest key
|
|
312
|
+
* belongs to one instance, so the other one rejects it and no data lands in
|
|
313
|
+
* the wrong place.
|
|
314
|
+
*/
|
|
315
|
+
function ingestEndpointForRegion(region) {
|
|
316
|
+
if (region === void 0 || region === null || region === "") return INGEST_ENDPOINTS["us"];
|
|
317
|
+
const parsed = parseRegion(region);
|
|
318
|
+
if (parsed) return INGEST_ENDPOINTS[parsed];
|
|
319
|
+
const label = String(region);
|
|
320
|
+
if (!warnedRegions.has(label)) {
|
|
321
|
+
warnedRegions.add(label);
|
|
322
|
+
console.warn(`[maple] unknown region "${label}"; expected one of ${MAPLE_REGIONS.join(", ")}. Using "us".`);
|
|
323
|
+
}
|
|
324
|
+
return INGEST_ENDPOINTS["us"];
|
|
325
|
+
}
|
|
326
|
+
/** Loop rather than `/\/+$/`: that pattern backtracks polynomially on a long run of slashes. */
|
|
327
|
+
function trimTrailingSlashes(value) {
|
|
328
|
+
let end = value.length;
|
|
329
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
|
|
330
|
+
return value.slice(0, end);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Resolve the ingest base URL. An explicit URL always beats a region, whichever
|
|
334
|
+
* source each came from, so a collector or proxy endpoint is never bypassed by
|
|
335
|
+
* a region set elsewhere. Candidates are tried in order; the first non-empty
|
|
336
|
+
* one wins.
|
|
337
|
+
*/
|
|
338
|
+
function resolveIngestEndpoint(options) {
|
|
339
|
+
for (const endpoint of options.endpoints) if (endpoint) return trimTrailingSlashes(endpoint);
|
|
340
|
+
return ingestEndpointForRegion(options.regions.find((value) => value !== void 0 && value !== null && value !== ""));
|
|
325
341
|
}
|
|
326
342
|
//#endregion
|
|
343
|
+
//#region src/version.ts
|
|
344
|
+
const SDK_VERSION = "0.9.0";
|
|
345
|
+
/** The `x-maple-sdk` value this build sends. */
|
|
346
|
+
const SDK_NAME = "maple-browser";
|
|
347
|
+
//#endregion
|
|
327
348
|
//#region src/tracing.ts
|
|
349
|
+
/** Span attributes that carry a page or request URL. */
|
|
350
|
+
const URL_ATTRIBUTES = ["url.full", "http.url"];
|
|
328
351
|
/**
|
|
329
|
-
* Captures every span's trace id into the session sink
|
|
330
|
-
*
|
|
352
|
+
* Captures every span's trace id into the session sink, and redacts URL
|
|
353
|
+
* attributes before anything can export them. Lightweight: runs alongside the
|
|
354
|
+
* BatchSpanProcessor, does no export of its own.
|
|
331
355
|
*/
|
|
332
356
|
var TraceIdCollector = class {
|
|
333
357
|
getUserId;
|
|
@@ -335,6 +359,10 @@ var TraceIdCollector = class {
|
|
|
335
359
|
this.getUserId = getUserId;
|
|
336
360
|
}
|
|
337
361
|
onStart(span) {
|
|
362
|
+
for (const key of URL_ATTRIBUTES) {
|
|
363
|
+
const value = span.attributes[key];
|
|
364
|
+
if (typeof value === "string") span.setAttribute(key, scrubUrl(value));
|
|
365
|
+
}
|
|
338
366
|
if (!hasConsent()) return;
|
|
339
367
|
recordTraceId(span.spanContext().traceId);
|
|
340
368
|
const sessionId = readSessionSink()?.sessionId;
|
|
@@ -381,6 +409,25 @@ var ConsentSpanExporter = class {
|
|
|
381
409
|
}
|
|
382
410
|
};
|
|
383
411
|
/**
|
|
412
|
+
* How long a span may sit in the batch queue before export.
|
|
413
|
+
*
|
|
414
|
+
* Shorter than OTel's 5s default: in a browser the queue is only as durable as
|
|
415
|
+
* the tab, and the unload flush below is a best-effort catch rather than a
|
|
416
|
+
* guarantee (a crashed or killed tab fires neither event). 2s trades a few more
|
|
417
|
+
* requests for a materially smaller loss window.
|
|
418
|
+
*/
|
|
419
|
+
const EXPORT_INTERVAL_MS = 2e3;
|
|
420
|
+
/**
|
|
421
|
+
* The provider this SDK registered, while it is live. `captureException` spans
|
|
422
|
+
* through it directly: the global provider may belong to the host app, which
|
|
423
|
+
* registered first and so kept the global.
|
|
424
|
+
*/
|
|
425
|
+
let mapleProvider;
|
|
426
|
+
/** A tracer on Maple's provider when tracing is live, otherwise the global one. */
|
|
427
|
+
function mapleTracer(name, version) {
|
|
428
|
+
return (mapleProvider ?? trace.getTracerProvider()).getTracer(name, version);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
384
431
|
* Set up browser OTel tracing exporting to Maple's ingest. When
|
|
385
432
|
* `tracingInstrumentFetch` is true, fetch() calls are auto-instrumented and
|
|
386
433
|
* their trace ids feed the session. Disable it when an external tracer (e.g.
|
|
@@ -402,7 +449,7 @@ function setupTracing(config) {
|
|
|
402
449
|
if (config.serviceNamespace) attributes["service.namespace"] = config.serviceNamespace;
|
|
403
450
|
if (config.serviceVersion) {
|
|
404
451
|
attributes[ATTR_SERVICE_VERSION] = config.serviceVersion;
|
|
405
|
-
attributes["
|
|
452
|
+
if (/^[0-9a-f]{7,40}$/i.test(config.serviceVersion)) attributes["vcs.ref.head.revision"] = config.serviceVersion;
|
|
406
453
|
}
|
|
407
454
|
if (config.environment) {
|
|
408
455
|
attributes["deployment.environment"] = config.environment;
|
|
@@ -410,27 +457,211 @@ function setupTracing(config) {
|
|
|
410
457
|
}
|
|
411
458
|
const exporter = new ConsentSpanExporter(new OTLPTraceExporter({
|
|
412
459
|
url: `${config.endpoint}/v1/traces`,
|
|
413
|
-
headers: {
|
|
460
|
+
headers: {
|
|
461
|
+
Authorization: `Bearer ${config.ingestKey}`,
|
|
462
|
+
[SDK_HINT_HEADER]: sdkHint(SDK_NAME, SDK_VERSION)
|
|
463
|
+
}
|
|
414
464
|
}));
|
|
415
465
|
const provider = new WebTracerProvider({
|
|
416
466
|
resource: resourceFromAttributes(attributes),
|
|
417
|
-
spanProcessors: [new TraceIdCollector(() => config.identity?.id), new BatchSpanProcessor(exporter)]
|
|
467
|
+
spanProcessors: [new TraceIdCollector(() => config.identity?.id), new BatchSpanProcessor(exporter, { scheduledDelayMillis: EXPORT_INTERVAL_MS })]
|
|
418
468
|
});
|
|
419
469
|
provider.register();
|
|
420
|
-
|
|
470
|
+
mapleProvider = provider;
|
|
471
|
+
const globalProvider = trace.getTracerProvider();
|
|
472
|
+
const ownsGlobals = globalProvider === provider || globalProvider instanceof ProxyTracerProvider && globalProvider.getDelegate() === provider;
|
|
473
|
+
const onExit = () => {
|
|
474
|
+
provider.forceFlush().catch(() => {});
|
|
475
|
+
};
|
|
476
|
+
const onVisibilityChange = () => {
|
|
477
|
+
if (document.visibilityState === "hidden") onExit();
|
|
478
|
+
};
|
|
479
|
+
const canListen = typeof document !== "undefined" && typeof document.addEventListener === "function";
|
|
480
|
+
if (canListen) {
|
|
481
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
482
|
+
window.addEventListener("pagehide", onExit);
|
|
483
|
+
}
|
|
484
|
+
const unregisterInstrumentations = config.tracingInstrumentFetch ? registerInstrumentations({
|
|
485
|
+
tracerProvider: provider,
|
|
486
|
+
instrumentations: [new FetchInstrumentation({
|
|
487
|
+
ignoreUrls: [new RegExp(`${escapeRegExp(config.endpoint)}/v1/`)],
|
|
488
|
+
propagateTraceHeaderCorsUrls: [...config.propagateTraceHeaderCorsUrls]
|
|
489
|
+
})]
|
|
490
|
+
}) : void 0;
|
|
421
491
|
return async () => {
|
|
492
|
+
if (canListen) {
|
|
493
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
494
|
+
window.removeEventListener("pagehide", onExit);
|
|
495
|
+
}
|
|
422
496
|
unregisterInstrumentations?.();
|
|
497
|
+
if (mapleProvider === provider) mapleProvider = void 0;
|
|
423
498
|
await provider.shutdown();
|
|
499
|
+
if (ownsGlobals) {
|
|
500
|
+
trace.disable();
|
|
501
|
+
context.disable();
|
|
502
|
+
propagation.disable();
|
|
503
|
+
}
|
|
424
504
|
};
|
|
425
505
|
}
|
|
426
506
|
function escapeRegExp(value) {
|
|
427
507
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
428
508
|
}
|
|
429
509
|
//#endregion
|
|
510
|
+
//#region src/errors.ts
|
|
511
|
+
const asError = (value) => {
|
|
512
|
+
if (value instanceof Error) return value;
|
|
513
|
+
if (typeof value === "string") return new Error(value);
|
|
514
|
+
if (typeof value === "object" && value !== null) {
|
|
515
|
+
const message = value.message;
|
|
516
|
+
if (typeof message === "string") return new Error(message);
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
return new Error(String(value));
|
|
520
|
+
} catch {
|
|
521
|
+
return /* @__PURE__ */ new Error("Unknown error");
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Errors already reported, by identity. Module-level so the global handlers and
|
|
526
|
+
* `captureException` share it: a framework boundary that reports an error and
|
|
527
|
+
* then rethrows it would otherwise produce two issues for one crash.
|
|
528
|
+
*/
|
|
529
|
+
let reported = /* @__PURE__ */ new WeakSet();
|
|
530
|
+
/** Whether this exact error object was already recorded. */
|
|
531
|
+
const alreadyReported = (error) => typeof error === "object" && error !== null && reported.has(error);
|
|
532
|
+
/**
|
|
533
|
+
* Record `error` on a one-off span. The error is claimed only when the span is
|
|
534
|
+
* recording: before `init()` the tracer is a no-op, and claiming it then would
|
|
535
|
+
* swallow the same error reported again once tracing is live.
|
|
536
|
+
*/
|
|
537
|
+
function recordException(error, options) {
|
|
538
|
+
const normalized = asError(error);
|
|
539
|
+
const span = mapleTracer(SDK_NAME, SDK_VERSION).startSpan(options.name ?? "exception", {
|
|
540
|
+
kind: SpanKind.INTERNAL,
|
|
541
|
+
attributes: {
|
|
542
|
+
...typeof location !== "undefined" ? { "url.full": scrubUrl(location.href) } : void 0,
|
|
543
|
+
...options.attributes
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
if (span.isRecording() && typeof error === "object" && error !== null) reported.add(error);
|
|
547
|
+
span.recordException(normalized);
|
|
548
|
+
span.setStatus({
|
|
549
|
+
code: SpanStatusCode.ERROR,
|
|
550
|
+
message: normalized.message
|
|
551
|
+
});
|
|
552
|
+
span.end();
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Record an error that no span was watching. Safe before `init()`: without a
|
|
556
|
+
* registered provider the OTel API hands back a no-op tracer and this does
|
|
557
|
+
* nothing. Reporting the same error object twice records it once.
|
|
558
|
+
*/
|
|
559
|
+
function captureException(error, options = {}) {
|
|
560
|
+
if (alreadyReported(error)) return;
|
|
561
|
+
recordException(error, options);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Register global handlers for uncaught errors and unhandled rejections.
|
|
565
|
+
* Returns a teardown that removes them.
|
|
566
|
+
*/
|
|
567
|
+
function setupErrorCapture() {
|
|
568
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return () => {};
|
|
569
|
+
const onError = (event) => {
|
|
570
|
+
const error = event.error ?? (event.message && event.filename ? new Error(event.message) : void 0);
|
|
571
|
+
if (error === void 0 || alreadyReported(error)) return;
|
|
572
|
+
recordException(error, {
|
|
573
|
+
name: "browser.uncaught_error",
|
|
574
|
+
attributes: {
|
|
575
|
+
"maple.exception.source": "window.onerror",
|
|
576
|
+
...event.filename ? { "code.file.path": event.filename } : void 0,
|
|
577
|
+
...event.lineno ? { "code.line.number": event.lineno } : void 0
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
const onUnhandledRejection = (event) => {
|
|
582
|
+
if (alreadyReported(event.reason)) return;
|
|
583
|
+
recordException(event.reason, {
|
|
584
|
+
name: "browser.unhandled_rejection",
|
|
585
|
+
attributes: { "maple.exception.source": "unhandledrejection" }
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
window.addEventListener("error", onError);
|
|
589
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
590
|
+
return () => {
|
|
591
|
+
window.removeEventListener("error", onError);
|
|
592
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/config.ts
|
|
597
|
+
/**
|
|
598
|
+
* Resolve the identity from either the new `user` object or the legacy
|
|
599
|
+
* `userId` string. `user` wins when both are set.
|
|
600
|
+
*/
|
|
601
|
+
function resolveIdentity(config) {
|
|
602
|
+
return normalizeIdentity(config.user ?? config.userId);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* A sample rate outside 0–1 (or not a number) is a typo, not a policy. Clamp it
|
|
606
|
+
* and say so, rather than recording everyone or no one without a word.
|
|
607
|
+
*/
|
|
608
|
+
function resolveSampleRate(raw) {
|
|
609
|
+
if (raw === void 0) return 1;
|
|
610
|
+
if (typeof raw !== "number" || Number.isNaN(raw)) {
|
|
611
|
+
console.warn(`[maple] replay.sampleRate must be a number between 0 and 1; got ${String(raw)}. Using 1.`);
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
if (raw < 0 || raw > 1) {
|
|
615
|
+
const clamped = Math.min(1, Math.max(0, raw));
|
|
616
|
+
console.warn(`[maple] replay.sampleRate must be between 0 and 1; got ${raw}. Using ${clamped}.`);
|
|
617
|
+
return clamped;
|
|
618
|
+
}
|
|
619
|
+
return raw;
|
|
620
|
+
}
|
|
621
|
+
function resolveConfig(config) {
|
|
622
|
+
if (!config.ingestKey) console.warn("[maple] MapleBrowser.init() has no ingestKey; ingest will reject every request.");
|
|
623
|
+
return {
|
|
624
|
+
ingestKey: config.ingestKey,
|
|
625
|
+
serviceName: config.serviceName,
|
|
626
|
+
endpoint: resolveIngestEndpoint({
|
|
627
|
+
endpoints: [config.endpoint],
|
|
628
|
+
regions: [config.region]
|
|
629
|
+
}),
|
|
630
|
+
serviceNamespace: config.serviceNamespace,
|
|
631
|
+
serviceVersion: config.serviceVersion,
|
|
632
|
+
environment: config.environment,
|
|
633
|
+
identity: resolveIdentity(config),
|
|
634
|
+
tracingEnabled: config.tracing?.enabled ?? true,
|
|
635
|
+
tracingInstrumentFetch: config.tracing?.instrumentFetch ?? true,
|
|
636
|
+
tracingCaptureErrors: config.tracing?.captureErrors ?? true,
|
|
637
|
+
propagateTraceHeaderCorsUrls: config.tracing?.propagateTraceHeaderCorsUrls ?? [],
|
|
638
|
+
replayEnabled: config.replay?.enabled ?? true,
|
|
639
|
+
replaySampleRate: resolveSampleRate(config.replay?.sampleRate),
|
|
640
|
+
maskAllInputs: config.privacy?.maskAllInputs ?? true,
|
|
641
|
+
maskAllText: config.privacy?.maskAllText ?? false,
|
|
642
|
+
persistVisitorId: config.privacy?.persistVisitorId ?? true,
|
|
643
|
+
crossSubdomainCookie: config.privacy?.crossSubdomainCookie ?? true,
|
|
644
|
+
cookieDomain: config.privacy?.cookieDomain,
|
|
645
|
+
requireConsent: config.privacy?.requireConsent ?? false,
|
|
646
|
+
captureUserEmail: config.privacy?.captureUserEmail ?? true,
|
|
647
|
+
respectDoNotTrack: config.privacy?.respectDoNotTrack ?? false,
|
|
648
|
+
sanitizeUrl: config.privacy?.sanitizeUrl
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
//#endregion
|
|
430
652
|
//#region src/init.ts
|
|
653
|
+
/** `x-maple-sdk` value for every request this build makes to ingest. */
|
|
654
|
+
const SDK_HINT = sdkHint(SDK_NAME, SDK_VERSION);
|
|
431
655
|
let active;
|
|
432
656
|
let activeConfig;
|
|
433
657
|
/**
|
|
658
|
+
* An `identify()` made before `init()`, applied when it runs. Auth callbacks
|
|
659
|
+
* routinely resolve before the SDK is initialized; dropping the call meant the
|
|
660
|
+
* whole first session went anonymous. Wrapped so a pending *clear* (`undefined`)
|
|
661
|
+
* is distinguishable from no call at all.
|
|
662
|
+
*/
|
|
663
|
+
let pendingIdentity;
|
|
664
|
+
/**
|
|
434
665
|
* Initialize Maple browser telemetry. With consent gating enabled the returned
|
|
435
666
|
* handle remains live while denied: granting starts capture, revoking detaches
|
|
436
667
|
* it without flushing, and a later grant starts cleanly again.
|
|
@@ -442,32 +673,40 @@ function init(rawConfig) {
|
|
|
442
673
|
shutdown: () => Promise.resolve()
|
|
443
674
|
};
|
|
444
675
|
const config = resolveConfig(rawConfig);
|
|
676
|
+
if (pendingIdentity) config.identity = normalizeIdentity(pendingIdentity.input);
|
|
677
|
+
pendingIdentity = void 0;
|
|
445
678
|
activeConfig = config;
|
|
446
679
|
configurePrivacy(config);
|
|
447
680
|
if (!hasConsent()) clearPendingEvents();
|
|
448
681
|
setActiveTraceIdProvider(() => trace.getActiveSpan()?.spanContext().traceId);
|
|
449
|
-
const
|
|
682
|
+
const replayEligible = config.replayEnabled && !isLikelyBot(navigator.userAgent);
|
|
450
683
|
let runtime;
|
|
451
684
|
let stopped = false;
|
|
452
685
|
let rotateOnNextStart = false;
|
|
453
686
|
let shutdownTracing;
|
|
687
|
+
let stopErrorCapture;
|
|
454
688
|
let generation = 0;
|
|
455
689
|
const startRuntime = () => {
|
|
456
690
|
if (stopped || runtime || !hasConsent()) return;
|
|
457
691
|
setVisitorTracking(config.persistVisitorId && mayPersistIdentifier());
|
|
458
692
|
const session = (rotateOnNextStart ? rotateSession() : void 0) ?? getSession();
|
|
459
693
|
rotateOnNextStart = false;
|
|
694
|
+
const recordReplay = replayEligible && claimReplaySample(config.replaySampleRate);
|
|
460
695
|
publishSessionSink(session.id);
|
|
461
696
|
const sink = startEventSink({
|
|
462
697
|
endpoint: config.endpoint,
|
|
463
698
|
ingestKey: config.ingestKey,
|
|
699
|
+
sdk: SDK_HINT,
|
|
464
700
|
maskAllInputs: config.maskAllInputs,
|
|
465
|
-
maskAllText: config.maskAllText
|
|
701
|
+
maskAllText: config.maskAllText,
|
|
702
|
+
getIdentity: () => activeConfig?.identity
|
|
466
703
|
}, session.id);
|
|
467
704
|
if (config.tracingEnabled && !shutdownTracing) shutdownTracing = setupTracing(config);
|
|
705
|
+
if (config.tracingEnabled && config.tracingCaptureErrors && !stopErrorCapture) stopErrorCapture = setupErrorCapture();
|
|
468
706
|
const shared = {
|
|
469
707
|
endpoint: config.endpoint,
|
|
470
708
|
ingestKey: config.ingestKey,
|
|
709
|
+
sdk: SDK_HINT,
|
|
471
710
|
serviceName: config.serviceName,
|
|
472
711
|
environment: config.environment,
|
|
473
712
|
serviceVersion: config.serviceVersion,
|
|
@@ -494,7 +733,7 @@ function init(rawConfig) {
|
|
|
494
733
|
runtime = next;
|
|
495
734
|
const ownGeneration = ++generation;
|
|
496
735
|
const stale = () => stopped || !hasConsent() || generation !== ownGeneration || runtime !== next;
|
|
497
|
-
next.replayPending = import("./replay-session-
|
|
736
|
+
next.replayPending = import("./replay-session-gSlAYwqV.mjs").then(({ startReplaySession }) => {
|
|
498
737
|
if (stale()) return;
|
|
499
738
|
next.replay = startReplaySession({
|
|
500
739
|
...shared,
|
|
@@ -546,6 +785,8 @@ function init(rawConfig) {
|
|
|
546
785
|
stopped = true;
|
|
547
786
|
stopConsentListener();
|
|
548
787
|
await stopRuntime(true);
|
|
788
|
+
stopErrorCapture?.();
|
|
789
|
+
stopErrorCapture = void 0;
|
|
549
790
|
await shutdownTracing?.();
|
|
550
791
|
shutdownTracing = void 0;
|
|
551
792
|
setActiveTraceIdProvider(() => void 0);
|
|
@@ -571,9 +812,16 @@ function init(rawConfig) {
|
|
|
571
812
|
*
|
|
572
813
|
* Each call replaces the identity rather than merging — merging would leak a
|
|
573
814
|
* signed-out user's email into whoever signs in next on a shared device.
|
|
815
|
+
*
|
|
816
|
+
* Safe before `init()`: the latest call is held and applied when `init()` runs,
|
|
817
|
+
* taking precedence over the `user` passed to it.
|
|
574
818
|
*/
|
|
575
819
|
function identify(input) {
|
|
576
|
-
if (typeof window === "undefined"
|
|
820
|
+
if (typeof window === "undefined") return;
|
|
821
|
+
if (!activeConfig) {
|
|
822
|
+
pendingIdentity = { input };
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
577
825
|
activeConfig.identity = normalizeIdentity(input);
|
|
578
826
|
}
|
|
579
827
|
//#endregion
|
|
@@ -589,6 +837,7 @@ function identify(input) {
|
|
|
589
837
|
* MapleBrowser.init({
|
|
590
838
|
* ingestKey: "maple_pk_...",
|
|
591
839
|
* serviceName: "acme-web",
|
|
840
|
+
* region: "eu", // omit for the US region
|
|
592
841
|
* })
|
|
593
842
|
*
|
|
594
843
|
* MapleBrowser.identify({ id: user.id, email: user.email, groupId: org.id, groupName: org.name })
|
|
@@ -599,6 +848,7 @@ const MapleBrowser = {
|
|
|
599
848
|
init,
|
|
600
849
|
identify,
|
|
601
850
|
track,
|
|
851
|
+
captureException,
|
|
602
852
|
setConsent
|
|
603
853
|
};
|
|
604
854
|
//#endregion
|