@maple-dev/browser 0.9.0 → 0.10.1

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.
@@ -0,0 +1,723 @@
1
+ import { E as selectorOf, F as scrubUrl, l as onSessionEvent, w as sdkHint, x as ingestHeaders } from "./sink-BOR9_z4b.mjs";
2
+ import { _ as consentRevokedAt, a as detachLogSink, c as resourceAttributes, d as attachSpanStash, f as claimPageSignal, g as consentAllowedSince, h as severityOf, i as attachLogSink, l as SDK_NAME, m as Severity, n as navigationSpanAt, o as emitLog, p as onErrorRecorded, r as onDocumentPageload, s as liveMapleTracer, u as SDK_VERSION, v as hasConsent, y as onConsentChange } from "./src-D4e1o0x4.mjs";
3
+ import { t as installConsoleCapture } from "./console-BrjBnOiX.mjs";
4
+ import { ROOT_CONTEXT, TraceFlags, context, trace } from "@opentelemetry/api";
5
+ import { resourceFromAttributes } from "@opentelemetry/resources";
6
+ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
7
+ import { JsonLogsSerializer, JsonTraceSerializer } from "@opentelemetry/otlp-transformer";
8
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
9
+ import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
10
+ //#region ../sdk-core/src/browser/breadcrumbs.ts
11
+ const MAX_CRUMBS = 50;
12
+ let crumbs = [];
13
+ let emit$1;
14
+ function push(crumb) {
15
+ const last = crumbs.at(-1);
16
+ if (crumb.type === "input" && crumb.target !== void 0 && last?.type === "input" && last.target === crumb.target) {
17
+ crumbs[crumbs.length - 1] = crumb;
18
+ return;
19
+ }
20
+ crumbs.push(crumb);
21
+ if (crumbs.length > MAX_CRUMBS) crumbs.shift();
22
+ }
23
+ function emitConsole(sink, level, message, timestamp, link) {
24
+ const severity = severityOf(level);
25
+ sink({
26
+ severityNumber: severity.number,
27
+ severityText: severity.text,
28
+ body: message,
29
+ timestamp,
30
+ link,
31
+ attributes: {
32
+ "maple.log.source": "console",
33
+ ...link ? { "maple.breadcrumb.type": "console" } : void 0
34
+ }
35
+ });
36
+ }
37
+ /** Start collecting into `sink`. Returns a stop. */
38
+ function startBreadcrumbs(sink, options) {
39
+ if (!options.breadcrumbs && options.captureConsole.length === 0) return () => {};
40
+ emit$1 = sink;
41
+ const live = () => emit$1 === sink;
42
+ const forwarded = new Set(options.captureConsole);
43
+ const stopEvents = options.breadcrumbs ? onSessionEvent((ev) => {
44
+ if (!live() || ev.type !== "click" && ev.type !== "input" && ev.type !== "navigation") return;
45
+ push({
46
+ timestamp: ev.timestamp ?? Date.now(),
47
+ type: ev.type,
48
+ target: ev.targetSelector,
49
+ message: ev.targetText,
50
+ url: ev.url ?? location.href
51
+ });
52
+ }) : () => {};
53
+ const stopConsole = installConsoleCapture((ev) => {
54
+ if (!live() || ev.message === void 0) return;
55
+ const timestamp = Date.now();
56
+ if (ev.level !== void 0 && forwarded.has(ev.level)) {
57
+ emitConsole(sink, ev.level, ev.message, timestamp);
58
+ return;
59
+ }
60
+ if (options.breadcrumbs) push({
61
+ timestamp,
62
+ type: "console",
63
+ level: ev.level,
64
+ message: ev.message
65
+ });
66
+ });
67
+ return () => {
68
+ if (live()) {
69
+ emit$1 = void 0;
70
+ crumbs = [];
71
+ }
72
+ stopEvents();
73
+ stopConsole();
74
+ };
75
+ }
76
+ /** Export the trail so far, linked to the error's span, and start a new one. */
77
+ function flushBreadcrumbs(link) {
78
+ const sink = emit$1;
79
+ if (!sink || crumbs.length === 0) return;
80
+ const trail = crumbs;
81
+ crumbs = [];
82
+ for (const crumb of trail) {
83
+ if (crumb.type === "console") {
84
+ emitConsole(sink, crumb.level, crumb.message ?? "", crumb.timestamp, link);
85
+ continue;
86
+ }
87
+ sink({
88
+ eventName: "maple.browser.breadcrumb",
89
+ severityNumber: Severity.INFO,
90
+ severityText: "INFO",
91
+ body: [
92
+ crumb.type,
93
+ crumb.target,
94
+ crumb.message
95
+ ].filter(Boolean).join(" "),
96
+ timestamp: crumb.timestamp,
97
+ link,
98
+ attributes: {
99
+ "maple.breadcrumb.type": crumb.type,
100
+ ...crumb.target ? { "maple.breadcrumb.target": crumb.target } : void 0,
101
+ ...crumb.url ? { "url.full": scrubUrl(crumb.url) } : void 0
102
+ }
103
+ });
104
+ }
105
+ }
106
+ //#endregion
107
+ //#region ../sdk-core/src/browser/document-timing.ts
108
+ const FETCH_PHASES = [
109
+ [
110
+ "dns",
111
+ (e) => e.domainLookupStart,
112
+ (e) => e.domainLookupEnd
113
+ ],
114
+ [
115
+ "connect",
116
+ (e) => e.connectStart,
117
+ (e) => e.connectEnd
118
+ ],
119
+ [
120
+ "request",
121
+ (e) => e.requestStart,
122
+ (e) => e.responseStart
123
+ ],
124
+ [
125
+ "response",
126
+ (e) => e.responseStart,
127
+ (e) => e.responseEnd
128
+ ]
129
+ ];
130
+ const PAGE_PHASES = [[
131
+ "domProcessing",
132
+ (e) => e.responseEnd,
133
+ (e) => e.domComplete
134
+ ], [
135
+ "loadEvent",
136
+ (e) => e.loadEventStart,
137
+ (e) => e.loadEventEnd
138
+ ]];
139
+ /** Epoch ms for a Navigation Timing offset. */
140
+ const at = (offset) => performance.timeOrigin + offset;
141
+ function spanPhases(child, parent, entry, phases) {
142
+ for (const [name, start, end] of phases) {
143
+ const from = start(entry);
144
+ const to = end(entry);
145
+ if (from <= 0 || to <= from) continue;
146
+ child(parent, name, at(from), at(to));
147
+ }
148
+ }
149
+ function record(child, pageload) {
150
+ const [entry] = performance.getEntriesByType("navigation");
151
+ if (!(entry instanceof PerformanceNavigationTiming) || entry.responseEnd <= 0) return;
152
+ spanPhases(child, child(pageload, "documentFetch", at(entry.fetchStart), at(entry.responseEnd), {
153
+ "url.full": scrubUrl(entry.name),
154
+ ...entry.responseStatus > 0 ? { "http.response.status_code": entry.responseStatus } : void 0,
155
+ ...entry.encodedBodySize > 0 ? { "http.response.body.size": entry.encodedBodySize } : void 0
156
+ }), entry, FETCH_PHASES);
157
+ spanPhases(child, pageload, entry, PAGE_PHASES);
158
+ }
159
+ /** Span the document's load under `pageload`, now or once the `load` event has finished. */
160
+ function recordDocumentTiming(child, pageload) {
161
+ if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") return;
162
+ const run = () => void setTimeout(() => {
163
+ try {
164
+ record(child, pageload);
165
+ } catch {}
166
+ }, 0);
167
+ if (document.readyState === "complete") run();
168
+ else window.addEventListener("load", run, { once: true });
169
+ }
170
+ //#endregion
171
+ //#region ../sdk-core/src/browser/offline.ts
172
+ const DB_NAME = "maple-offline";
173
+ const STORE = "batches";
174
+ const MAX_AGE_MS = 864e5;
175
+ const MAX_BATCHES = 100;
176
+ const isStoredBatch = (value) => typeof value === "object" && value !== null && "id" in value && typeof value.id === "number" && "signal" in value && (value.signal === "traces" || value.signal === "logs") && "body" in value && value.body instanceof Uint8Array && "createdAt" in value && typeof value.createdAt === "number";
177
+ const settle = (request) => new Promise((resolve, reject) => {
178
+ request.onsuccess = () => resolve(request.result);
179
+ request.onerror = () => reject(request.error);
180
+ });
181
+ function openDb() {
182
+ if (typeof indexedDB === "undefined") return Promise.resolve(void 0);
183
+ try {
184
+ const request = indexedDB.open(DB_NAME, 1);
185
+ request.onupgradeneeded = () => {
186
+ request.result.createObjectStore(STORE, {
187
+ keyPath: "id",
188
+ autoIncrement: true
189
+ });
190
+ };
191
+ return settle(request).catch(() => void 0);
192
+ } catch {
193
+ return Promise.resolve(void 0);
194
+ }
195
+ }
196
+ /** FNV-1a, hex: identifies the credentials without storing them. */
197
+ function fingerprint(value) {
198
+ let hash = 2166136261;
199
+ for (let i = 0; i < value.length; i++) {
200
+ hash ^= value.charCodeAt(i);
201
+ hash = Math.imul(hash, 16777619);
202
+ }
203
+ return (hash >>> 0).toString(16);
204
+ }
205
+ function startOfflineQueue(config) {
206
+ const db = openDb();
207
+ const headers = {
208
+ ...config.headers,
209
+ "Content-Type": "application/json"
210
+ };
211
+ const auth = Object.entries(config.headers).find(([name]) => name.toLowerCase() === "authorization");
212
+ const target = `${config.endpoint}|${fingerprint(auth?.[1] ?? "")}`;
213
+ const store = async (mode) => (await db)?.transaction(STORE, mode).objectStore(STORE);
214
+ const add = async (signal, body, createdAt) => {
215
+ if (!hasConsent() || createdAt <= consentRevokedAt()) return;
216
+ const batches = await store("readwrite");
217
+ if (!batches) return;
218
+ await settle(batches.add({
219
+ signal,
220
+ body,
221
+ createdAt,
222
+ target
223
+ }));
224
+ const keys = await settle(batches.getAllKeys());
225
+ for (const key of keys.slice(0, Math.max(0, keys.length - MAX_BATCHES))) await settle(batches.delete(key));
226
+ };
227
+ /** Send what is stored, oldest first. Stops at the first failure, keeping the rest. */
228
+ const drain = async () => {
229
+ const read = await store("readonly");
230
+ const stored = read ? (await settle(read.getAll())).filter(isStoredBatch) : [];
231
+ for (const batch of stored) {
232
+ if (!hasConsent()) return;
233
+ const expired = Date.now() - batch.createdAt > MAX_AGE_MS;
234
+ if (!expired && batch.target !== void 0 && batch.target !== target) continue;
235
+ if (!expired && batch.createdAt > consentRevokedAt()) {
236
+ const response = await fetch(`${config.endpoint}/v1/${batch.signal}`, {
237
+ method: "POST",
238
+ headers,
239
+ body: new Uint8Array(batch.body)
240
+ }).catch(() => void 0);
241
+ if (!response || response.status >= 500 || response.status === 429) return;
242
+ }
243
+ const write = await store("readwrite");
244
+ if (write) await settle(write.delete(batch.id));
245
+ }
246
+ };
247
+ /** The resend in flight: a second call joins it rather than returning before it is done. */
248
+ let inflight;
249
+ const run = async () => {
250
+ try {
251
+ if (typeof navigator !== "undefined" && navigator.locks) await navigator.locks.request(`${DB_NAME}-resend`, () => drain());
252
+ else await drain();
253
+ } catch {}
254
+ };
255
+ const resend = () => {
256
+ if (!hasConsent() || typeof navigator !== "undefined" && navigator.onLine === false) return Promise.resolve();
257
+ inflight ??= run().finally(() => {
258
+ inflight = void 0;
259
+ });
260
+ return inflight;
261
+ };
262
+ const clear = async () => {
263
+ const batches = await store("readwrite");
264
+ if (batches) await settle(batches.clear());
265
+ };
266
+ /** Writes in order: a revoke's clear runs after the writes before it, and `stop` closes the database after all of them. */
267
+ let writes = Promise.resolve();
268
+ const queue = (write) => {
269
+ writes = writes.then(write).catch(() => {});
270
+ };
271
+ const stash = (signal, body) => {
272
+ const createdAt = Date.now();
273
+ if (body && hasConsent()) queue(() => add(signal, body, createdAt));
274
+ };
275
+ const onOnline = () => void resend();
276
+ window.addEventListener("online", onOnline);
277
+ const stopConsent = onConsentChange((allowed) => {
278
+ if (!allowed) queue(clear);
279
+ });
280
+ resend();
281
+ return {
282
+ stash: (signal, body) => {
283
+ if (body.byteLength > 0) stash(signal, body);
284
+ },
285
+ resend,
286
+ stop: () => {
287
+ window.removeEventListener("online", onOnline);
288
+ stopConsent();
289
+ writes.then(() => db).then((opened) => opened?.close());
290
+ }
291
+ };
292
+ }
293
+ //#endregion
294
+ //#region ../sdk-core/src/browser/perf.ts
295
+ /** A frame this long is visible jank; the Long Animation Frames API reports from 50ms. */
296
+ const LONG_FRAME_MS = 100;
297
+ /** INP's "needs improvement" line. */
298
+ const SLOW_INTERACTION_MS = 200;
299
+ const epoch = (offset) => performance.timeOrigin + offset;
300
+ let sink;
301
+ function span(name, start, duration, attributes) {
302
+ sink?.(name, epoch(start), epoch(start + duration), attributes);
303
+ }
304
+ function scriptsOf(entry) {
305
+ const scripts = "scripts" in entry ? entry.scripts : void 0;
306
+ if (!Array.isArray(scripts)) return [];
307
+ return scripts.flatMap((script) => {
308
+ if (typeof script !== "object" || script === null || !("duration" in script)) return [];
309
+ if (typeof script.duration !== "number") return [];
310
+ const invoker = "invoker" in script ? script.invoker : void 0;
311
+ const sourceURL = "sourceURL" in script ? script.sourceURL : void 0;
312
+ const sourceFunctionName = "sourceFunctionName" in script ? script.sourceFunctionName : void 0;
313
+ return [{
314
+ duration: script.duration,
315
+ invoker: typeof invoker === "string" && invoker !== "" ? invoker : void 0,
316
+ sourceURL: typeof sourceURL === "string" && sourceURL !== "" ? sourceURL : void 0,
317
+ sourceFunctionName: typeof sourceFunctionName === "string" && sourceFunctionName !== "" ? sourceFunctionName : void 0
318
+ }];
319
+ });
320
+ }
321
+ /** Exported for tests: headless Chromium lists LoAF as supported but never renders a frame to report. */
322
+ function onLongFrame(entry) {
323
+ if (entry.duration < LONG_FRAME_MS) return;
324
+ const blocking = "blockingDuration" in entry && typeof entry.blockingDuration === "number" ? entry.blockingDuration : void 0;
325
+ const longest = scriptsOf(entry).sort((a, b) => b.duration - a.duration)[0];
326
+ span(entry.entryType === "longtask" ? "longtask" : "longAnimationFrame", entry.startTime, entry.duration, {
327
+ ...blocking !== void 0 ? { "maple.browser.frame.blocking_duration_ms": Math.round(blocking) } : void 0,
328
+ ...longest?.sourceURL ? { "code.file.path": scrubUrl(longest.sourceURL) } : void 0,
329
+ ...longest?.sourceFunctionName ? { "code.function.name": longest.sourceFunctionName } : void 0,
330
+ ...longest?.invoker ? { "maple.browser.script.invoker": longest.invoker } : void 0,
331
+ ...longest ? { "maple.browser.script.duration_ms": Math.round(longest.duration) } : void 0
332
+ });
333
+ }
334
+ function observe(type, onEntries, options = {}) {
335
+ if (typeof PerformanceObserver === "undefined" || !PerformanceObserver.supportedEntryTypes?.includes(type)) return () => {};
336
+ const observer = new PerformanceObserver((list) => onEntries(list.getEntries()));
337
+ observer.observe({
338
+ type,
339
+ buffered: true,
340
+ ...options
341
+ });
342
+ return () => observer.disconnect();
343
+ }
344
+ const processingOf = (entry) => entry.processingEnd - entry.processingStart;
345
+ /**
346
+ * The interaction an event belongs to. `0` means "not an interaction"; an engine
347
+ * without `interactionId` gets a per-event key, so its slow events are still spanned.
348
+ */
349
+ function interactionKey(entry) {
350
+ const id = entry.interactionId;
351
+ if (id === 0) return void 0;
352
+ return id === void 0 ? `${entry.name}:${Math.round(entry.startTime)}` : `id:${id}`;
353
+ }
354
+ function spanInteraction(entry) {
355
+ span(`interaction ${entry.name}`, entry.startTime, entry.duration, {
356
+ "maple.browser.interaction.input_delay_ms": Math.round(entry.processingStart - entry.startTime),
357
+ "maple.browser.interaction.processing_ms": Math.round(processingOf(entry)),
358
+ "maple.browser.interaction.presentation_ms": Math.round(entry.startTime + entry.duration - entry.processingEnd),
359
+ ...entry.target instanceof Element ? { "maple.browser.interaction.target": selectorOf(entry.target) } : void 0
360
+ });
361
+ }
362
+ function startPerf(startSpan, options) {
363
+ const stops = [];
364
+ if (options.longFrames || options.slowInteractions) sink = startSpan;
365
+ if (options.longFrames) {
366
+ const hasLoaf = typeof PerformanceObserver !== "undefined" && PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame") === true;
367
+ stops.push(observe(hasLoaf ? "long-animation-frame" : "longtask", (entries) => {
368
+ for (const entry of entries) onLongFrame(entry);
369
+ }));
370
+ }
371
+ if (options.slowInteractions) {
372
+ const seen = /* @__PURE__ */ new Set();
373
+ stops.push(observe("event", (entries) => {
374
+ const byInteraction = /* @__PURE__ */ new Map();
375
+ for (const entry of entries) {
376
+ if (!(entry instanceof PerformanceEventTiming)) continue;
377
+ const key = interactionKey(entry);
378
+ if (key === void 0 || entry.duration < SLOW_INTERACTION_MS || seen.has(key)) continue;
379
+ const best = byInteraction.get(key);
380
+ if (!best || processingOf(entry) > processingOf(best)) byInteraction.set(key, entry);
381
+ }
382
+ for (const [id, entry] of byInteraction) {
383
+ seen.add(id);
384
+ spanInteraction(entry);
385
+ }
386
+ if (seen.size > 500) seen.clear();
387
+ }, { durationThreshold: SLOW_INTERACTION_MS }));
388
+ }
389
+ return () => {
390
+ for (const stop of stops) stop();
391
+ if (sink === startSpan) sink = void 0;
392
+ };
393
+ }
394
+ //#endregion
395
+ //#region ../sdk-core/src/browser/reports.ts
396
+ /** Repeats of one report (a blocked image in a loop) are sent once per page, up to this many kinds. */
397
+ const MAX_REPORTS = 50;
398
+ /** Report bodies expose their fields as getters that only `toJSON` serializes. */
399
+ function parseReportBody(body) {
400
+ const raw = body === null || body === void 0 ? void 0 : JSON.parse(JSON.stringify(body));
401
+ const fields = new Map(typeof raw === "object" && raw !== null ? Object.entries(raw) : []);
402
+ const str = (key) => {
403
+ const value = fields.get(key);
404
+ return typeof value === "string" && value !== "" ? value : void 0;
405
+ };
406
+ const int = (key) => {
407
+ const value = fields.get(key);
408
+ return typeof value === "number" && value > 0 ? value : void 0;
409
+ };
410
+ return {
411
+ effectiveDirective: str("effectiveDirective") ?? str("violatedDirective"),
412
+ blockedURL: str("blockedURL") ?? str("blockedURI"),
413
+ disposition: str("disposition"),
414
+ sourceFile: str("sourceFile"),
415
+ lineNumber: int("lineNumber"),
416
+ columnNumber: int("columnNumber"),
417
+ id: str("id"),
418
+ message: str("message")
419
+ };
420
+ }
421
+ function sourceAttributes(fields) {
422
+ return {
423
+ ...fields.sourceFile ? { "code.file.path": scrubUrl(fields.sourceFile) } : void 0,
424
+ ...fields.lineNumber ? { "code.line.number": fields.lineNumber } : void 0,
425
+ ...fields.columnNumber ? { "code.column.number": fields.columnNumber } : void 0
426
+ };
427
+ }
428
+ function startReports(emitLog, options) {
429
+ if (!options.csp && !options.browserReports) return () => {};
430
+ const seen = /* @__PURE__ */ new Set();
431
+ const once = (key) => {
432
+ if (seen.has(key) || seen.size >= MAX_REPORTS) return false;
433
+ seen.add(key);
434
+ return true;
435
+ };
436
+ const csp = (fields) => {
437
+ const directive = fields.effectiveDirective ?? "unknown";
438
+ const blocked = scrubUrl(fields.blockedURL ?? "inline");
439
+ if (!once(`csp ${directive} ${blocked}`)) return;
440
+ emitLog({
441
+ eventName: "maple.browser.csp_violation",
442
+ severityNumber: Severity.WARN,
443
+ severityText: "WARN",
444
+ body: `${directive} blocked ${blocked}`,
445
+ attributes: {
446
+ "maple.csp.effective_directive": directive,
447
+ "maple.csp.blocked_uri": blocked,
448
+ ...fields.disposition ? { "maple.csp.disposition": fields.disposition } : void 0,
449
+ "url.full": scrubUrl(location.href),
450
+ ...sourceAttributes(fields)
451
+ }
452
+ });
453
+ };
454
+ const browserReport = (type, fields) => {
455
+ const id = fields.id ?? "unknown";
456
+ if (!once(`${type} ${id}`)) return;
457
+ emitLog({
458
+ eventName: "maple.browser.report",
459
+ severityNumber: Severity.WARN,
460
+ severityText: "WARN",
461
+ body: fields.message ?? `${type}: ${id}`,
462
+ attributes: {
463
+ "maple.report.type": type,
464
+ "maple.report.id": id,
465
+ ...sourceAttributes(fields)
466
+ }
467
+ });
468
+ };
469
+ const stops = [];
470
+ if (typeof ReportingObserver === "function") {
471
+ const types = [...options.csp ? ["csp-violation"] : [], ...options.browserReports ? ["deprecation", "intervention"] : []];
472
+ const observer = new ReportingObserver((reports) => {
473
+ for (const report of reports) {
474
+ const fields = parseReportBody(report.body);
475
+ if (report.type === "csp-violation") csp(fields);
476
+ else browserReport(report.type ?? "report", fields);
477
+ }
478
+ }, {
479
+ types,
480
+ buffered: true
481
+ });
482
+ observer.observe();
483
+ stops.push(() => observer.disconnect());
484
+ }
485
+ if (options.csp) {
486
+ const onViolation = (event) => csp({
487
+ effectiveDirective: event.effectiveDirective,
488
+ blockedURL: event.blockedURI || void 0,
489
+ disposition: event.disposition,
490
+ sourceFile: event.sourceFile,
491
+ lineNumber: event.lineNumber,
492
+ columnNumber: event.columnNumber
493
+ });
494
+ document.addEventListener("securitypolicyviolation", onViolation);
495
+ stops.push(() => document.removeEventListener("securitypolicyviolation", onViolation));
496
+ }
497
+ return () => {
498
+ for (const stop of stops) stop();
499
+ };
500
+ }
501
+ //#endregion
502
+ //#region ../sdk-core/src/browser/web-vitals.ts
503
+ let registered = false;
504
+ let emit;
505
+ let pageload;
506
+ function report(metric) {
507
+ emit?.({
508
+ eventName: "browser.web_vital",
509
+ severityNumber: Severity.INFO,
510
+ severityText: "INFO",
511
+ attributes: {
512
+ "browser.web_vital.name": metric.name.toLowerCase(),
513
+ "browser.web_vital.value": metric.value,
514
+ "browser.web_vital.delta": metric.delta,
515
+ "browser.web_vital.id": metric.id,
516
+ "browser.web_vital.rating": metric.rating,
517
+ "browser.web_vital.navigation_type": metric.navigationType,
518
+ "url.path": scrubUrl(location.pathname)
519
+ },
520
+ link: pageload?.()
521
+ });
522
+ }
523
+ /** Report vitals through `sink`, linked to the document's pageload span when there is one. Returns a stop. */
524
+ function startWebVitals(sink, getPageload) {
525
+ emit = sink;
526
+ pageload = getPageload;
527
+ if (!registered) {
528
+ registered = true;
529
+ for (const on of [
530
+ onCLS,
531
+ onFCP,
532
+ onINP,
533
+ onLCP,
534
+ onTTFB
535
+ ]) on(report);
536
+ }
537
+ return () => {
538
+ if (emit !== sink) return;
539
+ emit = void 0;
540
+ pageload = void 0;
541
+ };
542
+ }
543
+ //#endregion
544
+ //#region src/deferred/logs.ts
545
+ /** Same rule as spans: drop records made while consent was absent, even if it is granted by flush time. */
546
+ var ConsentLogExporter = class {
547
+ inner;
548
+ constructor(inner) {
549
+ this.inner = inner;
550
+ }
551
+ export(logs, callback) {
552
+ const since = consentAllowedSince();
553
+ const eligible = hasConsent() && Number.isFinite(since) ? logs.filter((log) => log.hrTime[0] * 1e3 + log.hrTime[1] / 1e6 >= since) : [];
554
+ if (eligible.length === 0) {
555
+ callback({ code: 0 });
556
+ return;
557
+ }
558
+ this.inner.export(eligible, callback);
559
+ }
560
+ forceFlush() {
561
+ return this.inner.forceFlush?.() ?? Promise.resolve();
562
+ }
563
+ shutdown() {
564
+ return this.inner.shutdown();
565
+ }
566
+ };
567
+ /** Hands batches the exporter gave up on to the offline queue. */
568
+ var OfflineLogExporter = class {
569
+ inner;
570
+ stash;
571
+ constructor(inner, stash) {
572
+ this.inner = inner;
573
+ this.stash = stash;
574
+ }
575
+ export(logs, callback) {
576
+ this.inner.export(logs, (result) => {
577
+ if (result.code !== 0) this.stash(logs);
578
+ callback(result);
579
+ });
580
+ }
581
+ forceFlush() {
582
+ return this.inner.forceFlush?.() ?? Promise.resolve();
583
+ }
584
+ shutdown() {
585
+ return this.inner.shutdown();
586
+ }
587
+ };
588
+ /** Start the OTel logs pipeline and drain the eager queue into it. Returns a shutdown. */
589
+ function startLogs(config, stashOffline) {
590
+ const otlp = new OTLPLogExporter({
591
+ url: `${config.endpoint}/v1/logs`,
592
+ headers: ingestHeaders({
593
+ ingestKey: config.ingestKey,
594
+ sdk: sdkHint(SDK_NAME, SDK_VERSION)
595
+ })
596
+ });
597
+ const exporter = new ConsentLogExporter(stashOffline ? new OfflineLogExporter(otlp, stashOffline) : otlp);
598
+ const provider = new LoggerProvider({
599
+ resource: resourceFromAttributes(resourceAttributes(config)),
600
+ processors: [new BatchLogRecordProcessor({
601
+ exporter,
602
+ scheduledDelayMillis: 2e3
603
+ })]
604
+ });
605
+ const logger = provider.getLogger(SDK_NAME, SDK_VERSION);
606
+ attachLogSink((record) => logger.emit({
607
+ eventName: record.eventName,
608
+ severityNumber: record.severityNumber,
609
+ severityText: record.severityText,
610
+ body: record.body,
611
+ attributes: record.attributes,
612
+ timestamp: record.timestamp,
613
+ context: record.link ? trace.setSpanContext(ROOT_CONTEXT, record.link) : ROOT_CONTEXT
614
+ }));
615
+ return async () => {
616
+ detachLogSink();
617
+ await provider.shutdown();
618
+ };
619
+ }
620
+ //#endregion
621
+ //#region src/deferred/index.ts
622
+ /** Jank spans nest under the navigation open when they began, on Maple's own provider. */
623
+ const perfSpan = (name, startMs, endMs, attributes) => {
624
+ const tracer = hasConsent() ? liveMapleTracer(SDK_NAME, SDK_VERSION) : void 0;
625
+ if (!tracer) return;
626
+ const navigation = navigationSpanAt(startMs);
627
+ const parent = navigation ? trace.setSpan(context.active(), navigation) : context.active();
628
+ tracer.startSpan(name, {
629
+ startTime: startMs,
630
+ attributes
631
+ }, parent).end(endMs);
632
+ };
633
+ /** Page signals this copy leased; another Maple SDK copy may already collect the rest. */
634
+ function leaseSignals(wanted) {
635
+ const releases = /* @__PURE__ */ new Map();
636
+ for (const signal of PAGE_SIGNALS) {
637
+ const release = wanted[signal] ? claimPageSignal(signal) : void 0;
638
+ if (release) releases.set(signal, release);
639
+ }
640
+ return {
641
+ has: (signal) => releases.has(signal),
642
+ release: () => {
643
+ for (const release of releases.values()) release();
644
+ }
645
+ };
646
+ }
647
+ const PAGE_SIGNALS = [
648
+ "breadcrumbs",
649
+ "browserReports",
650
+ "console",
651
+ "csp",
652
+ "longFrames",
653
+ "slowInteractions",
654
+ "webVitals"
655
+ ];
656
+ function startDeferred(config) {
657
+ let pageload;
658
+ onDocumentPageload((tracer, span) => {
659
+ pageload = span.spanContext();
660
+ if ((span.spanContext().traceFlags & TraceFlags.SAMPLED) === 0) return;
661
+ const child = (parent, name, startMs, endMs, attributes) => {
662
+ const next = tracer.startSpan(name, {
663
+ startTime: startMs,
664
+ attributes
665
+ }, trace.setSpan(context.active(), parent));
666
+ next.end(endMs);
667
+ return next;
668
+ };
669
+ recordDocumentTiming(child, span);
670
+ });
671
+ const leased = leaseSignals({
672
+ webVitals: config.webVitals,
673
+ breadcrumbs: config.breadcrumbs,
674
+ console: config.captureConsole.length > 0,
675
+ csp: config.reportCsp,
676
+ browserReports: config.reportBrowser,
677
+ longFrames: config.longFrames,
678
+ slowInteractions: config.slowInteractions
679
+ });
680
+ const stopVitals = leased.has("webVitals") ? startWebVitals(emitLog, () => pageload) : () => {};
681
+ const stopBreadcrumbs = startBreadcrumbs(emitLog, {
682
+ breadcrumbs: leased.has("breadcrumbs"),
683
+ captureConsole: leased.has("console") ? config.captureConsole : []
684
+ });
685
+ const stopErrorListener = onErrorRecorded(flushBreadcrumbs);
686
+ const stopReports = startReports(emitLog, {
687
+ csp: leased.has("csp"),
688
+ browserReports: leased.has("browserReports")
689
+ });
690
+ const stopPerf = startPerf(perfSpan, {
691
+ longFrames: leased.has("longFrames"),
692
+ slowInteractions: leased.has("slowInteractions")
693
+ });
694
+ const offline = config.offlineQueue ? startOfflineQueue({
695
+ endpoint: config.endpoint,
696
+ headers: ingestHeaders({
697
+ ingestKey: config.ingestKey,
698
+ sdk: sdkHint(SDK_NAME, SDK_VERSION)
699
+ })
700
+ }) : void 0;
701
+ attachSpanStash(offline && ((spans) => {
702
+ const body = spans.length > 0 ? JsonTraceSerializer.serializeRequest(spans) : void 0;
703
+ if (body) offline.stash("traces", body);
704
+ }));
705
+ const stopLogs = startLogs(config, offline && ((logs) => {
706
+ const body = logs.length > 0 ? JsonLogsSerializer.serializeRequest(logs) : void 0;
707
+ if (body) offline.stash("logs", body);
708
+ }));
709
+ return async () => {
710
+ stopVitals();
711
+ onDocumentPageload(void 0);
712
+ stopErrorListener();
713
+ stopBreadcrumbs();
714
+ stopReports();
715
+ stopPerf();
716
+ leased.release();
717
+ await stopLogs();
718
+ attachSpanStash(void 0);
719
+ offline?.stop();
720
+ };
721
+ }
722
+ //#endregion
723
+ export { startDeferred };