@maple-dev/browser 0.8.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/README.md +64 -3
- package/dist/index.d.mts +72 -19
- package/dist/index.mjs +265 -51
- package/dist/{replay-session-BYZfi3iZ.mjs → replay-session-gSlAYwqV.mjs} +41 -8
- package/dist/{sink-D9w1kg0Q.mjs → sink-DHPiMgU0.mjs} +445 -78
- package/package.json +11 -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";
|
|
@@ -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. */
|
|
@@ -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
|
}
|
|
@@ -209,18 +210,11 @@ function startMetadataSession(options) {
|
|
|
209
210
|
});
|
|
210
211
|
}
|
|
211
212
|
//#endregion
|
|
212
|
-
//#region ../browser-session/src/events/
|
|
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,48 +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
|
|
327
343
|
//#region src/version.ts
|
|
328
|
-
const SDK_VERSION = "0.
|
|
344
|
+
const SDK_VERSION = "0.9.0";
|
|
329
345
|
/** The `x-maple-sdk` value this build sends. */
|
|
330
346
|
const SDK_NAME = "maple-browser";
|
|
331
347
|
//#endregion
|
|
332
348
|
//#region src/tracing.ts
|
|
349
|
+
/** Span attributes that carry a page or request URL. */
|
|
350
|
+
const URL_ATTRIBUTES = ["url.full", "http.url"];
|
|
333
351
|
/**
|
|
334
|
-
* Captures every span's trace id into the session sink
|
|
335
|
-
*
|
|
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.
|
|
336
355
|
*/
|
|
337
356
|
var TraceIdCollector = class {
|
|
338
357
|
getUserId;
|
|
@@ -340,6 +359,10 @@ var TraceIdCollector = class {
|
|
|
340
359
|
this.getUserId = getUserId;
|
|
341
360
|
}
|
|
342
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
|
+
}
|
|
343
366
|
if (!hasConsent()) return;
|
|
344
367
|
recordTraceId(span.spanContext().traceId);
|
|
345
368
|
const sessionId = readSessionSink()?.sessionId;
|
|
@@ -395,6 +418,16 @@ var ConsentSpanExporter = class {
|
|
|
395
418
|
*/
|
|
396
419
|
const EXPORT_INTERVAL_MS = 2e3;
|
|
397
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
|
+
/**
|
|
398
431
|
* Set up browser OTel tracing exporting to Maple's ingest. When
|
|
399
432
|
* `tracingInstrumentFetch` is true, fetch() calls are auto-instrumented and
|
|
400
433
|
* their trace ids feed the session. Disable it when an external tracer (e.g.
|
|
@@ -416,7 +449,7 @@ function setupTracing(config) {
|
|
|
416
449
|
if (config.serviceNamespace) attributes["service.namespace"] = config.serviceNamespace;
|
|
417
450
|
if (config.serviceVersion) {
|
|
418
451
|
attributes[ATTR_SERVICE_VERSION] = config.serviceVersion;
|
|
419
|
-
attributes["
|
|
452
|
+
if (/^[0-9a-f]{7,40}$/i.test(config.serviceVersion)) attributes["vcs.ref.head.revision"] = config.serviceVersion;
|
|
420
453
|
}
|
|
421
454
|
if (config.environment) {
|
|
422
455
|
attributes["deployment.environment"] = config.environment;
|
|
@@ -434,6 +467,9 @@ function setupTracing(config) {
|
|
|
434
467
|
spanProcessors: [new TraceIdCollector(() => config.identity?.id), new BatchSpanProcessor(exporter, { scheduledDelayMillis: EXPORT_INTERVAL_MS })]
|
|
435
468
|
});
|
|
436
469
|
provider.register();
|
|
470
|
+
mapleProvider = provider;
|
|
471
|
+
const globalProvider = trace.getTracerProvider();
|
|
472
|
+
const ownsGlobals = globalProvider === provider || globalProvider instanceof ProxyTracerProvider && globalProvider.getDelegate() === provider;
|
|
437
473
|
const onExit = () => {
|
|
438
474
|
provider.forceFlush().catch(() => {});
|
|
439
475
|
};
|
|
@@ -445,26 +481,187 @@ function setupTracing(config) {
|
|
|
445
481
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
446
482
|
window.addEventListener("pagehide", onExit);
|
|
447
483
|
}
|
|
448
|
-
const unregisterInstrumentations = config.tracingInstrumentFetch ? registerInstrumentations({
|
|
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;
|
|
449
491
|
return async () => {
|
|
450
492
|
if (canListen) {
|
|
451
493
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
452
494
|
window.removeEventListener("pagehide", onExit);
|
|
453
495
|
}
|
|
454
496
|
unregisterInstrumentations?.();
|
|
497
|
+
if (mapleProvider === provider) mapleProvider = void 0;
|
|
455
498
|
await provider.shutdown();
|
|
499
|
+
if (ownsGlobals) {
|
|
500
|
+
trace.disable();
|
|
501
|
+
context.disable();
|
|
502
|
+
propagation.disable();
|
|
503
|
+
}
|
|
456
504
|
};
|
|
457
505
|
}
|
|
458
506
|
function escapeRegExp(value) {
|
|
459
507
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
460
508
|
}
|
|
461
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
|
|
462
652
|
//#region src/init.ts
|
|
463
653
|
/** `x-maple-sdk` value for every request this build makes to ingest. */
|
|
464
654
|
const SDK_HINT = sdkHint(SDK_NAME, SDK_VERSION);
|
|
465
655
|
let active;
|
|
466
656
|
let activeConfig;
|
|
467
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
|
+
/**
|
|
468
665
|
* Initialize Maple browser telemetry. With consent gating enabled the returned
|
|
469
666
|
* handle remains live while denied: granting starts capture, revoking detaches
|
|
470
667
|
* it without flushing, and a later grant starts cleanly again.
|
|
@@ -476,30 +673,36 @@ function init(rawConfig) {
|
|
|
476
673
|
shutdown: () => Promise.resolve()
|
|
477
674
|
};
|
|
478
675
|
const config = resolveConfig(rawConfig);
|
|
676
|
+
if (pendingIdentity) config.identity = normalizeIdentity(pendingIdentity.input);
|
|
677
|
+
pendingIdentity = void 0;
|
|
479
678
|
activeConfig = config;
|
|
480
679
|
configurePrivacy(config);
|
|
481
680
|
if (!hasConsent()) clearPendingEvents();
|
|
482
681
|
setActiveTraceIdProvider(() => trace.getActiveSpan()?.spanContext().traceId);
|
|
483
|
-
const
|
|
682
|
+
const replayEligible = config.replayEnabled && !isLikelyBot(navigator.userAgent);
|
|
484
683
|
let runtime;
|
|
485
684
|
let stopped = false;
|
|
486
685
|
let rotateOnNextStart = false;
|
|
487
686
|
let shutdownTracing;
|
|
687
|
+
let stopErrorCapture;
|
|
488
688
|
let generation = 0;
|
|
489
689
|
const startRuntime = () => {
|
|
490
690
|
if (stopped || runtime || !hasConsent()) return;
|
|
491
691
|
setVisitorTracking(config.persistVisitorId && mayPersistIdentifier());
|
|
492
692
|
const session = (rotateOnNextStart ? rotateSession() : void 0) ?? getSession();
|
|
493
693
|
rotateOnNextStart = false;
|
|
694
|
+
const recordReplay = replayEligible && claimReplaySample(config.replaySampleRate);
|
|
494
695
|
publishSessionSink(session.id);
|
|
495
696
|
const sink = startEventSink({
|
|
496
697
|
endpoint: config.endpoint,
|
|
497
698
|
ingestKey: config.ingestKey,
|
|
498
699
|
sdk: SDK_HINT,
|
|
499
700
|
maskAllInputs: config.maskAllInputs,
|
|
500
|
-
maskAllText: config.maskAllText
|
|
701
|
+
maskAllText: config.maskAllText,
|
|
702
|
+
getIdentity: () => activeConfig?.identity
|
|
501
703
|
}, session.id);
|
|
502
704
|
if (config.tracingEnabled && !shutdownTracing) shutdownTracing = setupTracing(config);
|
|
705
|
+
if (config.tracingEnabled && config.tracingCaptureErrors && !stopErrorCapture) stopErrorCapture = setupErrorCapture();
|
|
503
706
|
const shared = {
|
|
504
707
|
endpoint: config.endpoint,
|
|
505
708
|
ingestKey: config.ingestKey,
|
|
@@ -530,7 +733,7 @@ function init(rawConfig) {
|
|
|
530
733
|
runtime = next;
|
|
531
734
|
const ownGeneration = ++generation;
|
|
532
735
|
const stale = () => stopped || !hasConsent() || generation !== ownGeneration || runtime !== next;
|
|
533
|
-
next.replayPending = import("./replay-session-
|
|
736
|
+
next.replayPending = import("./replay-session-gSlAYwqV.mjs").then(({ startReplaySession }) => {
|
|
534
737
|
if (stale()) return;
|
|
535
738
|
next.replay = startReplaySession({
|
|
536
739
|
...shared,
|
|
@@ -582,6 +785,8 @@ function init(rawConfig) {
|
|
|
582
785
|
stopped = true;
|
|
583
786
|
stopConsentListener();
|
|
584
787
|
await stopRuntime(true);
|
|
788
|
+
stopErrorCapture?.();
|
|
789
|
+
stopErrorCapture = void 0;
|
|
585
790
|
await shutdownTracing?.();
|
|
586
791
|
shutdownTracing = void 0;
|
|
587
792
|
setActiveTraceIdProvider(() => void 0);
|
|
@@ -607,9 +812,16 @@ function init(rawConfig) {
|
|
|
607
812
|
*
|
|
608
813
|
* Each call replaces the identity rather than merging — merging would leak a
|
|
609
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.
|
|
610
818
|
*/
|
|
611
819
|
function identify(input) {
|
|
612
|
-
if (typeof window === "undefined"
|
|
820
|
+
if (typeof window === "undefined") return;
|
|
821
|
+
if (!activeConfig) {
|
|
822
|
+
pendingIdentity = { input };
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
613
825
|
activeConfig.identity = normalizeIdentity(input);
|
|
614
826
|
}
|
|
615
827
|
//#endregion
|
|
@@ -625,6 +837,7 @@ function identify(input) {
|
|
|
625
837
|
* MapleBrowser.init({
|
|
626
838
|
* ingestKey: "maple_pk_...",
|
|
627
839
|
* serviceName: "acme-web",
|
|
840
|
+
* region: "eu", // omit for the US region
|
|
628
841
|
* })
|
|
629
842
|
*
|
|
630
843
|
* MapleBrowser.identify({ id: user.id, email: user.email, groupId: org.id, groupName: org.name })
|
|
@@ -635,6 +848,7 @@ const MapleBrowser = {
|
|
|
635
848
|
init,
|
|
636
849
|
identify,
|
|
637
850
|
track,
|
|
851
|
+
captureException,
|
|
638
852
|
setConsent
|
|
639
853
|
};
|
|
640
854
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as warnDropped, E as activeTraceId, M as scrubUrl, O as withStartedTraceId, T as safeEmit, b as postSessionBlob, m as nextChunkSeq, n as getObservedTraceIds, o as startSessionLifecycle, p as markActivity, r as publishSessionSink, u as startEventSink, w as BLOCK_SELECTOR, x as postSessionMeta, y as gzip } from "./sink-DHPiMgU0.mjs";
|
|
2
2
|
import { record } from "rrweb";
|
|
3
3
|
//#region ../browser-session/src/replay/capture/console.ts
|
|
4
4
|
const LEVELS = [
|
|
@@ -9,6 +9,8 @@ const LEVELS = [
|
|
|
9
9
|
"debug"
|
|
10
10
|
];
|
|
11
11
|
const MAX_MESSAGE = 2e3;
|
|
12
|
+
/** No array logged into a 2,000-character message needs more elements than this. */
|
|
13
|
+
const MAX_ARRAY_ELEMENTS = 100;
|
|
12
14
|
/**
|
|
13
15
|
* Capture `console.*` calls as session events. Wraps each method, emits a
|
|
14
16
|
* distilled record, then forwards to the original so the host app's console
|
|
@@ -40,13 +42,29 @@ function formatArgs(args) {
|
|
|
40
42
|
if (typeof a === "string") return a;
|
|
41
43
|
if (a instanceof Error) return `${a.name}: ${a.message}`;
|
|
42
44
|
try {
|
|
43
|
-
return
|
|
45
|
+
return boundedStringify(a, MAX_MESSAGE) ?? String(a);
|
|
44
46
|
} catch {
|
|
45
47
|
return String(a);
|
|
46
48
|
}
|
|
47
49
|
}).join(" ");
|
|
48
50
|
return text.length > MAX_MESSAGE ? `${text.slice(0, MAX_MESSAGE)}…` : text;
|
|
49
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* `JSON.stringify` that stops descending once roughly `budget` characters have
|
|
54
|
+
* been produced. The message is cut to `MAX_MESSAGE` anyway, and serializing a
|
|
55
|
+
* whole store object on every `console.log` first was a main-thread cost paid
|
|
56
|
+
* on the host app's hot path. A replacer returning `undefined` skips a value
|
|
57
|
+
* without visiting its children, so the walk is bounded by the budget.
|
|
58
|
+
*/
|
|
59
|
+
function boundedStringify(value, budget) {
|
|
60
|
+
let used = 0;
|
|
61
|
+
return JSON.stringify(value, (key, nested) => {
|
|
62
|
+
if (used > budget) return void 0;
|
|
63
|
+
used += key.length + (typeof nested === "string" ? nested.length : 4);
|
|
64
|
+
if (Array.isArray(nested) && nested.length > MAX_ARRAY_ELEMENTS) return nested.slice(0, MAX_ARRAY_ELEMENTS);
|
|
65
|
+
return nested;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
50
68
|
//#endregion
|
|
51
69
|
//#region ../browser-session/src/replay/capture/network.ts
|
|
52
70
|
/**
|
|
@@ -59,10 +77,13 @@ function installNetworkCapture(emit, ignoreUrl) {
|
|
|
59
77
|
if (origFetch) window.fetch = async (input, init) => {
|
|
60
78
|
const url = requestUrl(input);
|
|
61
79
|
const method = requestMethod(input, init);
|
|
62
|
-
const
|
|
80
|
+
const ambientTraceId = activeTraceId();
|
|
63
81
|
const start = performance.now();
|
|
82
|
+
let traceId = ambientTraceId;
|
|
64
83
|
try {
|
|
65
|
-
const
|
|
84
|
+
const call = withStartedTraceId(() => origFetch(input, init));
|
|
85
|
+
traceId = call.traceId ?? ambientTraceId;
|
|
86
|
+
const res = await call.result;
|
|
66
87
|
record(url, method, res.status, start, traceId);
|
|
67
88
|
return res;
|
|
68
89
|
} catch (error) {
|
|
@@ -100,11 +121,13 @@ function installNetworkCapture(emit, ignoreUrl) {
|
|
|
100
121
|
XHR.prototype.send = function(...args) {
|
|
101
122
|
const meta = this;
|
|
102
123
|
const start = performance.now();
|
|
103
|
-
|
|
124
|
+
let traceId = activeTraceId();
|
|
104
125
|
this.addEventListener("loadend", () => {
|
|
105
126
|
record(meta.__mapleUrl ?? "", meta.__mapleMethod ?? "GET", this.status, start, traceId);
|
|
106
127
|
});
|
|
107
|
-
|
|
128
|
+
const call = withStartedTraceId(() => origSend.apply(this, args));
|
|
129
|
+
traceId = call.traceId ?? traceId;
|
|
130
|
+
return call.result;
|
|
108
131
|
};
|
|
109
132
|
}
|
|
110
133
|
return () => {
|
|
@@ -146,6 +169,7 @@ function startEventCapture(config, sessionId) {
|
|
|
146
169
|
//#endregion
|
|
147
170
|
//#region ../browser-session/src/replay/record.ts
|
|
148
171
|
const FULL_SNAPSHOT = 2;
|
|
172
|
+
const META = 4;
|
|
149
173
|
const INCREMENTAL = 3;
|
|
150
174
|
const SOURCE_MOUSE_INTERACTION = 2;
|
|
151
175
|
const MOUSE_CLICK = 2;
|
|
@@ -188,7 +212,13 @@ function startRecording(config, sessionId) {
|
|
|
188
212
|
const durationMs = Math.max(0, lastTimestamp - firstTimestamp);
|
|
189
213
|
const seq = nextChunkSeq();
|
|
190
214
|
resetBuffer();
|
|
191
|
-
|
|
215
|
+
let gzipped;
|
|
216
|
+
try {
|
|
217
|
+
gzipped = await gzip(new TextEncoder().encode(body));
|
|
218
|
+
} catch (error) {
|
|
219
|
+
warnDropped("chunk compression", error);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
192
222
|
if (await postSessionBlob(config, {
|
|
193
223
|
sessionId,
|
|
194
224
|
chunkSeq: seq,
|
|
@@ -207,6 +237,7 @@ function startRecording(config, sessionId) {
|
|
|
207
237
|
const active = markActivity();
|
|
208
238
|
if (active && active.id !== sessionId) return;
|
|
209
239
|
const e = event;
|
|
240
|
+
if (e.type === META && e.data && typeof e.data.href === "string") e.data.href = scrubUrl(e.data.href);
|
|
210
241
|
const isFullSnapshot = isCheckpoint === true || e.type === FULL_SNAPSHOT;
|
|
211
242
|
if (e.type === INCREMENTAL && e.data?.source === SOURCE_MOUSE_INTERACTION && e.data.type === MOUSE_CLICK) clickCount++;
|
|
212
243
|
let json;
|
|
@@ -231,6 +262,7 @@ function startRecording(config, sessionId) {
|
|
|
231
262
|
if (bufferBytes >= FLUSH_BYTES) flush();
|
|
232
263
|
},
|
|
233
264
|
maskAllInputs: config.maskAllInputs,
|
|
265
|
+
blockSelector: BLOCK_SELECTOR,
|
|
234
266
|
...config.maskAllText ? { maskTextSelector: "*" } : void 0,
|
|
235
267
|
checkoutEveryNms: CHECKOUT_EVERY_MS
|
|
236
268
|
});
|
|
@@ -283,7 +315,8 @@ function startReplaySession(options) {
|
|
|
283
315
|
ingestKey: options.ingestKey,
|
|
284
316
|
sdk: options.sdk,
|
|
285
317
|
maskAllInputs: options.maskAllInputs,
|
|
286
|
-
maskAllText: options.maskAllText
|
|
318
|
+
maskAllText: options.maskAllText,
|
|
319
|
+
getIdentity: options.getIdentity
|
|
287
320
|
};
|
|
288
321
|
let recorder;
|
|
289
322
|
let events;
|