@flareapp/js 2.6.0 → 2.8.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.
@@ -0,0 +1,2497 @@
1
+ let _flareapp_core = require("@flareapp/core");
2
+
3
+ //#region src/browser/BrowserFlushScheduler.ts
4
+ var BrowserFlushScheduler = class {
5
+ register(flush) {
6
+ if (typeof document === "undefined" || !document) return;
7
+ document.addEventListener("visibilitychange", () => {
8
+ if (document.visibilityState === "hidden") flush({ keepalive: true });
9
+ });
10
+ if (typeof window !== "undefined" && window) window.addEventListener("pagehide", () => flush({ keepalive: true }));
11
+ }
12
+ };
13
+
14
+ //#endregion
15
+ //#region src/browser/context/cookie.ts
16
+ /**
17
+ * Parses `document.cookie` into `http.request.cookies`, redacting the value of any cookie whose name
18
+ * matches `denylist`. Null-prototype accumulator so a cookie named `__proto__` is stored, not dropped.
19
+ */
20
+ function cookie(denylist) {
21
+ if (!window.document.cookie) return {};
22
+ const cookies = Object.create(null);
23
+ window.document.cookie.split("; ").forEach((rawCookie) => {
24
+ const idx = rawCookie.indexOf("=");
25
+ if (idx === -1) {
26
+ cookies[rawCookie] = denylist.test(rawCookie) ? "[redacted]" : "";
27
+ return;
28
+ }
29
+ const name = rawCookie.slice(0, idx);
30
+ const value = rawCookie.slice(idx + 1);
31
+ cookies[name] = denylist.test(name) ? "[redacted]" : value;
32
+ });
33
+ return { "http.request.cookies": cookies };
34
+ }
35
+
36
+ //#endregion
37
+ //#region src/browser/context/request.ts
38
+ /**
39
+ * @param hrefOverride when set, the `url.*` attributes come from it instead of the live
40
+ * `window.location.href` (a framework navigation root whose router knows the destination
41
+ * before the URL commits). The override is pre-validated by the caller.
42
+ */
43
+ function request(urlDenylist, hrefOverride) {
44
+ return {
45
+ ...(0, _flareapp_core.urlAttributes)(hrefOverride ?? window.location.href, urlDenylist),
46
+ "user_agent.original": window.navigator.userAgent,
47
+ "http.request.referrer": (0, _flareapp_core.redactUrlQuery)(window.document.referrer, urlDenylist),
48
+ "document.ready_state": window.document.readyState
49
+ };
50
+ }
51
+
52
+ //#endregion
53
+ //#region src/browser/context/collectBrowser.ts
54
+ function browserEntryPoint(config, urlOverride) {
55
+ if (typeof window === "undefined") return { "flare.entry_point.type": "web" };
56
+ const attrs = { "flare.entry_point.type": "web" };
57
+ const href = urlOverride ? urlOverride.href : window?.location?.href;
58
+ if (href) {
59
+ attrs["flare.entry_point.value"] = (0, _flareapp_core.redactUrlQuery)(href, config.urlDenylist);
60
+ const pathname = urlOverride ? urlOverride.pathname : window?.location?.pathname;
61
+ if (pathname) {
62
+ attrs["flare.entry_point.handler.identifier"] = pathname;
63
+ attrs["http.route"] = pathname;
64
+ attrs["flare.entry_point.handler.type"] = "browser";
65
+ }
66
+ }
67
+ return attrs;
68
+ }
69
+ const collectBrowser = (config) => {
70
+ const attrs = { ...browserEntryPoint(config) };
71
+ if (typeof window === "undefined") return attrs;
72
+ if (window?.location?.hostname) attrs["host.name"] = window.location.hostname;
73
+ Object.assign(attrs, request(config.urlDenylist));
74
+ Object.assign(attrs, cookie(config.urlDenylist));
75
+ return attrs;
76
+ };
77
+
78
+ //#endregion
79
+ //#region src/tracing/internalRequest.ts
80
+ /**
81
+ * Marks a request the SDK makes for its own bookkeeping (right now: fetching a source file so a
82
+ * stack frame can show a code snippet). The fetch patch passes those straight through: they are
83
+ * not the app's traffic, so tracing them puts a span in the customer's waterfall for a request
84
+ * their code never made, and propagating a `traceparent` on them is just as wrong.
85
+ *
86
+ * Flare's ingest calls are excluded by URL instead (`isFlareIngestUrl`), because their endpoints
87
+ * are known up front. A snippet fetch targets the customer's own asset, so only the caller knows.
88
+ */
89
+ const INTERNAL_REQUEST_KEY = "__flare_internal_request__";
90
+ /** An init that marks the request as Flare's own. Unknown init keys are ignored by `fetch`. */
91
+ function internalRequestInit(init) {
92
+ return {
93
+ ...init,
94
+ [INTERNAL_REQUEST_KEY]: true
95
+ };
96
+ }
97
+ function isInternalRequest(init) {
98
+ return init?.[INTERNAL_REQUEST_KEY] === true;
99
+ }
100
+
101
+ //#endregion
102
+ //#region src/browser/FetchFileReader.ts
103
+ /**
104
+ * Fetches source files so the stack-trace builder can render a snippet around the offending line.
105
+ * Only http(s) is fetched: other schemes (chrome-extension://, file://, blob:, data:) would cross a
106
+ * privilege boundary or hit a CORS/CSP wall for nothing. Returns null on any failure, never throws.
107
+ */
108
+ var FetchFileReader = class {
109
+ read(url) {
110
+ if (!/^https?:\/\//i.test(url)) return Promise.resolve(null);
111
+ return fetch(url, internalRequestInit()).then((response) => {
112
+ if (response.status !== 200) return null;
113
+ return response.text();
114
+ }).catch(() => null);
115
+ }
116
+ };
117
+
118
+ //#endregion
119
+ //#region src/env/index.ts
120
+ const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.8.0" : "?";
121
+
122
+ //#endregion
123
+ //#region src/tracing/fill.ts
124
+ /**
125
+ * Replace `source[name]` with `replacer(original)`, tagging the wrapper with a
126
+ * non-enumerable `__flare_original__` so the patch is idempotent and reversible.
127
+ * Ported from Sentry's `fill` (packages/core/src/utils/object.ts), minus the
128
+ * prototype/own-property copying we do not need for `fetch`.
129
+ */
130
+ function fill(source, name, replacer) {
131
+ const original = source[name];
132
+ if (typeof original !== "function") return;
133
+ if (original.__flare_original__) return;
134
+ const wrapped = replacer(original);
135
+ Object.defineProperty(wrapped, "__flare_original__", {
136
+ value: original,
137
+ enumerable: false,
138
+ configurable: true,
139
+ writable: true
140
+ });
141
+ source[name] = wrapped;
142
+ }
143
+ /** Restore a previously `fill`ed property to its original. Safe if never filled. */
144
+ function unfill(source, name) {
145
+ const current = source[name];
146
+ if (current && current.__flare_original__) source[name] = current.__flare_original__;
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/tracing/createPatcher.ts
151
+ /**
152
+ * One `installed` flag for the whole patch set, not per method: XHR's `open` records what `send` reads,
153
+ * so a third party wrapping one of them must never leave the set half patched.
154
+ *
155
+ * Target is passed per call, not captured, because callers look it up fresh (`globalThis.fetch` may not
156
+ * exist yet under SSR).
157
+ */
158
+ function createPatcher() {
159
+ let installed = false;
160
+ let names = [];
161
+ return {
162
+ get installed() {
163
+ return installed;
164
+ },
165
+ install(target, patches) {
166
+ if (installed) return;
167
+ function applyOne(name) {
168
+ const wrap = patches[name];
169
+ if (wrap) fill(target, name, wrap);
170
+ }
171
+ names = Object.keys(patches);
172
+ for (const name of names) applyOne(name);
173
+ installed = true;
174
+ },
175
+ uninstall(target) {
176
+ if (!installed) return;
177
+ if (!names.every((name) => {
178
+ const current = target[name];
179
+ return typeof current !== "function" || Boolean(current.__flare_original__);
180
+ })) return;
181
+ for (const name of names) unfill(target, name);
182
+ installed = false;
183
+ }
184
+ };
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/tracing/propagation.ts
189
+ /** Follows OTel/Sentry `tracePropagationTargets`: same-origin by default, `[]` disables all. */
190
+ function shouldPropagate(url, absoluteUrl, currentOrigin, targets) {
191
+ if (targets) {
192
+ if (targets.length === 0) return false;
193
+ return targets.some((t) => typeof t === "string" ? url.includes(t) : t.test(url));
194
+ }
195
+ return absoluteUrl !== null && absoluteUrl.origin === currentOrigin;
196
+ }
197
+ /** Null on a throwing or malformed entry: the caller then passes the source through untouched, so a
198
+ * bad merge never breaks the host request. */
199
+ function headerPairsFrom(source) {
200
+ try {
201
+ const pairs = [];
202
+ for (const entry of source) {
203
+ if (entry === null || typeof entry !== "object") return null;
204
+ const pair = Array.from(entry);
205
+ if (pair.length !== 2) return null;
206
+ pairs.push([String(pair[0]), String(pair[1])]);
207
+ }
208
+ return pairs;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ /** Fetch accepts any iterable of string pairs as HeadersInit (Map, URLSearchParams, cross-realm Headers). */
214
+ function isIterable(value) {
215
+ return value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.iterator] === "function";
216
+ }
217
+ /**
218
+ * A new `RequestInit` carrying `traceparent`, without mutating the caller's `Request` or `init`.
219
+ * Caller-wins: a `traceparent` the caller already set is left alone, matching XHR's
220
+ * `hasAppTraceparent` skip. Returning an init rather than a rebuilt `Request` keeps the caller's
221
+ * single-shot body intact.
222
+ */
223
+ function mergeTraceparentHeader(input, init, traceparent) {
224
+ const source = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
225
+ let headers;
226
+ if (source instanceof Headers) {
227
+ if (source.has("traceparent")) return init;
228
+ headers = new Headers(source);
229
+ headers.set("traceparent", traceparent);
230
+ } else if (Array.isArray(source)) {
231
+ if (source.some(([k]) => String(k).toLowerCase() === "traceparent")) return init;
232
+ headers = [...source, ["traceparent", traceparent]];
233
+ } else if (isIterable(source)) {
234
+ const pairs = headerPairsFrom(source);
235
+ if (pairs === null) headers = source;
236
+ else if (pairs.some(([k]) => k.toLowerCase() === "traceparent")) return init;
237
+ else headers = [...pairs, ["traceparent", traceparent]];
238
+ } else if (source) {
239
+ if (Object.keys(source).some((k) => k.toLowerCase() === "traceparent")) return init;
240
+ headers = {
241
+ ...source,
242
+ traceparent
243
+ };
244
+ } else headers = { traceparent };
245
+ const result = { headers };
246
+ if (init) {
247
+ const descriptors = Object.getOwnPropertyDescriptors(init);
248
+ delete descriptors.headers;
249
+ Object.defineProperties(result, descriptors);
250
+ }
251
+ if (result.duplex === void 0 && typeof Request !== "undefined" && input instanceof Request && input.body != null) result.duplex = "half";
252
+ return result;
253
+ }
254
+
255
+ //#endregion
256
+ //#region src/tracing/httpRequestSpan.ts
257
+ const INLINE_SCHEMES = new Set(["data:", "blob:"]);
258
+ /** The real browser context. Falls back to the origin where there is no document (SSR, tests). */
259
+ function browserUrlContext() {
260
+ const origin = globalThis.location?.origin ?? "";
261
+ return {
262
+ origin,
263
+ base: () => globalThis.document?.baseURI || origin
264
+ };
265
+ }
266
+ /** Resolve `url` to an absolute URL against `base`, or null if it cannot be parsed. */
267
+ function safeAbsolute(url, base) {
268
+ try {
269
+ return new URL(url, base || void 0);
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+ let ingestCacheKey = null;
275
+ let ingestCacheHrefs = [];
276
+ function resolvedIngestHrefs(config, base) {
277
+ const raw = [
278
+ config.ingestUrl,
279
+ config.logsIngestUrl,
280
+ config.tracesIngestUrl
281
+ ];
282
+ const key = `${base} ${raw.join(" ")}`;
283
+ if (key !== ingestCacheKey) {
284
+ ingestCacheKey = key;
285
+ ingestCacheHrefs = raw.filter((u) => typeof u === "string" && u.length > 0).map((u) => safeAbsolute(u, base)).filter((u) => u !== null).map((u) => u.href);
286
+ }
287
+ return ingestCacheHrefs;
288
+ }
289
+ function matchesIngestHref(href, ingestHref) {
290
+ if (!href.startsWith(ingestHref)) return false;
291
+ const next = href.charAt(ingestHref.length);
292
+ return next === "" || next === "/" || next === "?" || next === "#";
293
+ }
294
+ /**
295
+ * True when `resolved` targets one of Flare's own ingest endpoints (never traced). The configured
296
+ * URLs are resolved against `base` first: a relative one (a customer proxying ingest through their
297
+ * own origin) would otherwise never match, so every flush POST would open a span that arms the next
298
+ * flush, forever.
299
+ */
300
+ function isFlareIngestUrl(resolved, config, base) {
301
+ if (!resolved) return false;
302
+ return resolvedIngestHrefs(config, base).some((ingestHref) => matchesIngestHref(resolved.href, ingestHref));
303
+ }
304
+ /**
305
+ * Shared request-span attributes for a fetch/XHR call. The `url.*` attributes are redacted the same
306
+ * way as error reports, so tokens and reset codes never leak.
307
+ */
308
+ function requestSpanAttributes(method, resolved, url, config) {
309
+ return {
310
+ "http.request.method": method,
311
+ ...(0, _flareapp_core.urlAttributes)(resolved ? resolved.href : url, config.urlDenylist),
312
+ ...resolved ? { "server.address": resolved.hostname } : {},
313
+ ...resolved && resolved.port ? { "server.port": Number(resolved.port) } : {}
314
+ };
315
+ }
316
+ /**
317
+ * Completion mapping shared by fetch and XHR: record the status and mark an error on 5xx.
318
+ * `zeroIsError` additionally maps status 0 to error. XHR passes it only for http(s), where status
319
+ * 0 at DONE is always a network/CORS failure or abort; file:// and custom schemes return 0 on
320
+ * success, so it isn't set there. Fetch never passes it (an opaque no-cors response is 0, not error).
321
+ */
322
+ function endHttpRequestSpan(span, status, opts) {
323
+ span.setAttribute("http.response.status_code", status);
324
+ if (status >= 500 || opts?.zeroIsError && status === 0) span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
325
+ span.end();
326
+ }
327
+ function finishHttpSpanError(span, error) {
328
+ span.setStatus({
329
+ code: _flareapp_core.SpanStatusCode.Error,
330
+ message: error instanceof Error ? error.message : String(error)
331
+ });
332
+ span.end();
333
+ }
334
+ /**
335
+ * Propagation gate plus `traceparent` build shared by fetch and XHR. Returns null when
336
+ * `shouldPropagate` rejects the URL (caller then skips header injection).
337
+ */
338
+ function traceparentFor(span, resolved, url, origin, config) {
339
+ if (!shouldPropagate(resolved ? resolved.href : url, resolved, origin, config.tracePropagationTargets)) return null;
340
+ return (0, _flareapp_core.buildTraceparent)(span.traceId, span.spanId, span.isRecording);
341
+ }
342
+ /**
343
+ * Open a request span for one outgoing fetch or XHR call. Null means the URL is one of Flare's own
344
+ * ingest endpoints, so the caller passes the request through untraced.
345
+ *
346
+ * `absoluteUrl` comes back with the span because both callers need it afterwards: for the traceparent
347
+ * gate, and for XHR's http(s)-only status-0 rule.
348
+ */
349
+ function startHttpRequestSpan(tracer, request) {
350
+ const { method, url, urls, spanType } = request;
351
+ const config = tracer.config;
352
+ const base = urls.base();
353
+ const resolved = safeAbsolute(url, base);
354
+ if (isFlareIngestUrl(resolved, config, base)) return null;
355
+ if (resolved && INLINE_SCHEMES.has(resolved.protocol)) return null;
356
+ const pathname = resolved ? resolved.pathname : url;
357
+ return {
358
+ span: tracer.startSpan(`${method} ${pathname}`, {
359
+ spanType,
360
+ attributes: requestSpanAttributes(method, resolved, url, config)
361
+ }),
362
+ absoluteUrl: resolved
363
+ };
364
+ }
365
+
366
+ //#endregion
367
+ //#region src/tracing/instrumentationGuard.ts
368
+ /** For a callback the host invokes: a router guard, a store subscriber, ... */
369
+ function insulate(fn) {
370
+ return (...args) => {
371
+ try {
372
+ fn(...args);
373
+ } catch {}
374
+ };
375
+ }
376
+ /** Invoke a teardown fn now (if present), swallowing any throw. For cleanup chains. */
377
+ function safeInvoke(fn) {
378
+ try {
379
+ fn?.();
380
+ } catch {}
381
+ }
382
+ const instrumented = /* @__PURE__ */ new WeakMap();
383
+ /**
384
+ * Instrument `target` at most once at a time, tearing down any prior instrumentation of the same object
385
+ * first. Vite HMR re-runs boot code against a router that survives the reload, so without this every
386
+ * cycle appends another listener set that is never removed. Keyed on the object, so a genuinely new
387
+ * router is unaffected.
388
+ *
389
+ * `install` hands each teardown to `track` as it produces it. A router's own `subscribe` / `on` / guard
390
+ * registration can throw, and `install` runs during the host's bootstrap, so a throw part-way through
391
+ * unwinds what already succeeded (newest first) and stops here rather than reaching the host.
392
+ *
393
+ * @returns the cleanup, or a no-op when the install failed and already unwound itself.
394
+ */
395
+ function instrumentOnce(target, install) {
396
+ instrumented.get(target)?.();
397
+ const teardowns = [];
398
+ function unwind() {
399
+ for (let i = teardowns.length - 1; i >= 0; i--) safeInvoke(teardowns[i]);
400
+ }
401
+ try {
402
+ install((teardown) => {
403
+ teardowns.push(teardown);
404
+ });
405
+ } catch {
406
+ unwind();
407
+ return () => {};
408
+ }
409
+ function cleanup() {
410
+ unwind();
411
+ if (instrumented.get(target) === cleanup) instrumented.delete(target);
412
+ }
413
+ instrumented.set(target, cleanup);
414
+ return cleanup;
415
+ }
416
+
417
+ //#endregion
418
+ //#region src/tracing/supportsNativeFetch.ts
419
+ /** True if `fn` is the browser's native fetch (not a polyfill/wrapper). */
420
+ function isNativeFetch(fn) {
421
+ return typeof fn === "function" && /native code/.test(Function.prototype.toString.call(fn));
422
+ }
423
+ /**
424
+ * Whether the current global `fetch` is native. A polyfilled fetch (e.g. whatwg-fetch) is
425
+ * XHR-backed; skip instrumenting it so the XHR patch is the single source for those requests.
426
+ * Ported from Sentry, including the hidden-iframe fallback used when another library has already
427
+ * wrapped `fetch` and the direct toString check is unreliable.
428
+ */
429
+ function supportsNativeFetch() {
430
+ const globals = globalThis;
431
+ if (typeof globals.fetch !== "function") return false;
432
+ if (isNativeFetch(globals.fetch)) return true;
433
+ let result = false;
434
+ const document = globals.document;
435
+ if (document && typeof document.createElement === "function") {
436
+ let sandbox = null;
437
+ try {
438
+ sandbox = document.createElement("iframe");
439
+ sandbox.hidden = true;
440
+ document.head.appendChild(sandbox);
441
+ const sandboxWindow = sandbox.contentWindow;
442
+ if (sandboxWindow && typeof sandboxWindow.fetch === "function") result = isNativeFetch(sandboxWindow.fetch);
443
+ } catch {
444
+ result = false;
445
+ } finally {
446
+ try {
447
+ sandbox?.remove();
448
+ } catch {}
449
+ }
450
+ }
451
+ return result;
452
+ }
453
+
454
+ //#endregion
455
+ //#region src/tracing/instrumentFetch.ts
456
+ function resolveRequest(input, init) {
457
+ let url;
458
+ let method = init?.method;
459
+ if (typeof Request !== "undefined" && input instanceof Request) {
460
+ url = input.url;
461
+ method = method ?? input.method;
462
+ } else url = typeof input === "string" ? input : String(input);
463
+ return {
464
+ method: (method ?? "GET").toUpperCase(),
465
+ url
466
+ };
467
+ }
468
+ /**
469
+ * Build a fetch replacement that opens a `browser_fetch` span per call, injects `traceparent` on
470
+ * propagation-eligible URLs, and ends the span on settle. Pure factory: `urls` is injected (node
471
+ * test env has no `location` or `document`), so this is unit-testable without a browser.
472
+ */
473
+ function createFetchWrapper(tracer, original, urls) {
474
+ return function(input, init) {
475
+ const call = (i) => original.call(this, input, i);
476
+ let started = null;
477
+ let url = "";
478
+ let passthrough = false;
479
+ try {
480
+ passthrough = !tracer.config.enableTracing || isInternalRequest(init);
481
+ if (!passthrough) {
482
+ const resolved = resolveRequest(input, init);
483
+ url = resolved.url;
484
+ started = startHttpRequestSpan(tracer, {
485
+ method: resolved.method,
486
+ url,
487
+ urls,
488
+ spanType: _flareapp_core.BrowserSpanType.Fetch
489
+ });
490
+ }
491
+ } catch {
492
+ started = null;
493
+ }
494
+ if (passthrough || !started) return call(init);
495
+ const { span, absoluteUrl } = started;
496
+ let finalInit = init;
497
+ try {
498
+ const traceparent = traceparentFor(span, absoluteUrl, url, urls.origin, tracer.config);
499
+ if (traceparent) finalInit = mergeTraceparentHeader(input, init, traceparent);
500
+ } catch {
501
+ finalInit = init;
502
+ }
503
+ const endSpan = insulate((response) => endHttpRequestSpan(span, response.status));
504
+ const failSpan = insulate((error) => finishHttpSpanError(span, error));
505
+ const finishError = (error) => {
506
+ failSpan(error);
507
+ return Promise.reject(error);
508
+ };
509
+ let promise;
510
+ try {
511
+ promise = call(finalInit);
512
+ } catch (error) {
513
+ return finishError(error);
514
+ }
515
+ return promise.then((response) => {
516
+ endSpan(response);
517
+ return response;
518
+ }, finishError);
519
+ };
520
+ }
521
+ const patcher$1 = createPatcher();
522
+ /**
523
+ * Patch the global `fetch` so outgoing requests are traced. No-op when there is no `fetch` or it
524
+ * is not native (a polyfilled/XHR-backed fetch is left for the XHR patch). Idempotent via `fill`.
525
+ * Reversible via `unpatchFetch`.
526
+ */
527
+ function instrumentFetch(tracer) {
528
+ if (patcher$1.installed) return;
529
+ const globals = globalThis;
530
+ if (typeof globals.fetch !== "function") return;
531
+ if (!supportsNativeFetch()) return;
532
+ const urls = browserUrlContext();
533
+ patcher$1.install(globals, { fetch: (original) => createFetchWrapper(tracer, original, urls) });
534
+ }
535
+ /** Restore the original global `fetch`. Safe if never patched. */
536
+ function unpatchFetch() {
537
+ patcher$1.uninstall(globalThis);
538
+ }
539
+
540
+ //#endregion
541
+ //#region src/tracing/instrumentXHR.ts
542
+ const XHR_DONE = 4;
543
+ const xhrState = /* @__PURE__ */ new WeakMap();
544
+ /**
545
+ * Drop the span and listener references once a request is done with them. The entry itself stays in
546
+ * the WeakMap for the re-send `ended` guard, so without this it would keep the Span and the listener
547
+ * closure alive for as long as the app holds on to the XHR.
548
+ */
549
+ function releaseRequestRefs(state) {
550
+ state.span = void 0;
551
+ state.onDone = void 0;
552
+ }
553
+ /**
554
+ * Patch `open` to capture method/URL. Bails (records no state) when either is missing.
555
+ * Calling `open()` on an in-flight request ends that prior request's span (marked aborted)
556
+ * and detaches its `readystatechange` listener before the new request's method/URL are captured.
557
+ */
558
+ function createXHROpen(original) {
559
+ return function(method, url, ...rest) {
560
+ const prior = xhrState.get(this);
561
+ if (prior && prior.span && !prior.ended) {
562
+ prior.ended = true;
563
+ if (prior.onDone) this.removeEventListener("readystatechange", prior.onDone);
564
+ try {
565
+ prior.span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
566
+ prior.span.end();
567
+ } catch {}
568
+ releaseRequestRefs(prior);
569
+ }
570
+ try {
571
+ if (method && url != null) xhrState.set(this, {
572
+ method: String(method).toUpperCase(),
573
+ url: String(url),
574
+ hasAppTraceparent: false,
575
+ ended: false
576
+ });
577
+ else xhrState.delete(this);
578
+ } catch {
579
+ xhrState.delete(this);
580
+ }
581
+ return original.apply(this, [
582
+ method,
583
+ url,
584
+ ...rest
585
+ ]);
586
+ };
587
+ }
588
+ /**
589
+ * Patch `setRequestHeader` to note when the app sets its own `traceparent`.
590
+ * There is no `getRequestHeader`, so this is the only way to avoid emitting a
591
+ * second `traceparent` (repeat calls merge into one malformed header).
592
+ */
593
+ function createXHRSetRequestHeader(original) {
594
+ return function(name, value) {
595
+ original.call(this, name, value);
596
+ if (typeof name === "string" && name.toLowerCase() === "traceparent") {
597
+ const state = xhrState.get(this);
598
+ if (state) state.hasAppTraceparent = true;
599
+ }
600
+ };
601
+ }
602
+ function setTraceparentHeader(xhr, traceparent) {
603
+ if (!traceparent) return;
604
+ try {
605
+ xhr.setRequestHeader("traceparent", traceparent);
606
+ } catch {}
607
+ }
608
+ /** Patch `send` to open the span, inject `traceparent`, and end on `readyState === 4`. */
609
+ function createXHRSend(tracer, original, urls) {
610
+ return function(body) {
611
+ const send = () => original.call(this, body);
612
+ const config = tracer.config;
613
+ const state = xhrState.get(this);
614
+ if (!config.enableTracing || !state) return send();
615
+ if (state.ended) return send();
616
+ let started = null;
617
+ try {
618
+ started = startHttpRequestSpan(tracer, {
619
+ method: state.method,
620
+ url: state.url,
621
+ urls,
622
+ spanType: _flareapp_core.BrowserSpanType.Xhr
623
+ });
624
+ } catch {
625
+ started = null;
626
+ }
627
+ if (!started) return send();
628
+ const { span, absoluteUrl } = started;
629
+ state.span = span;
630
+ if (!state.hasAppTraceparent) {
631
+ let traceparent = null;
632
+ try {
633
+ traceparent = traceparentFor(span, absoluteUrl, state.url, urls.origin, config);
634
+ } catch {}
635
+ setTraceparentHeader(this, traceparent);
636
+ }
637
+ const onDone = () => {
638
+ if (this.readyState !== XHR_DONE) return;
639
+ this.removeEventListener("readystatechange", onDone);
640
+ if (state.ended) return;
641
+ state.ended = true;
642
+ let status = 0;
643
+ try {
644
+ status = this.status;
645
+ } catch {}
646
+ try {
647
+ const zeroIsError = absoluteUrl !== null && (absoluteUrl.protocol === "http:" || absoluteUrl.protocol === "https:");
648
+ endHttpRequestSpan(span, status, { zeroIsError });
649
+ } catch {}
650
+ releaseRequestRefs(state);
651
+ };
652
+ this.addEventListener("readystatechange", onDone);
653
+ state.onDone = onDone;
654
+ try {
655
+ return send();
656
+ } catch (error) {
657
+ this.removeEventListener("readystatechange", onDone);
658
+ state.ended = true;
659
+ try {
660
+ finishHttpSpanError(span, error);
661
+ } catch {}
662
+ releaseRequestRefs(state);
663
+ throw error;
664
+ }
665
+ };
666
+ }
667
+ const patcher = createPatcher();
668
+ let patchedPrototype = null;
669
+ /**
670
+ * Patch `XMLHttpRequest.prototype` (`open`, `setRequestHeader`, `send`) so outgoing
671
+ * XHR requests are traced. No-op where `XMLHttpRequest` is absent (SSR). Idempotent
672
+ * via `fill`. Reversible via `unpatchXHR`.
673
+ */
674
+ function instrumentXHR(tracer) {
675
+ if (patcher.installed) return;
676
+ const xhrConstructor = globalThis.XMLHttpRequest;
677
+ if (typeof xhrConstructor !== "function" || !xhrConstructor.prototype) return;
678
+ const urls = browserUrlContext();
679
+ patcher.install(xhrConstructor.prototype, {
680
+ open: (original) => createXHROpen(original),
681
+ setRequestHeader: (original) => createXHRSetRequestHeader(original),
682
+ send: (original) => createXHRSend(tracer, original, urls)
683
+ });
684
+ patchedPrototype = xhrConstructor.prototype;
685
+ }
686
+ /** Restore the original `XMLHttpRequest.prototype` methods. Safe if never patched. */
687
+ function unpatchXHR() {
688
+ if (!patchedPrototype) return;
689
+ patcher.uninstall(patchedPrototype);
690
+ if (!patcher.installed) patchedPrototype = null;
691
+ }
692
+
693
+ //#endregion
694
+ //#region src/tracing/absoluteHref.ts
695
+ /**
696
+ * Resolve a router-reported href against the page we are on. Returns the `URL`, so a caller that
697
+ * wants the pathname as well as the href does not parse it a second time.
698
+ *
699
+ * Undefined outside a browser or for an unparseable href, so the caller can leave its attribute alone.
700
+ */
701
+ function absoluteUrl(href) {
702
+ if (href == null || typeof window === "undefined") return;
703
+ try {
704
+ return new URL(href, window.location.href);
705
+ } catch {
706
+ return;
707
+ }
708
+ }
709
+ /**
710
+ * The href form of `absoluteUrl`. Pass one built by the router's own `createHref`/`resolve` (see
711
+ * `resolveHref`), not a bare path: routers strip the app's base path, so `origin + path` yields an
712
+ * address the server does not have.
713
+ */
714
+ function absoluteHref(href) {
715
+ return absoluteUrl(href)?.href;
716
+ }
717
+
718
+ //#endregion
719
+ //#region src/browser/context/collectBrowserSpanContext.ts
720
+ /**
721
+ * Entry point plus request identity for a pageload/navigation root. Deliberately leaner than the report
722
+ * context: no cookies, no structured query params, no host.name (that is resource-level). Captured at
723
+ * span start, so a long-lived root reflects the page it represents rather than the page at close.
724
+ *
725
+ * @param hrefOverride destination href for a router that reports where it is going before the URL
726
+ * commits. Only the URL-derived keys come from it; the rest always reflect the live document. An
727
+ * unparseable override falls back to the live location instead of throwing into root creation.
728
+ */
729
+ function collectBrowserSpanContext(config, hrefOverride) {
730
+ if (typeof window === "undefined") return {};
731
+ const url = absoluteUrl(hrefOverride);
732
+ return {
733
+ ...browserEntryPoint(config, url),
734
+ ...request(config.urlDenylist, url?.href)
735
+ };
736
+ }
737
+ /**
738
+ * Updates a root's url after a redirect, or when a newer navigation replaces this one. The root opened
739
+ * with the first destination, so without this it reports a page the user never reached.
740
+ *
741
+ * Does not touch `flare.entry_point.handler.identifier` or `http.route`. Those hold the route template,
742
+ * and reading them back from the href would turn `/product/[id]` into `/product/p01`.
743
+ *
744
+ * Always sets `url.query`, even to an empty string. You can overwrite a span attribute but not remove
745
+ * it, so going from `/a?x=1` to `/b` would otherwise keep the old query.
746
+ */
747
+ function browserSpanUrlAttributes(config, href) {
748
+ if (typeof window === "undefined") return {};
749
+ const resolved = absoluteUrl(href);
750
+ if (!resolved) return {};
751
+ const attributes = (0, _flareapp_core.urlAttributes)(resolved.href, config.urlDenylist);
752
+ return {
753
+ "url.query": "",
754
+ ...attributes,
755
+ "flare.entry_point.value": attributes["url.full"]
756
+ };
757
+ }
758
+
759
+ //#endregion
760
+ //#region src/tracing/IdleRootController.ts
761
+ /** Browser defaults for the three idle-root timeouts, in ms. Overridable per Config. */
762
+ const DEFAULT_IDLE_TIMEOUTS = {
763
+ idleTimeout: 1e3,
764
+ finalTimeout: 3e4,
765
+ childSpanTimeout: 15e3
766
+ };
767
+ /**
768
+ * Owns one root span's idle lifecycle: open while child spans in its trace are active, closing after
769
+ * `idleTimeout` with no open children, or on the `finalTimeout` / `childSpanTimeout` backstops. Deps are
770
+ * injected so this is testable without real timers or a real tracer.
771
+ */
772
+ var IdleRootController = class {
773
+ openChildren = 0;
774
+ lastChildEndTime = null;
775
+ settleTime = null;
776
+ idleTimer = null;
777
+ finalTimer = null;
778
+ childTimer = null;
779
+ ended = false;
780
+ held = false;
781
+ unsubscribe;
782
+ constructor(deps, timeouts) {
783
+ this.deps = deps;
784
+ this.timeouts = timeouts;
785
+ deps.setActiveRoot(deps.root);
786
+ this.unsubscribe = deps.addSpanListener((e) => this.onSpanEvent(e.phase, e.span));
787
+ const elapsedMs = Math.max(0, (deps.now() - deps.rootStartTime) / 1e6);
788
+ const remainingMs = Math.max(0, timeouts.finalTimeout - elapsedMs);
789
+ this.finalTimer = deps.setTimeout(() => this.finish(deps.now()), remainingMs);
790
+ this.held = !!deps.held;
791
+ this.armIdle();
792
+ }
793
+ get isEnded() {
794
+ return this.ended;
795
+ }
796
+ /** For a route change or pagehide. */
797
+ endNow() {
798
+ this.finish(this.openChildren > 0 || this.held ? this.deps.now() : this.trimmedEnd());
799
+ }
800
+ /**
801
+ * Records the settle moment as a close floor and hands the root back to the normal idle lifecycle. It
802
+ * deliberately does not close here: a router settles before the framework mounts the new route
803
+ * component (vue-router runs `afterEach` in the route-update tick, Vue mounts on the next flush), so
804
+ * closing at settle cleared the active root ahead of every post-navigation mount: every component span
805
+ * read a null root, and a trailing fetch opened a root of its own.
806
+ */
807
+ releaseHold() {
808
+ if (this.ended || !this.held) return;
809
+ this.held = false;
810
+ if (this.openChildren === 0) this.settleTime = this.deps.now();
811
+ this.armIdle();
812
+ }
813
+ onSpanEvent(phase, span) {
814
+ if (this.ended) return;
815
+ if (span === this.deps.root) return;
816
+ if (span.traceId !== this.deps.root.traceId) return;
817
+ if (phase === "start") {
818
+ this.onChildStarted();
819
+ return;
820
+ }
821
+ this.onChildEnded(span);
822
+ }
823
+ onChildStarted() {
824
+ this.openChildren++;
825
+ this.clearIdle();
826
+ if (this.openChildren === 1) this.armChildTimeout();
827
+ }
828
+ onChildEnded(span) {
829
+ this.openChildren = Math.max(0, this.openChildren - 1);
830
+ this.lastChildEndTime = span.endTimeUnixNano || this.deps.now();
831
+ if (this.openChildren > 0) return;
832
+ this.clearChildTimeout();
833
+ this.armIdle();
834
+ }
835
+ armIdle() {
836
+ this.clearIdle();
837
+ if (this.held) return;
838
+ this.idleTimer = this.deps.setTimeout(() => {
839
+ if (this.openChildren > 0) return;
840
+ this.finish(this.trimmedEnd());
841
+ }, this.timeouts.idleTimeout);
842
+ }
843
+ /** The latest of the floor, a released hold's settle moment, and the last child's end, so a root
844
+ * covers its children without ever padding out to `now()`. */
845
+ trimmedEnd() {
846
+ return Math.max(this.deps.endFloor(), this.settleTime ?? 0, this.lastChildEndTime ?? 0);
847
+ }
848
+ clearIdle() {
849
+ if (this.idleTimer !== null) {
850
+ this.deps.clearTimeout(this.idleTimer);
851
+ this.idleTimer = null;
852
+ }
853
+ }
854
+ armChildTimeout() {
855
+ this.childTimer = this.deps.setTimeout(() => this.finish(this.deps.now()), this.timeouts.childSpanTimeout);
856
+ }
857
+ clearChildTimeout() {
858
+ if (this.childTimer !== null) {
859
+ this.deps.clearTimeout(this.childTimer);
860
+ this.childTimer = null;
861
+ }
862
+ }
863
+ finish(atTimeNano) {
864
+ if (this.ended) return;
865
+ this.ended = true;
866
+ atTimeNano = Math.max(atTimeNano, this.deps.rootStartTime);
867
+ this.clearIdle();
868
+ this.clearChildTimeout();
869
+ if (this.finalTimer !== null) {
870
+ this.deps.clearTimeout(this.finalTimer);
871
+ this.finalTimer = null;
872
+ }
873
+ this.unsubscribe();
874
+ this.deps.beforeEnd?.();
875
+ this.deps.root.end(atTimeNano);
876
+ this.deps.setActiveRoot(void 0);
877
+ }
878
+ };
879
+
880
+ //#endregion
881
+ //#region src/tracing/navigation.ts
882
+ /** The path the address bar is on, or '' outside a browser. */
883
+ function currentPath() {
884
+ return typeof location !== "undefined" ? location.pathname : "";
885
+ }
886
+ /**
887
+ * Prefers the router's parameterized template over the raw path, so names aggregate per route rather
888
+ * than per url. `derive` runs inside a try: a router that throws on an unresolved match chain falls back
889
+ * to the url name instead of taking the host down.
890
+ */
891
+ function routeName(derive, fallbackPath, url) {
892
+ try {
893
+ const name = derive();
894
+ if (name) return {
895
+ name,
896
+ source: "route",
897
+ url
898
+ };
899
+ } catch {}
900
+ return {
901
+ name: fallbackPath,
902
+ source: "url",
903
+ url
904
+ };
905
+ }
906
+ /**
907
+ * `build` is the router's own href builder (vue-router's `resolve`, React Router's `createHref`), which
908
+ * is what puts the app's base path and hash prefix back on. Without it an app served from `/app/` reports
909
+ * `/product/p01` for the real `/app/product/p01`. A router that throws still gets a url from `fallback`.
910
+ */
911
+ function resolveHref(build, fallback) {
912
+ let href = fallback;
913
+ try {
914
+ href = build() ?? fallback;
915
+ } catch {}
916
+ return absoluteHref(href);
917
+ }
918
+
919
+ //#endregion
920
+ //#region src/tracing/navigationTiming.ts
921
+ /** Split out so the timestamp maths is testable without a Navigation Timing entry. */
922
+ function computePageloadStartNano(timeOriginMs, startTimeMs) {
923
+ return Math.round((timeOriginMs + (startTimeMs ?? 0)) * 1e6);
924
+ }
925
+ /**
926
+ * Choose the pageload root's start time: navigation start while that window is still open,
927
+ * otherwise `now`. Starting at `now` (when tracing began after the final cap, or the pageload was
928
+ * already traced) avoids a backdated root reporting a bogus multi-second duration.
929
+ */
930
+ function resolvePageloadStartNano(backdatedNano, nowNano, finalTimeoutNano, alreadyTraced) {
931
+ if (alreadyTraced) return nowNano;
932
+ if (nowNano - backdatedNano > finalTimeoutNano) return nowNano;
933
+ return backdatedNano;
934
+ }
935
+ /** The Navigation Timing API, or null where it is missing or only partly implemented. */
936
+ function navigationTiming() {
937
+ const perf = globalThis.performance;
938
+ if (!perf || typeof perf.getEntriesByType !== "function" || typeof perf.timeOrigin !== "number") return null;
939
+ return perf;
940
+ }
941
+ function navigationEntry(perf) {
942
+ return perf.getEntriesByType("navigation")[0];
943
+ }
944
+ /**
945
+ * The pageload root's start time in unix nanoseconds, backdated to navigation start via the
946
+ * Navigation Timing entry. Falls back to the tracer's clock when the API is unavailable.
947
+ */
948
+ function pageloadStartNano() {
949
+ const perf = navigationTiming();
950
+ if (!perf) return (0, _flareapp_core.defaultNowNano)();
951
+ return computePageloadStartNano(perf.timeOrigin, navigationEntry(perf)?.startTime);
952
+ }
953
+ function computePageloadEndNano(timeOriginMs, loadEventEndMs, domContentLoadedEventEndMs, nowNano) {
954
+ const endMs = loadEventEndMs || domContentLoadedEventEndMs || 0;
955
+ if (!endMs) return nowNano;
956
+ return Math.round((timeOriginMs + endMs) * 1e6);
957
+ }
958
+ /**
959
+ * The pageload root's end time in unix nanoseconds, taken from the Navigation
960
+ * Timing `loadEventEnd` (the browser's own "page finished loading" mark), falling
961
+ * back to `domContentLoadedEventEnd`, then the tracer's clock when neither has
962
+ * fired yet or the API is unavailable. Used as the pageload root's close floor so a
963
+ * childless pageload reports its real load duration rather than idle-timeout padding.
964
+ */
965
+ function pageloadEndNano() {
966
+ const perf = navigationTiming();
967
+ if (!perf) return (0, _flareapp_core.defaultNowNano)();
968
+ const entry = navigationEntry(perf);
969
+ return computePageloadEndNano(perf.timeOrigin, entry?.loadEventEnd, entry?.domContentLoadedEventEnd, (0, _flareapp_core.defaultNowNano)());
970
+ }
971
+
972
+ //#endregion
973
+ //#region src/tracing/webvitals/lib/bfcache.ts
974
+ let bfcacheRestoreTime = -1;
975
+ const getBFCacheRestoreTime = () => bfcacheRestoreTime;
976
+ const onBFCacheRestore = (cb) => {
977
+ addEventListener("pageshow", (event) => {
978
+ if (event.persisted) {
979
+ bfcacheRestoreTime = event.timeStamp;
980
+ cb(event);
981
+ }
982
+ }, true);
983
+ };
984
+
985
+ //#endregion
986
+ //#region src/tracing/webvitals/lib/bindReporter.ts
987
+ const getRating = (value, thresholds) => {
988
+ if (value > thresholds[1]) return "poor";
989
+ if (value > thresholds[0]) return "needs-improvement";
990
+ return "good";
991
+ };
992
+ const bindReporter = (callback, metric, thresholds, reportAllChanges) => {
993
+ let prevValue;
994
+ let delta;
995
+ return (forceReport) => {
996
+ if (metric.value >= 0) {
997
+ if (forceReport || reportAllChanges) {
998
+ delta = metric.value - (prevValue ?? 0);
999
+ if (delta || prevValue === void 0) {
1000
+ prevValue = metric.value;
1001
+ metric.delta = delta;
1002
+ metric.rating = getRating(metric.value, thresholds);
1003
+ callback(metric);
1004
+ }
1005
+ }
1006
+ }
1007
+ };
1008
+ };
1009
+
1010
+ //#endregion
1011
+ //#region src/tracing/webvitals/lib/doubleRAF.ts
1012
+ const doubleRAF = (cb) => {
1013
+ requestAnimationFrame(() => requestAnimationFrame(cb));
1014
+ };
1015
+
1016
+ //#endregion
1017
+ //#region src/tracing/webvitals/lib/getNavigationEntry.ts
1018
+ const getNavigationEntry = () => {
1019
+ const navigationEntry = performance.getEntriesByType("navigation")[0];
1020
+ if (navigationEntry && navigationEntry.responseStart > 0 && navigationEntry.responseStart < performance.now()) return navigationEntry;
1021
+ };
1022
+
1023
+ //#endregion
1024
+ //#region src/tracing/webvitals/lib/getActivationStart.ts
1025
+ const getActivationStart = () => {
1026
+ return getNavigationEntry()?.activationStart ?? 0;
1027
+ };
1028
+
1029
+ //#endregion
1030
+ //#region src/tracing/webvitals/lib/getVisibilityWatcher.ts
1031
+ let firstHiddenTime = -1;
1032
+ const onHiddenFunctions = /* @__PURE__ */ new Set();
1033
+ const initHiddenTime = () => {
1034
+ return document.visibilityState === "hidden" && !document.prerendering ? 0 : Infinity;
1035
+ };
1036
+ const onVisibilityUpdate = (event) => {
1037
+ if (document.visibilityState === "hidden") {
1038
+ if (event.type === "visibilitychange") for (const onHiddenFunction of onHiddenFunctions) onHiddenFunction();
1039
+ if (!isFinite(firstHiddenTime)) {
1040
+ firstHiddenTime = event.type === "visibilitychange" ? event.timeStamp : 0;
1041
+ removeEventListener("prerenderingchange", onVisibilityUpdate, true);
1042
+ }
1043
+ }
1044
+ };
1045
+ const getVisibilityWatcher = (reset = false) => {
1046
+ if (reset) firstHiddenTime = Infinity;
1047
+ if (firstHiddenTime < 0) {
1048
+ const activationStart = getActivationStart();
1049
+ firstHiddenTime = (!document.prerendering ? globalThis.performance.getEntriesByType("visibility-state").find((e) => e.name === "hidden" && e.startTime >= activationStart)?.startTime : void 0) ?? initHiddenTime();
1050
+ addEventListener("visibilitychange", onVisibilityUpdate, true);
1051
+ addEventListener("prerenderingchange", onVisibilityUpdate, true);
1052
+ onBFCacheRestore(() => {
1053
+ setTimeout(() => {
1054
+ firstHiddenTime = initHiddenTime();
1055
+ });
1056
+ });
1057
+ }
1058
+ return {
1059
+ get firstHiddenTime() {
1060
+ return firstHiddenTime;
1061
+ },
1062
+ onHidden(cb) {
1063
+ onHiddenFunctions.add(cb);
1064
+ }
1065
+ };
1066
+ };
1067
+
1068
+ //#endregion
1069
+ //#region src/tracing/webvitals/lib/generateUniqueID.ts
1070
+ /**
1071
+ * Performantly generate a unique, 30-char string by combining a version
1072
+ * number, the current timestamp with a 13-digit number integer.
1073
+ * @return {string}
1074
+ */
1075
+ const generateUniqueID = () => {
1076
+ return `v6-${Date.now()}-${Math.floor(Math.random() * 8999999999999) + 0xe8d4a51000}`;
1077
+ };
1078
+
1079
+ //#endregion
1080
+ //#region src/tracing/webvitals/lib/initMetric.ts
1081
+ const initMetric = (name, value = -1, navigationType, navigationId = 0, navigationInteractionId, navigationURL, navigationStartTime) => {
1082
+ const hardNavEntry = getNavigationEntry();
1083
+ const hardNavId = hardNavEntry?.navigationId || 0;
1084
+ let _navigationType = "navigate";
1085
+ if (navigationType) _navigationType = navigationType;
1086
+ else if (getBFCacheRestoreTime() >= 0) _navigationType = "back-forward-cache";
1087
+ else if (hardNavEntry) {
1088
+ if (document.prerendering || getActivationStart() > 0) _navigationType = "prerender";
1089
+ else if (document.wasDiscarded) _navigationType = "restore";
1090
+ else if (hardNavEntry.type) _navigationType = hardNavEntry.type.replace(/_/g, "-");
1091
+ }
1092
+ return {
1093
+ name,
1094
+ value,
1095
+ rating: "good",
1096
+ delta: 0,
1097
+ entries: [],
1098
+ id: generateUniqueID(),
1099
+ navigationType: _navigationType,
1100
+ navigationId: navigationId || hardNavId,
1101
+ navigationInteractionId,
1102
+ navigationURL: navigationURL || hardNavEntry?.name,
1103
+ navigationStartTime: navigationStartTime || 0
1104
+ };
1105
+ };
1106
+
1107
+ //#endregion
1108
+ //#region src/tracing/webvitals/lib/initUnique.ts
1109
+ const instanceMap = /* @__PURE__ */ new WeakMap();
1110
+ /**
1111
+ * A function that accepts and identity object and a class object and returns
1112
+ * either a new instance of that class or an existing instance, if the
1113
+ * identity object was previously used.
1114
+ */
1115
+ function initUnique(identityObj, ClassObj) {
1116
+ let classInstances = instanceMap.get(ClassObj);
1117
+ if (!classInstances) {
1118
+ classInstances = /* @__PURE__ */ new WeakMap();
1119
+ instanceMap.set(ClassObj, classInstances);
1120
+ }
1121
+ if (!classInstances.get(identityObj)) classInstances.set(identityObj, new ClassObj());
1122
+ return classInstances.get(identityObj);
1123
+ }
1124
+
1125
+ //#endregion
1126
+ //#region src/tracing/webvitals/lib/LayoutShiftManager.ts
1127
+ var LayoutShiftManager = class {
1128
+ _onAfterProcessingUnexpectedShift;
1129
+ _sessionValue = 0;
1130
+ _sessionEntries = [];
1131
+ _processEntry(entry) {
1132
+ if (entry.hadRecentInput) return;
1133
+ const firstSessionEntry = this._sessionEntries[0];
1134
+ const lastSessionEntry = this._sessionEntries.at(-1);
1135
+ if (this._sessionValue && firstSessionEntry && lastSessionEntry && entry.startTime - lastSessionEntry.startTime < 1e3 && entry.startTime - firstSessionEntry.startTime < 5e3) {
1136
+ this._sessionValue += entry.value;
1137
+ this._sessionEntries.push(entry);
1138
+ } else {
1139
+ this._sessionValue = entry.value;
1140
+ this._sessionEntries = [entry];
1141
+ }
1142
+ this._onAfterProcessingUnexpectedShift?.(entry);
1143
+ }
1144
+ };
1145
+
1146
+ //#endregion
1147
+ //#region src/tracing/webvitals/lib/observe.ts
1148
+ /**
1149
+ * Takes a performance entry type and a callback function, and creates a
1150
+ * `PerformanceObserver` instance that will observe the specified entry type
1151
+ * with buffering enabled and call the callback _for each entry_.
1152
+ *
1153
+ * This function also feature-detects entry support and wraps the logic in a
1154
+ * try/catch to avoid errors in unsupporting browsers.
1155
+ */
1156
+ const observe = (types, callback, opts = {}) => {
1157
+ try {
1158
+ const supportedTypes = types.filter((t) => PerformanceObserver.supportedEntryTypes.includes(t));
1159
+ if (supportedTypes.length > 0) {
1160
+ const po = new PerformanceObserver((list) => {
1161
+ queueMicrotask(() => {
1162
+ const entries = list.getEntries();
1163
+ if (supportedTypes.length > 1) entries.sort((a, b) => {
1164
+ return a.startTime + a.duration - (b.startTime + b.duration);
1165
+ });
1166
+ callback(entries);
1167
+ });
1168
+ });
1169
+ for (const t of supportedTypes) po.observe({
1170
+ type: t,
1171
+ buffered: true,
1172
+ ...opts
1173
+ });
1174
+ return po;
1175
+ }
1176
+ } catch {}
1177
+ };
1178
+
1179
+ //#endregion
1180
+ //#region src/tracing/webvitals/lib/softNavs.ts
1181
+ const checkSoftNavsEnabled = (opts) => {
1182
+ return globalThis.PerformanceObserver?.supportedEntryTypes.includes("soft-navigation") && typeof globalThis.PerformanceSoftNavigation?.prototype?.getLargestInteractionContentfulPaint === "function" && opts && opts.reportSoftNavs;
1183
+ };
1184
+ const storeSoftNavEntry = (map, entry) => {
1185
+ map.set(entry.navigationId, entry);
1186
+ if (map.size > 2) {
1187
+ const firstKey = map.keys().next().value;
1188
+ if (firstKey !== void 0) map.delete(firstKey);
1189
+ }
1190
+ };
1191
+
1192
+ //#endregion
1193
+ //#region src/tracing/webvitals/lib/runOnce.ts
1194
+ const runOnce = (cb) => {
1195
+ let called = false;
1196
+ return () => {
1197
+ if (!called) {
1198
+ cb();
1199
+ called = true;
1200
+ }
1201
+ };
1202
+ };
1203
+
1204
+ //#endregion
1205
+ //#region src/tracing/webvitals/lib/FCPEntryManager.ts
1206
+ var FCPEntryManager = class {
1207
+ _softNavigationEntryMap;
1208
+ };
1209
+
1210
+ //#endregion
1211
+ //#region src/tracing/webvitals/lib/whenActivated.ts
1212
+ const whenActivated = (callback) => {
1213
+ if (document.prerendering) addEventListener("prerenderingchange", callback, true);
1214
+ else callback();
1215
+ };
1216
+
1217
+ //#endregion
1218
+ //#region src/tracing/webvitals/onFCP.ts
1219
+ /** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */
1220
+ const FCPThresholds = [1800, 3e3];
1221
+ /**
1222
+ * Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and
1223
+ * calls the `callback` function once the value is ready, along with the
1224
+ * relevant `paint` performance entry used to determine the value. The reported
1225
+ * value is a `DOMHighResTimeStamp`.
1226
+ */
1227
+ const onFCP = (onReport, opts = {}) => {
1228
+ const softNavsEnabled = checkSoftNavsEnabled(opts);
1229
+ whenActivated(() => {
1230
+ const fcpEntryManager = initUnique(opts, FCPEntryManager);
1231
+ const visibilityWatcher = getVisibilityWatcher();
1232
+ let metric = initMetric("FCP");
1233
+ let report;
1234
+ const handleEntries = (entries) => {
1235
+ for (const entry of entries) if (entry.name === "first-contentful-paint") {
1236
+ po.disconnect();
1237
+ if (entry.startTime < visibilityWatcher.firstHiddenTime) {
1238
+ metric.value = Math.max(entry.startTime - getActivationStart(), 0);
1239
+ metric.entries.push(entry);
1240
+ metric.navigationId = entry.navigationId || metric.navigationId;
1241
+ report(true);
1242
+ }
1243
+ }
1244
+ };
1245
+ const po = observe(["paint"], handleEntries);
1246
+ if (po) {
1247
+ report = bindReporter(onReport, metric, FCPThresholds, opts.reportAllChanges);
1248
+ onBFCacheRestore((event) => {
1249
+ metric = initMetric("FCP", -1, "back-forward-cache", metric.navigationId, metric.navigationInteractionId, metric.navigationURL, getBFCacheRestoreTime());
1250
+ report = bindReporter(onReport, metric, FCPThresholds, opts.reportAllChanges);
1251
+ doubleRAF(() => {
1252
+ metric.value = performance.now() - event.timeStamp;
1253
+ report(true);
1254
+ });
1255
+ });
1256
+ }
1257
+ if (softNavsEnabled) {
1258
+ const handleSoftNavEntries = (entries) => {
1259
+ entries.forEach((entry) => {
1260
+ if (fcpEntryManager._softNavigationEntryMap && entry.navigationId) storeSoftNavEntry(fcpEntryManager._softNavigationEntryMap, entry);
1261
+ metric = initMetric("FCP", Math.max((entry.presentationTime || entry.paintTime || 0) - entry.startTime, 0), "soft-navigation", entry.navigationId, entry.interactionId, entry.name, entry.startTime);
1262
+ report = bindReporter(onReport, metric, FCPThresholds, opts.reportAllChanges);
1263
+ report(true);
1264
+ });
1265
+ };
1266
+ observe(["soft-navigation"], handleSoftNavEntries, opts);
1267
+ }
1268
+ });
1269
+ };
1270
+
1271
+ //#endregion
1272
+ //#region src/tracing/webvitals/onCLS.ts
1273
+ /** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
1274
+ const CLSThresholds = [.1, .25];
1275
+ /**
1276
+ * Calculates the [CLS](https://web.dev/articles/cls) value for the current page and
1277
+ * calls the `callback` function once the value is ready to be reported, along
1278
+ * with all `layout-shift` performance entries that were used in the metric
1279
+ * value calculation. The reported value is a `double` (corresponding to a
1280
+ * [layout shift score](https://web.dev/articles/cls#layout_shift_score)).
1281
+ *
1282
+ * If the `reportAllChanges` configuration option is set to `true`, the
1283
+ * `callback` function will be called as soon as the value is initially
1284
+ * determined as well as any time the value changes throughout the page
1285
+ * lifespan.
1286
+ *
1287
+ * _**Important:** CLS should be continually monitored for changes throughout
1288
+ * the entire lifespan of a page—including if the user returns to the page after
1289
+ * it's been hidden/backgrounded. However, since browsers often [will not fire
1290
+ * additional callbacks once the user has backgrounded a
1291
+ * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
1292
+ * `callback` is always called when the page's visibility state changes to
1293
+ * hidden. As a result, the `callback` function might be called multiple times
1294
+ * during the same page load._
1295
+ */
1296
+ const onCLS = (onReport, opts = {}) => {
1297
+ const visibilityWatcher = getVisibilityWatcher();
1298
+ onFCP(runOnce(() => {
1299
+ let metric = initMetric("CLS", 0);
1300
+ let report;
1301
+ const layoutShiftManager = initUnique(opts, LayoutShiftManager);
1302
+ const initNewCLSMetric = (navigationType, navigationId, navigationInteractionId, navigationURL, navigationStartTime) => {
1303
+ metric = initMetric("CLS", 0, navigationType, navigationId, navigationInteractionId, navigationURL, navigationStartTime);
1304
+ layoutShiftManager._sessionValue = 0;
1305
+ report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);
1306
+ };
1307
+ const updateAndReportMetric = (forceReport = false) => {
1308
+ if (layoutShiftManager._sessionValue > metric.value) {
1309
+ metric.value = layoutShiftManager._sessionValue;
1310
+ metric.entries = layoutShiftManager._sessionEntries;
1311
+ }
1312
+ report(forceReport);
1313
+ };
1314
+ const handleSoftNavEntry = (entry) => {
1315
+ updateAndReportMetric(true);
1316
+ initNewCLSMetric("soft-navigation", entry.navigationId, entry.interactionId, entry.name, entry.startTime);
1317
+ };
1318
+ const handleEntries = (entries) => {
1319
+ for (const entry of entries) {
1320
+ if (entry.entryType === "soft-navigation") {
1321
+ handleSoftNavEntry(entry);
1322
+ continue;
1323
+ }
1324
+ layoutShiftManager._processEntry(entry);
1325
+ }
1326
+ updateAndReportMetric();
1327
+ };
1328
+ const types = ["layout-shift"];
1329
+ if (checkSoftNavsEnabled(opts)) types.push("soft-navigation");
1330
+ const po = observe(types, handleEntries);
1331
+ if (po) {
1332
+ report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);
1333
+ visibilityWatcher.onHidden(() => {
1334
+ handleEntries(po.takeRecords());
1335
+ report(true);
1336
+ });
1337
+ onBFCacheRestore(() => {
1338
+ initNewCLSMetric("back-forward-cache", metric.navigationId, metric.navigationInteractionId, metric.navigationURL, getBFCacheRestoreTime());
1339
+ doubleRAF(report);
1340
+ });
1341
+ setTimeout(report);
1342
+ }
1343
+ }));
1344
+ };
1345
+
1346
+ //#endregion
1347
+ //#region src/tracing/webvitals/lib/polyfills/interactionCountPolyfill.ts
1348
+ let interactionCountEstimate = 0;
1349
+ let minKnownInteractionId = Infinity;
1350
+ let maxKnownInteractionId = 0;
1351
+ const updateEstimate = (entries) => {
1352
+ for (const entry of entries) if (entry.interactionId) {
1353
+ minKnownInteractionId = Math.min(minKnownInteractionId, entry.interactionId);
1354
+ maxKnownInteractionId = Math.max(maxKnownInteractionId, entry.interactionId);
1355
+ interactionCountEstimate = maxKnownInteractionId ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 : 0;
1356
+ }
1357
+ };
1358
+ let po;
1359
+ /**
1360
+ * Returns the `interactionCount` value using the native API (if available)
1361
+ * or the polyfill estimate in this module.
1362
+ */
1363
+ const getInteractionCount = () => {
1364
+ return po ? interactionCountEstimate : performance.interactionCount ?? 0;
1365
+ };
1366
+ /**
1367
+ * Feature detects native support or initializes the polyfill if needed.
1368
+ */
1369
+ const initInteractionCountPolyfill = () => {
1370
+ if ("interactionCount" in performance || po) return;
1371
+ po = observe(["event"], updateEstimate, { durationThreshold: 0 });
1372
+ };
1373
+
1374
+ //#endregion
1375
+ //#region src/tracing/webvitals/lib/InteractionManager.ts
1376
+ const MAX_INTERACTIONS_TO_CONSIDER = 10;
1377
+ let prevInteractionCount = 0;
1378
+ /**
1379
+ * Returns the interaction count since the last bfcache restore (or for the
1380
+ * full page lifecycle if there were no bfcache restores).
1381
+ */
1382
+ const getInteractionCountForNavigation = () => {
1383
+ return getInteractionCount() - prevInteractionCount;
1384
+ };
1385
+ var InteractionManager = class {
1386
+ /**
1387
+ * A list of longest interactions on the page (by latency) sorted so the
1388
+ * longest one is first. The list is at most MAX_INTERACTIONS_TO_CONSIDER
1389
+ * long.
1390
+ */
1391
+ _longestInteractionList = [];
1392
+ /**
1393
+ * A mapping of longest interactions by their interaction ID.
1394
+ * This is used for faster lookup.
1395
+ */
1396
+ _longestInteractionMap = /* @__PURE__ */ new Map();
1397
+ _onBeforeProcessingEntry;
1398
+ _onAfterProcessingINPCandidate;
1399
+ _resetInteractions() {
1400
+ prevInteractionCount = getInteractionCount();
1401
+ this._longestInteractionList.length = 0;
1402
+ this._longestInteractionMap.clear();
1403
+ }
1404
+ /**
1405
+ * Returns the estimated p98 longest interaction based on the stored
1406
+ * interaction candidates and the interaction count for the current page.
1407
+ */
1408
+ _estimateP98LongestInteraction(navigationType) {
1409
+ const interactionCountForNavigation = getInteractionCountForNavigation();
1410
+ const candidateInteractionIndex = Math.min(this._longestInteractionList.length - 1, Math.floor(interactionCountForNavigation / 50));
1411
+ if (interactionCountForNavigation && candidateInteractionIndex === -1 && (navigationType === "soft-navigation" || navigationType === "back-forward-cache")) return {
1412
+ _latency: 8,
1413
+ id: -1,
1414
+ entries: []
1415
+ };
1416
+ return this._longestInteractionList[candidateInteractionIndex];
1417
+ }
1418
+ /**
1419
+ * Takes a performance entry and adds it to the list of worst interactions
1420
+ * if its duration is long enough to make it among the worst. If the
1421
+ * entry is part of an existing interaction, it is merged and the latency
1422
+ * and entries list is updated as needed.
1423
+ */
1424
+ _processEntry(entry) {
1425
+ this._onBeforeProcessingEntry?.(entry);
1426
+ if (!(entry.interactionId || entry.entryType === "first-input")) return;
1427
+ const minLongestInteraction = this._longestInteractionList.at(-1);
1428
+ let interaction = this._longestInteractionMap.get(entry.interactionId);
1429
+ if (interaction || this._longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || entry.duration > minLongestInteraction._latency) {
1430
+ if (interaction) {
1431
+ if (entry.duration > interaction._latency) {
1432
+ interaction.entries = [entry];
1433
+ interaction._latency = entry.duration;
1434
+ } else if (entry.duration === interaction._latency && entry.startTime === interaction.entries[0].startTime) interaction.entries.push(entry);
1435
+ } else {
1436
+ interaction = {
1437
+ id: entry.interactionId,
1438
+ entries: [entry],
1439
+ _latency: entry.duration
1440
+ };
1441
+ this._longestInteractionMap.set(interaction.id, interaction);
1442
+ this._longestInteractionList.push(interaction);
1443
+ }
1444
+ this._longestInteractionList.sort((a, b) => b._latency - a._latency);
1445
+ if (this._longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
1446
+ const removedInteractions = this._longestInteractionList.splice(MAX_INTERACTIONS_TO_CONSIDER);
1447
+ for (const interaction of removedInteractions) this._longestInteractionMap.delete(interaction.id);
1448
+ }
1449
+ this._onAfterProcessingINPCandidate?.(interaction);
1450
+ }
1451
+ }
1452
+ };
1453
+
1454
+ //#endregion
1455
+ //#region src/tracing/webvitals/lib/whenIdleOrHidden.ts
1456
+ /**
1457
+ * Runs the passed callback during the next idle period, or immediately
1458
+ * if the browser's visibility state is (or becomes) hidden.
1459
+ */
1460
+ const whenIdleOrHidden = (cb) => {
1461
+ const timeout = "requestIdleCallback" in globalThis ? 1e3 : 0;
1462
+ const rIC = globalThis.requestIdleCallback || setTimeout;
1463
+ const cIC = globalThis.cancelIdleCallback || clearTimeout;
1464
+ if (document.visibilityState === "hidden") cb();
1465
+ else {
1466
+ const wrappedCb = runOnce(cb);
1467
+ let idleHandle = -1;
1468
+ const onHidden = () => {
1469
+ cIC(idleHandle);
1470
+ wrappedCb();
1471
+ };
1472
+ addEventListener("visibilitychange", onHidden, {
1473
+ once: true,
1474
+ capture: true
1475
+ });
1476
+ idleHandle = rIC(() => {
1477
+ removeEventListener("visibilitychange", onHidden, { capture: true });
1478
+ wrappedCb();
1479
+ }, { timeout });
1480
+ }
1481
+ };
1482
+
1483
+ //#endregion
1484
+ //#region src/tracing/webvitals/onINP.ts
1485
+ /** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */
1486
+ const INPThresholds = [200, 500];
1487
+ const DEFAULT_DURATION_THRESHOLD = 40;
1488
+ /**
1489
+ * Calculates the [INP](https://web.dev/articles/inp) value for the current
1490
+ * page and calls the `callback` function once the value is ready, along with
1491
+ * the `event` performance entries reported for that interaction. The reported
1492
+ * value is a `DOMHighResTimeStamp`.
1493
+ *
1494
+ * A custom `durationThreshold` configuration option can optionally be passed
1495
+ * to control what `event-timing` entries are considered for INP reporting. The
1496
+ * default threshold is `40`, which means INP scores of less than 40 will not
1497
+ * be reported. To avoid reporting no interactions in these cases, the library
1498
+ * will fall back to the input delay of the first interaction. Note that this
1499
+ * will not affect your 75th percentile INP value unless that value is also
1500
+ * less than 40 (well below the recommended
1501
+ * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold).
1502
+ *
1503
+ * If the `reportAllChanges` configuration option is set to `true`, the
1504
+ * `callback` function will be called as soon as the value is initially
1505
+ * determined as well as any time the value changes throughout the page
1506
+ * lifespan.
1507
+ *
1508
+ * _**Important:** INP should be continually monitored for changes throughout
1509
+ * the entire lifespan of a page—including if the user returns to the page after
1510
+ * it's been hidden/backgrounded. However, since browsers often [will not fire
1511
+ * additional callbacks once the user has backgrounded a
1512
+ * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
1513
+ * `callback` is always called when the page's visibility state changes to
1514
+ * hidden. As a result, the `callback` function might be called multiple times
1515
+ * during the same page load._
1516
+ */
1517
+ const onINP = (onReport, opts = {}) => {
1518
+ if (!(globalThis.PerformanceEventTiming && "interactionId" in PerformanceEventTiming.prototype)) return;
1519
+ const visibilityWatcher = getVisibilityWatcher();
1520
+ whenActivated(() => {
1521
+ initInteractionCountPolyfill();
1522
+ let metric = initMetric("INP");
1523
+ let report;
1524
+ const interactionManager = initUnique(opts, InteractionManager);
1525
+ const initNewINPMetric = (navigationType, navigationId, navigationInteractionId, navigationURL, navigationStartTime) => {
1526
+ interactionManager._resetInteractions();
1527
+ metric = initMetric("INP", -1, navigationType, navigationId, navigationInteractionId, navigationURL, navigationStartTime);
1528
+ report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges);
1529
+ };
1530
+ const updateINPMetric = () => {
1531
+ const inp = interactionManager._estimateP98LongestInteraction(metric.navigationType);
1532
+ if (inp && inp._latency !== metric.value) {
1533
+ metric.value = inp._latency;
1534
+ metric.entries = inp.entries;
1535
+ report();
1536
+ }
1537
+ };
1538
+ const handleSoftNavEntry = (entry) => {
1539
+ updateINPMetric();
1540
+ report(true);
1541
+ initNewINPMetric("soft-navigation", entry.navigationId, entry.interactionId, entry.name, entry.startTime);
1542
+ };
1543
+ const handleEntries = (entries, forceReport = false) => {
1544
+ whenIdleOrHidden(() => {
1545
+ for (const entry of entries) {
1546
+ if (entry.entryType === "soft-navigation") {
1547
+ handleSoftNavEntry(entry);
1548
+ continue;
1549
+ }
1550
+ interactionManager._processEntry(entry);
1551
+ }
1552
+ updateINPMetric();
1553
+ if (forceReport) report(true);
1554
+ });
1555
+ };
1556
+ const types = ["event", "first-input"];
1557
+ if (checkSoftNavsEnabled(opts)) types.push("soft-navigation");
1558
+ const po = observe(types, handleEntries, {
1559
+ ...opts,
1560
+ durationThreshold: opts.durationThreshold ?? DEFAULT_DURATION_THRESHOLD
1561
+ });
1562
+ report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges);
1563
+ if (po) {
1564
+ visibilityWatcher.onHidden(() => {
1565
+ handleEntries(po.takeRecords(), true);
1566
+ });
1567
+ onBFCacheRestore(() => {
1568
+ initNewINPMetric("back-forward-cache", metric.navigationId, metric.navigationInteractionId, metric.navigationURL, getBFCacheRestoreTime());
1569
+ });
1570
+ }
1571
+ });
1572
+ };
1573
+
1574
+ //#endregion
1575
+ //#region src/tracing/webvitals/lib/LCPEntryManager.ts
1576
+ var LCPEntryManager = class {
1577
+ _onBeforeProcessingEntry;
1578
+ _softNavigationEntryMap;
1579
+ _processEntry(entry) {
1580
+ this._onBeforeProcessingEntry?.(entry);
1581
+ }
1582
+ };
1583
+
1584
+ //#endregion
1585
+ //#region src/tracing/webvitals/onLCP.ts
1586
+ /** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */
1587
+ const LCPThresholds = [2500, 4e3];
1588
+ /**
1589
+ * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and
1590
+ * calls the `callback` function once the value is ready (along with the
1591
+ * relevant `largest-contentful-paint` performance entry used to determine the
1592
+ * value). The reported value is a `DOMHighResTimeStamp`.
1593
+ *
1594
+ * If the `reportAllChanges` configuration option is set to `true`, the
1595
+ * `callback` function will be called any time a new `largest-contentful-paint`
1596
+ * performance entry is dispatched, or once the final value of the metric has
1597
+ * been determined.
1598
+ */
1599
+ const onLCP = (onReport, opts = {}) => {
1600
+ let isFinalized = false;
1601
+ const softNavsEnabled = checkSoftNavsEnabled(opts);
1602
+ whenActivated(() => {
1603
+ let visibilityWatcher = getVisibilityWatcher();
1604
+ let metric = initMetric("LCP");
1605
+ let report;
1606
+ const lcpEntryManager = initUnique(opts, LCPEntryManager);
1607
+ const initNewLCPMetric = (navigation, navigationId, navigationInteractionId, navigationURL, navigationStartTime) => {
1608
+ metric = initMetric("LCP", -1, navigation, navigationId, navigationInteractionId, navigationURL, navigationStartTime);
1609
+ report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges);
1610
+ isFinalized = false;
1611
+ if (navigation === "soft-navigation") visibilityWatcher = getVisibilityWatcher(true);
1612
+ };
1613
+ const handleSoftNavEntry = (entry) => {
1614
+ if (lcpEntryManager._softNavigationEntryMap && entry.navigationId) storeSoftNavEntry(lcpEntryManager._softNavigationEntryMap, entry);
1615
+ if (!isFinalized) report(true);
1616
+ initNewLCPMetric("soft-navigation", entry.navigationId, entry.interactionId, entry.name, entry.startTime);
1617
+ const largestInteractionContentfulPaint = entry.getLargestInteractionContentfulPaint?.();
1618
+ if (largestInteractionContentfulPaint) handleEntries([largestInteractionContentfulPaint]);
1619
+ };
1620
+ const handleEntries = (entries) => {
1621
+ if (!opts.reportAllChanges && !softNavsEnabled) entries = entries.slice(-1);
1622
+ for (const entry of entries) {
1623
+ if (!entry) continue;
1624
+ if (entry.entryType === "soft-navigation") {
1625
+ handleSoftNavEntry(entry);
1626
+ continue;
1627
+ }
1628
+ let value = 0;
1629
+ let metricEntries = [];
1630
+ let renderTime = entry.startTime;
1631
+ if (entry.entryType === "largest-contentful-paint") {
1632
+ value = Math.max(entry.startTime - getActivationStart(), 0);
1633
+ lcpEntryManager._processEntry(entry);
1634
+ metricEntries = [entry];
1635
+ } else if (entry.entryType === "interaction-contentful-paint") {
1636
+ const ICPEntry = entry;
1637
+ if (!metric.navigationId) continue;
1638
+ if ("interactionId" in ICPEntry && ICPEntry.interactionId != metric.navigationInteractionId) continue;
1639
+ renderTime = ICPEntry.largestContentfulPaint?.renderTime || 0;
1640
+ value = Math.max(renderTime - entry.startTime, 0);
1641
+ if (ICPEntry.largestContentfulPaint) {
1642
+ lcpEntryManager._processEntry(ICPEntry.largestContentfulPaint);
1643
+ metricEntries = [ICPEntry.largestContentfulPaint];
1644
+ }
1645
+ }
1646
+ if (renderTime < visibilityWatcher.firstHiddenTime) {
1647
+ metric.value = value;
1648
+ metric.entries = metricEntries;
1649
+ report();
1650
+ }
1651
+ }
1652
+ };
1653
+ const types = ["largest-contentful-paint"];
1654
+ if (softNavsEnabled) types.push("interaction-contentful-paint", "soft-navigation");
1655
+ const po = observe(types, handleEntries);
1656
+ if (po) {
1657
+ report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges);
1658
+ const finalizeEventTypes = [
1659
+ "keydown",
1660
+ "click",
1661
+ "visibilitychange"
1662
+ ];
1663
+ const finalizeLCP = (event) => {
1664
+ if (event.isTrusted && !isFinalized) {
1665
+ const metricIdToFinalize = metric.id;
1666
+ whenIdleOrHidden(() => {
1667
+ if (!isFinalized) {
1668
+ if (!softNavsEnabled) {
1669
+ po.disconnect();
1670
+ for (const type of finalizeEventTypes) removeEventListener(type, finalizeLCP, { capture: true });
1671
+ }
1672
+ if (metricIdToFinalize === metric.id) {
1673
+ isFinalized = true;
1674
+ report(true);
1675
+ }
1676
+ }
1677
+ });
1678
+ }
1679
+ };
1680
+ for (const type of finalizeEventTypes) addEventListener(type, finalizeLCP, { capture: true });
1681
+ onBFCacheRestore((event) => {
1682
+ initNewLCPMetric("back-forward-cache", metric.navigationId, metric.navigationInteractionId, metric.navigationURL, getBFCacheRestoreTime());
1683
+ report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges);
1684
+ doubleRAF(() => {
1685
+ metric.value = performance.now() - event.timeStamp;
1686
+ isFinalized = true;
1687
+ report(true);
1688
+ });
1689
+ });
1690
+ }
1691
+ });
1692
+ };
1693
+
1694
+ //#endregion
1695
+ //#region src/tracing/webvitals/onTTFB.ts
1696
+ /** Thresholds for TTFB. See https://web.dev/articles/ttfb#what_is_a_good_ttfb_score */
1697
+ const TTFBThresholds = [800, 1800];
1698
+ /**
1699
+ * Runs in the next task after the page is done loading and/or prerendering.
1700
+ * @param callback
1701
+ */
1702
+ const whenReady = (callback) => {
1703
+ if (document.prerendering) whenActivated(() => whenReady(callback));
1704
+ else if (document.readyState !== "complete") addEventListener("load", () => whenReady(callback), true);
1705
+ else setTimeout(callback);
1706
+ };
1707
+ /**
1708
+ * Calculates the [TTFB](https://web.dev/articles/ttfb) value for the
1709
+ * current page and calls the `callback` function once the page has loaded,
1710
+ * along with the relevant `navigation` performance entry used to determine the
1711
+ * value. The reported value is a `DOMHighResTimeStamp`.
1712
+ *
1713
+ * Note, this function waits until after the page is loaded to call `callback`
1714
+ * in order to ensure all properties of the `navigation` entry are populated.
1715
+ * This is useful if you want to report on other metrics exposed by the
1716
+ * [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
1717
+ * example, the TTFB metric starts from the page's [time
1718
+ * origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
1719
+ * includes time spent on DNS lookup, connection negotiation, network latency,
1720
+ * and server processing time.
1721
+ */
1722
+ const onTTFB = (onReport, opts = {}) => {
1723
+ const softNavsEnabled = checkSoftNavsEnabled(opts);
1724
+ let metric = initMetric("TTFB");
1725
+ let report = bindReporter(onReport, metric, TTFBThresholds, opts.reportAllChanges);
1726
+ whenReady(() => {
1727
+ const hardNavEntry = getNavigationEntry();
1728
+ if (hardNavEntry) {
1729
+ const responseStart = hardNavEntry.responseStart;
1730
+ metric.value = Math.max(responseStart - getActivationStart(), 0);
1731
+ metric.entries = [hardNavEntry];
1732
+ report(true);
1733
+ onBFCacheRestore(() => {
1734
+ metric = initMetric("TTFB", 0, "back-forward-cache", metric.navigationId, metric.navigationInteractionId, metric.navigationURL, getBFCacheRestoreTime());
1735
+ report = bindReporter(onReport, metric, TTFBThresholds, opts.reportAllChanges);
1736
+ report(true);
1737
+ });
1738
+ if (softNavsEnabled) {
1739
+ const reportSoftNavTTFBs = (entries) => {
1740
+ entries.forEach((entry) => {
1741
+ if (entry.navigationId) {
1742
+ metric = initMetric("TTFB", 0, "soft-navigation", entry.navigationId, entry.interactionId, entry.name, entry.startTime);
1743
+ metric.entries = [entry];
1744
+ report = bindReporter(onReport, metric, TTFBThresholds, opts.reportAllChanges);
1745
+ report(true);
1746
+ }
1747
+ });
1748
+ };
1749
+ observe(["soft-navigation"], reportSoftNavTTFBs, opts);
1750
+ }
1751
+ }
1752
+ });
1753
+ };
1754
+
1755
+ //#endregion
1756
+ //#region src/tracing/webVitals.ts
1757
+ /**
1758
+ * Final the moment they first report, so they can ride the pageload span itself. The other three keep
1759
+ * changing until the page goes away, and stamping an early value on the root would leave the root and
1760
+ * the later span disagreeing about the same vital.
1761
+ */
1762
+ const EARLY_VITALS = ["ttfb", "fcp"];
1763
+ /** The one place a vital becomes a wire key. Both the pageload stamp and the late span go through it. */
1764
+ function vitalAttributes(vitals) {
1765
+ const attributes = {};
1766
+ for (const [name, value] of Object.entries(vitals)) if (typeof value === "number") attributes[`browser.web_vital.${name}`] = value;
1767
+ return attributes;
1768
+ }
1769
+ /**
1770
+ * Turns the leftover values into one zero-duration span. Pure on purpose: the shape is what the backend
1771
+ * groups on, and this way it is testable without a tracer or a clock.
1772
+ *
1773
+ * Both timestamps sit at the pageload root's start. `spans_2` buckets on `start_time_unix_nano`, so
1774
+ * stamping the report moment would drop a tab left open for forty minutes into a minute forty minutes
1775
+ * after the page actually loaded.
1776
+ *
1777
+ * Returns null when nothing is left to report, so the caller emits no span at all.
1778
+ */
1779
+ function buildVitalsSpan(input) {
1780
+ const vitals = vitalAttributes(input.vitals);
1781
+ if (Object.keys(vitals).length === 0) return null;
1782
+ return {
1783
+ name: input.routeName,
1784
+ startTimeUnixNano: input.rootStartTimeUnixNano,
1785
+ endTimeUnixNano: input.rootStartTimeUnixNano,
1786
+ attributes: {
1787
+ ...input.contextAttributes,
1788
+ "flare.entry_point.handler.identifier": input.routeName,
1789
+ "http.route": input.routeName,
1790
+ "flare.route.source": input.routeSource,
1791
+ ...vitals
1792
+ }
1793
+ };
1794
+ }
1795
+ let collected = {};
1796
+ let subscribed = false;
1797
+ let recording = false;
1798
+ let taken = false;
1799
+ function defaultSubscribers() {
1800
+ return {
1801
+ onTTFB: (cb) => onTTFB(cb),
1802
+ onFCP: (cb) => onFCP(cb),
1803
+ onLCP: (cb) => onLCP(cb, { reportAllChanges: true }),
1804
+ onCLS: (cb) => onCLS(cb, { reportAllChanges: true }),
1805
+ onINP: (cb) => onINP(cb, { reportAllChanges: true })
1806
+ };
1807
+ }
1808
+ /**
1809
+ * Subscribes at most once per document: upstream's on* functions return no unsubscribe handle, so a
1810
+ * second call would attach a second set of observers with no way to detach either.
1811
+ */
1812
+ function startWebVitals(subscribers = defaultSubscribers()) {
1813
+ recording = true;
1814
+ if (subscribed) return;
1815
+ subscribed = true;
1816
+ subscribe(subscribers.onTTFB, "ttfb");
1817
+ subscribe(subscribers.onFCP, "fcp");
1818
+ subscribe(subscribers.onLCP, "lcp");
1819
+ subscribe(subscribers.onCLS, "cls");
1820
+ subscribe(subscribers.onINP, "inp");
1821
+ }
1822
+ function subscribe(on, name) {
1823
+ try {
1824
+ on((metric) => record(name, metric));
1825
+ } catch {}
1826
+ }
1827
+ function record(name, metric) {
1828
+ if (!recording || typeof metric?.value !== "number") return;
1829
+ collected[name] = metric.value;
1830
+ }
1831
+ /**
1832
+ * Stops recording and drops what was collected. The observers themselves cannot be detached, and both
1833
+ * the `subscribed` and `taken` latches deliberately survive: clearing `subscribed` would let a re-enable
1834
+ * attach a second set of observers that is just as undetachable as the first, and clearing `taken` would
1835
+ * let those surviving observers refill `collected` and ship a second `browser_web_vital` for the same
1836
+ * document once the page is re-enabled and later hidden.
1837
+ */
1838
+ function stopWebVitals() {
1839
+ recording = false;
1840
+ collected = {};
1841
+ }
1842
+ /**
1843
+ * The vitals that are already final when the pageload root closes, removed from `collected` so the late
1844
+ * span cannot report them a second time. Naturally idempotent: a second call finds the keys gone.
1845
+ */
1846
+ function takeEarlyVitals() {
1847
+ if (!recording) return null;
1848
+ const taking = {};
1849
+ for (const name of EARLY_VITALS) {
1850
+ const value = collected[name];
1851
+ if (value !== void 0) {
1852
+ taking[name] = value;
1853
+ delete collected[name];
1854
+ }
1855
+ }
1856
+ return Object.keys(taking).length === 0 ? null : taking;
1857
+ }
1858
+ /** Everything still outstanding, once. Null afterwards, and null when nothing is left. */
1859
+ function takeWebVitals() {
1860
+ if (taken || !recording || Object.keys(collected).length === 0) return null;
1861
+ taken = true;
1862
+ const taking = collected;
1863
+ collected = {};
1864
+ return taking;
1865
+ }
1866
+ /**
1867
+ * Undoes a take after the emit failed partway through, so the values are not lost and a later trigger
1868
+ * can retry. Safe to assign outright rather than merge: the whole emit runs synchronously between the
1869
+ * take and a catch block calling this, so nothing else can have written to `collected` in between.
1870
+ */
1871
+ function restoreWebVitals(vitals) {
1872
+ collected = vitals;
1873
+ taken = false;
1874
+ }
1875
+
1876
+ //#endregion
1877
+ //#region src/tracing/browserTracing.ts
1878
+ let controller = null;
1879
+ let uninstall = null;
1880
+ let lastPath = "";
1881
+ let pageloadTraced = false;
1882
+ let navSource = null;
1883
+ let activeFlare = null;
1884
+ let currentRoot = null;
1885
+ let pendingRouteName = null;
1886
+ let pendingRouteNameOwner = null;
1887
+ let pageloadRoot = null;
1888
+ let pageloadRootStartNano = 0;
1889
+ let pageloadRoute = null;
1890
+ let pageloadContext = {};
1891
+ function resolveTimeouts(config) {
1892
+ return {
1893
+ idleTimeout: config.idleTimeout ?? DEFAULT_IDLE_TIMEOUTS.idleTimeout,
1894
+ finalTimeout: config.finalTimeout ?? DEFAULT_IDLE_TIMEOUTS.finalTimeout,
1895
+ childSpanTimeout: config.childSpanTimeout ?? DEFAULT_IDLE_TIMEOUTS.childSpanTimeout
1896
+ };
1897
+ }
1898
+ /** No-ops once the controller has ended: it can close itself asynchronously via a timer, before this
1899
+ * module's `controller` reference is cleared. */
1900
+ function withLiveController(fn) {
1901
+ if (!controller || controller.isEnded) return;
1902
+ try {
1903
+ fn(controller);
1904
+ } catch (error) {
1905
+ if (activeFlare?.config.debug) console.error("Flare: browser tracing controller callback failed", error);
1906
+ }
1907
+ }
1908
+ /** Run `fn` only while the current root is still open. Swallows a throw, like `withLiveController`. */
1909
+ function ifRootLive(fn) {
1910
+ withLiveController(() => fn());
1911
+ }
1912
+ function startRoot(flare, options) {
1913
+ const { spanType, startTimeUnixNano, name = location.pathname, urlOverride, hold, backdated } = options;
1914
+ let root;
1915
+ try {
1916
+ const context = collectBrowserSpanContext(flare.config, urlOverride);
1917
+ root = flare.startSpan(name, {
1918
+ spanType,
1919
+ startTimeUnixNano,
1920
+ forceRoot: true,
1921
+ attributes: {
1922
+ ...context,
1923
+ "flare.route.source": "url"
1924
+ }
1925
+ });
1926
+ controller = new IdleRootController({
1927
+ root,
1928
+ addSpanListener: (fn) => flare.tracer.addSpanListener(fn),
1929
+ setActiveRoot: (span) => flare.tracer.setActiveRoot(span),
1930
+ now: _flareapp_core.defaultNowNano,
1931
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
1932
+ clearTimeout: (handle) => clearTimeout(handle),
1933
+ rootStartTime: startTimeUnixNano,
1934
+ endFloor: spanType === _flareapp_core.BrowserSpanType.Pageload && backdated ? pageloadEndNano : () => startTimeUnixNano,
1935
+ held: hold,
1936
+ beforeEnd: spanType === _flareapp_core.BrowserSpanType.Pageload ? () => stampEarlyVitals(root, flare) : void 0
1937
+ }, resolveTimeouts(flare.config));
1938
+ currentRoot = root;
1939
+ if (spanType === _flareapp_core.BrowserSpanType.Pageload) {
1940
+ pageloadRoot = root;
1941
+ pageloadRootStartNano = startTimeUnixNano;
1942
+ pageloadRoute = {
1943
+ name,
1944
+ source: "url"
1945
+ };
1946
+ pageloadContext = { ...context };
1947
+ }
1948
+ } catch (error) {
1949
+ controller = null;
1950
+ currentRoot = null;
1951
+ if (spanType === _flareapp_core.BrowserSpanType.Pageload) {
1952
+ pageloadRoot = null;
1953
+ pageloadRootStartNano = 0;
1954
+ pageloadRoute = null;
1955
+ pageloadContext = {};
1956
+ }
1957
+ try {
1958
+ root?.end();
1959
+ } catch {}
1960
+ try {
1961
+ flare.tracer.setActiveRoot(void 0);
1962
+ } catch {}
1963
+ if (flare.config.debug) console.error("Flare: failed to start browser tracing root", error);
1964
+ }
1965
+ }
1966
+ function onUrlChanged(flare) {
1967
+ const path = location.pathname;
1968
+ if (path === lastPath) return;
1969
+ lastPath = path;
1970
+ if (navSource) return;
1971
+ withLiveController((live) => live.endNow());
1972
+ startRoot(flare, {
1973
+ spanType: _flareapp_core.BrowserSpanType.Navigation,
1974
+ startTimeUnixNano: (0, _flareapp_core.defaultNowNano)(),
1975
+ name: path
1976
+ });
1977
+ }
1978
+ /**
1979
+ * Writes the already-final vitals onto the pageload root itself, from `IdleRootController`'s beforeEnd
1980
+ * hook. Whatever has not reported yet stays in `collected` and rides the later `browser_web_vital`
1981
+ * span instead, so no vital is ever sent twice and none is lost.
1982
+ *
1983
+ * Swallows its own failures: this runs inside the root's close path, and a throw here would leave the
1984
+ * root open forever.
1985
+ */
1986
+ function stampEarlyVitals(root, flare) {
1987
+ try {
1988
+ const early = takeEarlyVitals();
1989
+ if (!early) return;
1990
+ for (const [key, value] of Object.entries(vitalAttributes(early))) root.setAttribute(key, value);
1991
+ } catch (error) {
1992
+ if (flare.config.debug) console.error("Flare: failed to stamp web vitals on the pageload root", error);
1993
+ }
1994
+ }
1995
+ /**
1996
+ * Emits whatever the pageload root could not carry as one zero-duration `browser_web_vital` span,
1997
+ * parented to that root. Page hide only, and once per document.
1998
+ *
1999
+ * Deliberately NOT on navigation: LCP, CLS and INP keep moving all document long, so emitting at the
2000
+ * first route change froze them a second after load, and a session whose first action was a nav click
2001
+ * reported no INP at all. One span per document is a backend constraint, so the emit waits for the last
2002
+ * moment we get instead. The cost is a page whose hide event never fires reports no vitals.
2003
+ *
2004
+ * `pageloadRoot` has usually ended by now; reading `traceId` and `spanId` off an ended span is fine,
2005
+ * and passing the `Span` rather than a `{ traceId, spanId }` pair is what makes sampling inherit:
2006
+ * `resolveTrace()` reads `parent.isRecording` instead of re-rolling the sampler.
2007
+ */
2008
+ function emitWebVitals(flare) {
2009
+ const root = pageloadRoot;
2010
+ const route = pageloadRoute;
2011
+ if (!root || !route) return;
2012
+ const vitals = takeWebVitals();
2013
+ if (!vitals) return;
2014
+ try {
2015
+ const planned = buildVitalsSpan({
2016
+ vitals,
2017
+ rootStartTimeUnixNano: pageloadRootStartNano,
2018
+ routeName: route.name,
2019
+ routeSource: route.source,
2020
+ contextAttributes: pageloadContext
2021
+ });
2022
+ if (!planned) return;
2023
+ flare.startSpan(planned.name, {
2024
+ parent: root,
2025
+ forceRoot: true,
2026
+ spanType: _flareapp_core.BrowserSpanType.WebVital,
2027
+ startTimeUnixNano: planned.startTimeUnixNano,
2028
+ attributes: planned.attributes
2029
+ }).end(planned.endTimeUnixNano);
2030
+ } catch (error) {
2031
+ restoreWebVitals(vitals);
2032
+ if (flare.config.debug) console.error("Flare: failed to emit web vitals", error);
2033
+ }
2034
+ }
2035
+ /** Opens a backdated pageload root, then a navigation root per History change. No-op outside a browser. Idempotent. */
2036
+ function startBrowserTracing(flare) {
2037
+ if (typeof window === "undefined" || typeof history === "undefined" || typeof location === "undefined") return;
2038
+ if (uninstall) return;
2039
+ activeFlare = flare;
2040
+ lastPath = location.pathname;
2041
+ const finalTimeoutNano = resolveTimeouts(flare.config).finalTimeout * 1e6;
2042
+ const navigationStart = pageloadStartNano();
2043
+ const pageloadStart = resolvePageloadStartNano(navigationStart, (0, _flareapp_core.defaultNowNano)(), finalTimeoutNano, pageloadTraced);
2044
+ pageloadTraced = true;
2045
+ startRoot(flare, {
2046
+ spanType: _flareapp_core.BrowserSpanType.Pageload,
2047
+ startTimeUnixNano: pageloadStart,
2048
+ backdated: pageloadStart === navigationStart
2049
+ });
2050
+ startWebVitals();
2051
+ if (pendingRouteName) {
2052
+ const route = pendingRouteName;
2053
+ const owner = pendingRouteNameOwner;
2054
+ pendingRouteName = null;
2055
+ pendingRouteNameOwner = null;
2056
+ if (owner === navSource) applyRouteName(route);
2057
+ }
2058
+ const handle = () => {
2059
+ if (!uninstall) return;
2060
+ try {
2061
+ onUrlChanged(flare);
2062
+ } catch (error) {
2063
+ if (flare.config.debug) console.error("Flare: browser tracing navigation handler failed", error);
2064
+ }
2065
+ };
2066
+ function wrapHistoryMethod(original) {
2067
+ return function(...args) {
2068
+ const result = original.apply(this, args);
2069
+ handle();
2070
+ return result;
2071
+ };
2072
+ }
2073
+ fill(history, "pushState", wrapHistoryMethod);
2074
+ fill(history, "replaceState", wrapHistoryMethod);
2075
+ window.addEventListener("popstate", handle);
2076
+ function endRootAndFlush() {
2077
+ if (controller && !controller.isEnded) try {
2078
+ controller.endNow();
2079
+ } catch (error) {
2080
+ if (flare.config.debug) console.error("Flare: failed to end tracing root on page hide", error);
2081
+ }
2082
+ emitWebVitals(flare);
2083
+ try {
2084
+ flare.tracer.flush({ keepalive: true });
2085
+ } catch (error) {
2086
+ if (flare.config.debug) console.error("Flare: failed to flush spans on page hide", error);
2087
+ }
2088
+ }
2089
+ const onPageHide = () => endRootAndFlush();
2090
+ const onVisibilityChange = () => {
2091
+ if (document.visibilityState === "hidden") endRootAndFlush();
2092
+ };
2093
+ window.addEventListener("pagehide", onPageHide);
2094
+ document.addEventListener("visibilitychange", onVisibilityChange);
2095
+ uninstall = () => {
2096
+ unfill(history, "pushState");
2097
+ unfill(history, "replaceState");
2098
+ window.removeEventListener("popstate", handle);
2099
+ window.removeEventListener("pagehide", onPageHide);
2100
+ document.removeEventListener("visibilitychange", onVisibilityChange);
2101
+ };
2102
+ }
2103
+ /** Idempotent. */
2104
+ function stopBrowserTracing() {
2105
+ withLiveController((live) => live.endNow());
2106
+ controller = null;
2107
+ if (uninstall) {
2108
+ uninstall();
2109
+ uninstall = null;
2110
+ }
2111
+ activeFlare = null;
2112
+ currentRoot = null;
2113
+ pendingRouteName = null;
2114
+ pendingRouteNameOwner = null;
2115
+ lastPath = "";
2116
+ stopWebVitals();
2117
+ pageloadRoot = null;
2118
+ pageloadRootStartNano = 0;
2119
+ pageloadRoute = null;
2120
+ pageloadContext = {};
2121
+ }
2122
+ /**
2123
+ * Computes the url attributes a route rename carries, or null when there is nothing to add. Guarded
2124
+ * so the pin below (which runs outside ifRootLive's own try/catch) cannot throw into the host.
2125
+ */
2126
+ function urlAttributesFor(route) {
2127
+ if (route.url === void 0 || !activeFlare) return null;
2128
+ try {
2129
+ return browserSpanUrlAttributes(activeFlare.config, route.url);
2130
+ } catch {
2131
+ return null;
2132
+ }
2133
+ }
2134
+ /**
2135
+ * Rename the current root and update the attributes that go with the name, and pin the pageload's route
2136
+ * for the vitals emit. No-op once it closed; the pin is NOT gated the same way, see below.
2137
+ * With no root yet the name is held for the pageload root that opens next, rather than dropped.
2138
+ * `owner` stamps who is holding it, so a stale or superseded source cannot land its name later.
2139
+ */
2140
+ function applyRouteName(route, owner) {
2141
+ const root = currentRoot;
2142
+ if (!root) {
2143
+ pendingRouteName = route;
2144
+ pendingRouteNameOwner = owner ?? null;
2145
+ return;
2146
+ }
2147
+ const urlAttrs = urlAttributesFor(route);
2148
+ ifRootLive(() => {
2149
+ root.name = route.name;
2150
+ root.setAttribute("flare.entry_point.handler.identifier", route.name);
2151
+ root.setAttribute("http.route", route.name);
2152
+ root.setAttribute("flare.route.source", route.source);
2153
+ if (!urlAttrs) return;
2154
+ for (const [key, value] of Object.entries(urlAttrs)) root.setAttribute(key, value);
2155
+ });
2156
+ if (root !== pageloadRoot) return;
2157
+ pageloadRoute = {
2158
+ name: route.name,
2159
+ source: route.source
2160
+ };
2161
+ if (urlAttrs) pageloadContext = {
2162
+ ...pageloadContext,
2163
+ ...urlAttrs
2164
+ };
2165
+ }
2166
+ /**
2167
+ * While registered, the built-in History detection opens no roots and the caller drives navigation
2168
+ * through the returned handle. Last-wins, and a stale handle no-ops, so an HMR-replaced bootstrap
2169
+ * cannot tear down a newer registration.
2170
+ */
2171
+ function registerNavigationSource() {
2172
+ const token = {};
2173
+ if (navSource && activeFlare?.config.debug) console.debug("Flare: navigation source replaced");
2174
+ navSource = token;
2175
+ function active() {
2176
+ return navSource === token;
2177
+ }
2178
+ return {
2179
+ startNavigation(opts) {
2180
+ if (!active() || !activeFlare) return;
2181
+ const path = opts?.path ?? currentPath();
2182
+ lastPath = path;
2183
+ withLiveController((live) => live.endNow());
2184
+ startRoot(activeFlare, {
2185
+ spanType: _flareapp_core.BrowserSpanType.Navigation,
2186
+ startTimeUnixNano: (0, _flareapp_core.defaultNowNano)(),
2187
+ name: path,
2188
+ urlOverride: opts?.url,
2189
+ hold: opts?.hold
2190
+ });
2191
+ },
2192
+ setActiveRouteName(route) {
2193
+ if (!active()) return;
2194
+ applyRouteName(route, token);
2195
+ },
2196
+ settleNavigation(route) {
2197
+ if (!active()) return;
2198
+ applyRouteName(route, token);
2199
+ withLiveController((live) => live.releaseHold());
2200
+ },
2201
+ unregister() {
2202
+ if (!active()) return;
2203
+ withLiveController((live) => live.releaseHold());
2204
+ navSource = null;
2205
+ lastPath = currentPath();
2206
+ pendingRouteName = null;
2207
+ pendingRouteNameOwner = null;
2208
+ }
2209
+ };
2210
+ }
2211
+ /** For sibling tracing modules (the component-profiler seam) that need the live tracer. */
2212
+ function activeTracingFlare() {
2213
+ return activeFlare;
2214
+ }
2215
+
2216
+ //#endregion
2217
+ //#region src/createFlareResolver.ts
2218
+ /**
2219
+ * `process.env.NODE_ENV` is replaced inline by bundlers. The try/catch keeps a process-less
2220
+ * environment safe: treat "undetermined" as production (warn, never crash).
2221
+ */
2222
+ function isDevMode() {
2223
+ try {
2224
+ return process.env.NODE_ENV !== "production";
2225
+ } catch {
2226
+ return false;
2227
+ }
2228
+ }
2229
+ /**
2230
+ * Builds a per-package Flare resolver: `registerDefaultFlare` (wired once by the web entry) and
2231
+ * `resolveFlare` (called at wiring time). Each call holds its own default-provider state. The check
2232
+ * that warns about the Electron `__flare` bridge uses `packageName` in its message;
2233
+ * `injectInstruction` replaces the closing hint for packages whose advice differs (for example
2234
+ * svelte, which points at the preprocessor's importSource).
2235
+ */
2236
+ function createFlareResolver(config) {
2237
+ const { packageName } = config;
2238
+ const injectInstruction = config.injectInstruction ?? `Import ${packageName}/inject and pass the @flareapp/electron/renderer instance instead.`;
2239
+ let defaultProvider = null;
2240
+ function registerDefaultFlare(provider) {
2241
+ if (typeof window !== "undefined" && window.__flare) {
2242
+ const message = `[flare] ${packageName} (web root) was imported in a renderer where the Electron bridge is present, pulling the keyed @flareapp/js singleton into the renderer. ` + injectInstruction;
2243
+ if (isDevMode()) throw new Error(message);
2244
+ console.warn(message);
2245
+ }
2246
+ defaultProvider = provider;
2247
+ }
2248
+ function resolveFlare(explicit) {
2249
+ if (explicit) return explicit;
2250
+ if (defaultProvider) return defaultProvider();
2251
+ throw new Error(`[flare] No Flare instance available. Pass \`flare\` (e.g. from @flareapp/electron/renderer), or import ${packageName} (the package root) to use the @flareapp/js default singleton.`);
2252
+ }
2253
+ return {
2254
+ registerDefaultFlare,
2255
+ resolveFlare
2256
+ };
2257
+ }
2258
+
2259
+ //#endregion
2260
+ //#region src/browser/catchWindowErrors.ts
2261
+ /**
2262
+ * Wire up global `error` and `unhandledrejection` listeners, routing reports through `window.flare`
2263
+ * (assigned by Flare.light()/configure()). Events are dropped, not queued, when the global is absent.
2264
+ */
2265
+ function catchWindowErrors() {
2266
+ if (typeof window === "undefined") return;
2267
+ window.addEventListener("error", (event) => {
2268
+ const flare = window.flare;
2269
+ if (!flare) return;
2270
+ if (event.error instanceof Error) flare.reportSilently(event.error);
2271
+ });
2272
+ window.addEventListener("unhandledrejection", (event) => {
2273
+ const flare = window.flare;
2274
+ if (!flare) return;
2275
+ (0, _flareapp_core.routeRejection)(flare, event.reason);
2276
+ });
2277
+ }
2278
+
2279
+ //#endregion
2280
+ //#region src/tracing/componentProfiler.ts
2281
+ /** Unix nanos on the same clock the tracer uses for span timestamps. */
2282
+ const nowNano = _flareapp_core.defaultNowNano;
2283
+ /**
2284
+ * Reserved up front so descendants can point at a span before it is recorded. Null when the trace is at
2285
+ * its span cap: descendants record before this span does, so an id the cap will refuse orphans them.
2286
+ */
2287
+ function reserveSpanId(traceId) {
2288
+ if (traceId !== void 0 && !activeTracingFlare()?.tracer.claimSpanSlot(traceId)) return null;
2289
+ return (0, _flareapp_core.spanId)();
2290
+ }
2291
+ /** The root a top-level component nests under. Null when tracing is off or no root is recording. */
2292
+ function activeComponentRoot() {
2293
+ try {
2294
+ const root = activeTracingFlare()?.tracer.getActiveSpan();
2295
+ if (!root || !root.isRecording) return null;
2296
+ return {
2297
+ traceId: root.traceId,
2298
+ parentSpanId: root.spanId
2299
+ };
2300
+ } catch {
2301
+ return null;
2302
+ }
2303
+ }
2304
+ /**
2305
+ * An ancestor's context is only usable while it still belongs to the live trace. A profiled component
2306
+ * that survives a navigation (a layout around a swapped page body) froze its context under the pageload
2307
+ * trace, and `recordComponentSpan` would drop anything pointing at that closed root.
2308
+ */
2309
+ function resolveComponentParent(inherited, live) {
2310
+ if (inherited && live && inherited.traceId === live.traceId) return inherited;
2311
+ return live;
2312
+ }
2313
+ /**
2314
+ * Records only while the reserved root is still the live recording root, and drops the span otherwise.
2315
+ * Dropping avoids starting a fresh TraceState for a dead trace, which would re-run the sampler, and
2316
+ * avoids adding a child to a root that already shipped.
2317
+ */
2318
+ function recordComponentSpan(record) {
2319
+ try {
2320
+ const flare = activeTracingFlare();
2321
+ if (!flare) return;
2322
+ const root = flare.tracer.getActiveSpan();
2323
+ if (!root || root.traceId !== record.parent.traceId || !root.isRecording) return;
2324
+ flare.startSpan(record.name, {
2325
+ spanId: record.spanId,
2326
+ parent: {
2327
+ traceId: record.parent.traceId,
2328
+ spanId: record.parent.parentSpanId
2329
+ },
2330
+ spanType: _flareapp_core.BrowserSpanType.Component,
2331
+ startTimeUnixNano: record.startTimeUnixNano,
2332
+ attributes: {
2333
+ ...record.attributes,
2334
+ "flare.component.name": record.name
2335
+ },
2336
+ claimed: true
2337
+ }).end(record.endTimeUnixNano);
2338
+ } catch {}
2339
+ }
2340
+
2341
+ //#endregion
2342
+ Object.defineProperty(exports, 'BrowserFlushScheduler', {
2343
+ enumerable: true,
2344
+ get: function () {
2345
+ return BrowserFlushScheduler;
2346
+ }
2347
+ });
2348
+ Object.defineProperty(exports, 'CLIENT_VERSION', {
2349
+ enumerable: true,
2350
+ get: function () {
2351
+ return CLIENT_VERSION;
2352
+ }
2353
+ });
2354
+ Object.defineProperty(exports, 'FetchFileReader', {
2355
+ enumerable: true,
2356
+ get: function () {
2357
+ return FetchFileReader;
2358
+ }
2359
+ });
2360
+ Object.defineProperty(exports, 'absoluteHref', {
2361
+ enumerable: true,
2362
+ get: function () {
2363
+ return absoluteHref;
2364
+ }
2365
+ });
2366
+ Object.defineProperty(exports, 'absoluteUrl', {
2367
+ enumerable: true,
2368
+ get: function () {
2369
+ return absoluteUrl;
2370
+ }
2371
+ });
2372
+ Object.defineProperty(exports, 'activeComponentRoot', {
2373
+ enumerable: true,
2374
+ get: function () {
2375
+ return activeComponentRoot;
2376
+ }
2377
+ });
2378
+ Object.defineProperty(exports, 'catchWindowErrors', {
2379
+ enumerable: true,
2380
+ get: function () {
2381
+ return catchWindowErrors;
2382
+ }
2383
+ });
2384
+ Object.defineProperty(exports, 'collectBrowser', {
2385
+ enumerable: true,
2386
+ get: function () {
2387
+ return collectBrowser;
2388
+ }
2389
+ });
2390
+ Object.defineProperty(exports, 'createFlareResolver', {
2391
+ enumerable: true,
2392
+ get: function () {
2393
+ return createFlareResolver;
2394
+ }
2395
+ });
2396
+ Object.defineProperty(exports, 'currentPath', {
2397
+ enumerable: true,
2398
+ get: function () {
2399
+ return currentPath;
2400
+ }
2401
+ });
2402
+ Object.defineProperty(exports, 'instrumentFetch', {
2403
+ enumerable: true,
2404
+ get: function () {
2405
+ return instrumentFetch;
2406
+ }
2407
+ });
2408
+ Object.defineProperty(exports, 'instrumentOnce', {
2409
+ enumerable: true,
2410
+ get: function () {
2411
+ return instrumentOnce;
2412
+ }
2413
+ });
2414
+ Object.defineProperty(exports, 'instrumentXHR', {
2415
+ enumerable: true,
2416
+ get: function () {
2417
+ return instrumentXHR;
2418
+ }
2419
+ });
2420
+ Object.defineProperty(exports, 'insulate', {
2421
+ enumerable: true,
2422
+ get: function () {
2423
+ return insulate;
2424
+ }
2425
+ });
2426
+ Object.defineProperty(exports, 'nowNano', {
2427
+ enumerable: true,
2428
+ get: function () {
2429
+ return nowNano;
2430
+ }
2431
+ });
2432
+ Object.defineProperty(exports, 'recordComponentSpan', {
2433
+ enumerable: true,
2434
+ get: function () {
2435
+ return recordComponentSpan;
2436
+ }
2437
+ });
2438
+ Object.defineProperty(exports, 'registerNavigationSource', {
2439
+ enumerable: true,
2440
+ get: function () {
2441
+ return registerNavigationSource;
2442
+ }
2443
+ });
2444
+ Object.defineProperty(exports, 'reserveSpanId', {
2445
+ enumerable: true,
2446
+ get: function () {
2447
+ return reserveSpanId;
2448
+ }
2449
+ });
2450
+ Object.defineProperty(exports, 'resolveComponentParent', {
2451
+ enumerable: true,
2452
+ get: function () {
2453
+ return resolveComponentParent;
2454
+ }
2455
+ });
2456
+ Object.defineProperty(exports, 'resolveHref', {
2457
+ enumerable: true,
2458
+ get: function () {
2459
+ return resolveHref;
2460
+ }
2461
+ });
2462
+ Object.defineProperty(exports, 'routeName', {
2463
+ enumerable: true,
2464
+ get: function () {
2465
+ return routeName;
2466
+ }
2467
+ });
2468
+ Object.defineProperty(exports, 'safeInvoke', {
2469
+ enumerable: true,
2470
+ get: function () {
2471
+ return safeInvoke;
2472
+ }
2473
+ });
2474
+ Object.defineProperty(exports, 'startBrowserTracing', {
2475
+ enumerable: true,
2476
+ get: function () {
2477
+ return startBrowserTracing;
2478
+ }
2479
+ });
2480
+ Object.defineProperty(exports, 'stopBrowserTracing', {
2481
+ enumerable: true,
2482
+ get: function () {
2483
+ return stopBrowserTracing;
2484
+ }
2485
+ });
2486
+ Object.defineProperty(exports, 'unpatchFetch', {
2487
+ enumerable: true,
2488
+ get: function () {
2489
+ return unpatchFetch;
2490
+ }
2491
+ });
2492
+ Object.defineProperty(exports, 'unpatchXHR', {
2493
+ enumerable: true,
2494
+ get: function () {
2495
+ return unpatchXHR;
2496
+ }
2497
+ });