@flareapp/js 2.11.0 → 2.12.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.
@@ -73,10 +73,11 @@ var FormChangeRecorder = class {
73
73
  //#endregion
74
74
  //#region src/tracing/utils/absoluteHref.ts
75
75
  /**
76
- * Resolve a router-reported href against the page we are on. Returns the `URL`, so a caller that
77
- * wants the pathname as well as the href does not parse it a second time.
76
+ * Resolves a router-reported href against the current page. Returns the `URL`, so a caller that also
77
+ * wants the pathname does not have to parse it again.
78
78
  *
79
- * Undefined outside a browser or for an unparseable href, so the caller can leave its attribute alone.
79
+ * Returns undefined outside a browser or for an unparseable href, so the caller can leave its
80
+ * attribute alone.
80
81
  */
81
82
  function absoluteUrl(href) {
82
83
  if (href == null || typeof window === "undefined") return;
@@ -87,9 +88,9 @@ function absoluteUrl(href) {
87
88
  }
88
89
  }
89
90
  /**
90
- * The href form of `absoluteUrl`. Pass one built by the router's own `createHref`/`resolve` (see
91
- * `resolveHref`), not a bare path: routers strip the app's base path, so `origin + path` yields an
92
- * address the server does not have.
91
+ * The href form of `absoluteUrl`. Pass a value built by the router's own `createHref`/`resolve`, not
92
+ * a bare path routers strip the app's base path, so `origin + path` gives an address the server
93
+ * does not have.
93
94
  */
94
95
  function absoluteHref(href) {
95
96
  return absoluteUrl(href)?.href;
@@ -100,9 +101,14 @@ function absoluteHref(href) {
100
101
  function currentPath() {
101
102
  return typeof location !== "undefined" ? location.pathname : "";
102
103
  }
104
+ /** The whole address, query string included. */
103
105
  function currentHref() {
104
106
  return typeof location !== "undefined" ? location.href : "";
105
107
  }
108
+ /**
109
+ * Prefers the router's route template (`/product/:id`) over the raw path, so all urls of one route
110
+ * group together. If `derive` throws, the fallback path is used instead of breaking the app.
111
+ */
106
112
  function routeName(derive, fallbackPath, url) {
107
113
  try {
108
114
  const name = derive();
@@ -119,9 +125,9 @@ function routeName(derive, fallbackPath, url) {
119
125
  };
120
126
  }
121
127
  /**
122
- * `build` is the router's own href builder (vue-router `resolve`, React Router `createHref`). It puts
123
- * the app's base path and hash prefix back. Without it, an app served from `/app/` reports
124
- * `/product/p01` instead of `/app/product/p01`. If `build` throws, we use `fallback`.
128
+ * `build` is the router's own href builder (vue-router `resolve`, React Router `createHref`). It restores
129
+ * the app's base path and hash prefix, so an app served from `/app/` reports `/app/product/p01` instead
130
+ * of `/product/p01`. Falls back to `fallbackHref` if `build` throws.
125
131
  */
126
132
  function resolveHref(build, fallbackHref) {
127
133
  let href = fallbackHref;
@@ -133,12 +139,6 @@ function resolveHref(build, fallbackHref) {
133
139
 
134
140
  //#endregion
135
141
  //#region src/tracing/utils/fill.ts
136
- /**
137
- * Replace `source[name]` with `replacer(original)`, tagging the wrapper with a
138
- * non-enumerable `__flare_original__` so the patch is idempotent and reversible.
139
- * Ported from Sentry's `fill` (packages/core/src/utils/object.ts), minus the
140
- * prototype/own-property copying we do not need for `fetch`.
141
- */
142
142
  function fill(source, name, replacer) {
143
143
  const original = source[name];
144
144
  if (typeof original !== "function") return;
@@ -152,7 +152,6 @@ function fill(source, name, replacer) {
152
152
  });
153
153
  source[name] = wrapped;
154
154
  }
155
- /** Restore a previously `fill`ed property to its original. Safe if never filled. */
156
155
  function unfill(source, name) {
157
156
  const current = source[name];
158
157
  if (current && current.__flare_original__) source[name] = current.__flare_original__;
@@ -262,7 +261,6 @@ function registerNavigationSource() {
262
261
  }
263
262
  };
264
263
  }
265
- /** A name from an earlier call is only valid while that source is still registered. */
266
264
  function isActiveNavigationSource(token) {
267
265
  return token !== null && source === token;
268
266
  }
@@ -331,12 +329,6 @@ How to fix: use one Flare instance, and check your bundle for two copies of @fla
331
329
  function hasRequestSubscribers() {
332
330
  return subscribers.size > 0 || mutator !== null;
333
331
  }
334
- /**
335
- * Tells every subscriber a request is about to go out. Returns the (possibly mutated) `init` and
336
- * `headers` plus one `settle` callback that fans the result out to every subscriber. Returns null
337
- * when nothing acted on the request; the wrapper must then call the real fetch or send untouched.
338
- * A subscriber that throws is skipped, so instrumentation never breaks the app's request.
339
- */
340
332
  function publishRequestStart(start) {
341
333
  const handlers = [];
342
334
  for (const subscriber of subscribers) try {
@@ -367,17 +359,7 @@ function publishRequestStart(start) {
367
359
 
368
360
  //#endregion
369
361
  //#region src/tracing/requests/internalRequest.ts
370
- /**
371
- * Marks a request the SDK makes for its own bookkeeping (right now: fetching a source file so a
372
- * stack frame can show a code snippet). The fetch patch passes those straight through: they are
373
- * not the app's traffic, so tracing them puts a span in the customer's waterfall for a request
374
- * their code never made, and propagating a `traceparent` on them is just as wrong.
375
- *
376
- * Flare's ingest calls are excluded by URL instead (`isFlareIngestUrl`), because their endpoints
377
- * are known up front. A snippet fetch targets the customer's own asset, so only the caller knows.
378
- */
379
362
  const INTERNAL_REQUEST_KEY = "__flare_internal_request__";
380
- /** An init that marks the request as Flare's own. Unknown init keys are ignored by `fetch`. */
381
363
  function internalRequestInit(init) {
382
364
  return {
383
365
  ...init,
@@ -390,16 +372,9 @@ function isInternalRequest(init) {
390
372
 
391
373
  //#endregion
392
374
  //#region src/tracing/requests/supportsNativeFetch.ts
393
- /** True if `fn` is the browser's native fetch (not a polyfill/wrapper). */
394
375
  function isNativeFetch(fn) {
395
376
  return typeof fn === "function" && /native code/.test(Function.prototype.toString.call(fn));
396
377
  }
397
- /**
398
- * Whether the current global `fetch` is native. A polyfilled fetch (e.g. whatwg-fetch) is
399
- * XHR-backed; skip instrumenting it so the XHR patch is the single source for those requests.
400
- * Ported from Sentry, including the hidden-iframe fallback used when another library has already
401
- * wrapped `fetch` and the direct toString check is unreliable.
402
- */
403
378
  function supportsNativeFetch() {
404
379
  const globals = globalThis;
405
380
  if (typeof globals.fetch !== "function") return false;
@@ -427,10 +402,6 @@ function supportsNativeFetch() {
427
402
 
428
403
  //#endregion
429
404
  //#region src/tracing/utils/createPatcher.ts
430
- /**
431
- * One `installed` flag for the whole set, not one per method: `open` remembers the URL that `send`
432
- * reads, so a half patched set is broken.
433
- */
434
405
  function createPatcher() {
435
406
  let installed = false;
436
407
  let names = [];
@@ -638,12 +609,6 @@ function unpatchXHR() {
638
609
  //#endregion
639
610
  //#region src/instrumentation/requests/requestPatches.ts
640
611
  let subscriptions = 0;
641
- /**
642
- * Keeps fetch and XHR patched while at least one subscriber lives. Counted, so turning tracing off
643
- * cannot remove a patch that breadcrumbs still need.
644
- *
645
- * @param subscribe registers one subscriber and returns its own teardown
646
- */
647
612
  function withRequestPatches(subscribe) {
648
613
  if (subscriptions === 0) {
649
614
  instrumentFetch();
@@ -666,7 +631,6 @@ function withRequestPatches(subscribe) {
666
631
 
667
632
  //#endregion
668
633
  //#region src/tracing/requests/propagation.ts
669
- /** Follows OTel/Sentry `tracePropagationTargets`: same-origin by default, `[]` disables all. */
670
634
  function shouldPropagate(url, absoluteUrl, currentOrigin, targets) {
671
635
  if (targets) {
672
636
  if (targets.length === 0) return false;
@@ -674,8 +638,6 @@ function shouldPropagate(url, absoluteUrl, currentOrigin, targets) {
674
638
  }
675
639
  return absoluteUrl !== null && absoluteUrl.origin === currentOrigin;
676
640
  }
677
- /** Null on a throwing or malformed entry: the caller then passes the source through untouched, so a
678
- * bad merge never breaks the host request. */
679
641
  function headerPairsFrom(source) {
680
642
  try {
681
643
  const pairs = [];
@@ -690,16 +652,9 @@ function headerPairsFrom(source) {
690
652
  return null;
691
653
  }
692
654
  }
693
- /** Fetch accepts any iterable of string pairs as HeadersInit (Map, URLSearchParams, cross-realm Headers). */
694
655
  function isIterable(value) {
695
656
  return value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.iterator] === "function";
696
657
  }
697
- /**
698
- * A new `RequestInit` carrying `traceparent`, without mutating the caller's `Request` or `init`.
699
- * Caller-wins: a `traceparent` the caller already set is left alone, matching XHR's
700
- * `hasAppTraceparent` skip. Returning an init rather than a rebuilt `Request` keeps the caller's
701
- * single-shot body intact.
702
- */
703
658
  function mergeTraceparentHeader(input, init, traceparent) {
704
659
  const source = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
705
660
  let headers;
@@ -739,7 +694,6 @@ const REQUEST_SPAN_TYPES = {
739
694
  xhr: _flareapp_core.BrowserSpanType.Xhr
740
695
  };
741
696
  const INLINE_SCHEMES = new Set(["data:", "blob:"]);
742
- /** The real browser context. Falls back to the origin where there is no document (SSR, tests). */
743
697
  function browserUrlContext() {
744
698
  const origin = globalThis.location?.origin ?? "";
745
699
  return {
@@ -747,7 +701,6 @@ function browserUrlContext() {
747
701
  base: () => globalThis.document?.baseURI || origin
748
702
  };
749
703
  }
750
- /** Resolve `url` to an absolute URL against `base`, or null if it cannot be parsed. */
751
704
  function safeAbsolute(url, base) {
752
705
  try {
753
706
  return new URL(url, base || void 0);
@@ -775,20 +728,10 @@ function matchesIngestHref(href, ingestHref) {
775
728
  const next = href.charAt(ingestHref.length);
776
729
  return next === "" || next === "/" || next === "?" || next === "#";
777
730
  }
778
- /**
779
- * True when `resolved` targets one of Flare's own ingest endpoints (never traced). The configured
780
- * URLs are resolved against `base` first: a relative one (a customer proxying ingest through their
781
- * own origin) would otherwise never match, so every flush POST would open a span that arms the next
782
- * flush, forever.
783
- */
784
731
  function isFlareIngestUrl(resolved, config, base) {
785
732
  if (!resolved) return false;
786
733
  return resolvedIngestHrefs(config, base).some((ingestHref) => matchesIngestHref(resolved.href, ingestHref));
787
734
  }
788
- /**
789
- * Shared request-span attributes for a fetch/XHR call. The `url.*` attributes are redacted the same
790
- * way as error reports, so tokens and reset codes never leak.
791
- */
792
735
  function requestSpanAttributes(method, resolved, url, config) {
793
736
  return {
794
737
  "http.request.method": method,
@@ -797,12 +740,6 @@ function requestSpanAttributes(method, resolved, url, config) {
797
740
  ...resolved && resolved.port ? { "server.port": Number(resolved.port) } : {}
798
741
  };
799
742
  }
800
- /**
801
- * Completion mapping shared by fetch and XHR: record the status and mark an error on 5xx.
802
- * `zeroIsError` additionally maps status 0 to error. XHR passes it only for http(s), where status
803
- * 0 at DONE is always a network/CORS failure or abort; file:// and custom schemes return 0 on
804
- * success, so it isn't set there. Fetch never passes it (an opaque no-cors response is 0, not error).
805
- */
806
743
  function endHttpRequestSpan(span, status, opts) {
807
744
  span.setAttribute("http.response.status_code", status);
808
745
  if (status >= 500 || opts?.zeroIsError && status === 0) span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
@@ -815,21 +752,10 @@ function finishHttpSpanError(span, error) {
815
752
  });
816
753
  span.end();
817
754
  }
818
- /**
819
- * Propagation gate plus `traceparent` build shared by fetch and XHR. Returns null when
820
- * `shouldPropagate` rejects the URL (caller then skips header injection).
821
- */
822
755
  function traceparentFor(span, resolved, url, origin, config) {
823
756
  if (!shouldPropagate(resolved ? resolved.href : url, resolved, origin, config.tracePropagationTargets)) return null;
824
757
  return (0, _flareapp_core.buildTraceparent)(span.traceId, span.spanId, span.isRecording);
825
758
  }
826
- /**
827
- * Open a request span for one outgoing fetch or XHR call. Null means the URL is one of Flare's own
828
- * ingest endpoints, so the caller passes the request through untraced.
829
- *
830
- * `absoluteUrl` comes back with the span because both callers need it afterwards: for the traceparent
831
- * gate, and for XHR's http(s)-only status-0 rule.
832
- */
833
759
  function startHttpRequestSpan(tracer, request) {
834
760
  const { method, url, urls, spanType } = request;
835
761
  const config = tracer.config;
@@ -849,12 +775,6 @@ function startHttpRequestSpan(tracer, request) {
849
775
 
850
776
  //#endregion
851
777
  //#region src/tracing/requests/traceRequests.ts
852
- /**
853
- * For http and https, status 0 at DONE means the request got no response.
854
- *
855
- * Other schemes return 0 when they succeed. file:// does, and so do custom ones like Electron's
856
- * registerFileProtocol. A URL we could not parse is not an error either.
857
- */
858
778
  function zeroIsError(absoluteUrl) {
859
779
  return absoluteUrl !== null && (absoluteUrl.protocol === "http:" || absoluteUrl.protocol === "https:");
860
780
  }
@@ -865,7 +785,6 @@ function propagate(span, absoluteUrl, start, urls, tracer) {
865
785
  if (start.input === void 0) return {};
866
786
  return { init: mergeTraceparentHeader(start.input, start.init, traceparent) };
867
787
  }
868
- /** Tracing takes the mutation slot, not a plain subscription, because it adds a `traceparent` header. */
869
788
  function traceRequests(tracer, urls) {
870
789
  return claimRequestMutation((start) => {
871
790
  if (!tracer.config.enableTracing) return;
@@ -941,7 +860,6 @@ var RequestRecorder = class {
941
860
 
942
861
  //#endregion
943
862
  //#region src/breadcrumbs/index.ts
944
- /** Starts every recorder, returns one teardown. A recorder that fails to install is skipped. */
945
863
  function startBreadcrumbs(host) {
946
864
  if (typeof document === "undefined") return () => {};
947
865
  const recorders = [
@@ -975,10 +893,6 @@ var BrowserFlushScheduler = class {
975
893
 
976
894
  //#endregion
977
895
  //#region src/browser/context/cookie.ts
978
- /**
979
- * Parses `document.cookie` into `http.request.cookies`, redacting the value of any cookie whose name
980
- * matches `denylist`. Null-prototype accumulator so a cookie named `__proto__` is stored, not dropped.
981
- */
982
896
  function cookie(denylist) {
983
897
  if (!window.document.cookie) return {};
984
898
  const cookies = Object.create(null);
@@ -1037,7 +951,6 @@ function readTimezone() {
1037
951
 
1038
952
  //#endregion
1039
953
  //#region src/browser/context/deviceInfo.ts
1040
- /** Reads what the User-Agent lacks: hardware, screen, network. Static reads cached; screen and network per call. */
1041
954
  var BrowserDeviceInfoProvider = class {
1042
955
  staticInfo = null;
1043
956
  collect() {
@@ -1072,16 +985,10 @@ var BrowserDeviceInfoProvider = class {
1072
985
  return this.staticInfo;
1073
986
  }
1074
987
  };
1075
- /** Shared singleton so the static read is cached across reports. */
1076
988
  const browserDeviceInfoProvider = new BrowserDeviceInfoProvider();
1077
989
 
1078
990
  //#endregion
1079
991
  //#region src/browser/context/request.ts
1080
- /**
1081
- * @param hrefOverride when set, the `url.*` attributes come from it instead of the live
1082
- * `window.location.href` (a framework navigation root whose router knows the destination
1083
- * before the URL commits). The override is pre-validated by the caller.
1084
- */
1085
992
  function request(urlDenylist, hrefOverride) {
1086
993
  return {
1087
994
  ...(0, _flareapp_core.urlAttributes)(hrefOverride ?? window.location.href, urlDenylist),
@@ -1137,7 +1044,7 @@ var FetchFileReader = class {
1137
1044
 
1138
1045
  //#endregion
1139
1046
  //#region src/env/index.ts
1140
- const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.11.0" : "?";
1047
+ const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.12.0" : "?";
1141
1048
 
1142
1049
  //#endregion
1143
1050
  //#region src/tracing/utils/instrumentationGuard.ts
@@ -1157,16 +1064,16 @@ function safeInvoke(fn) {
1157
1064
  }
1158
1065
  const instrumented = /* @__PURE__ */ new WeakMap();
1159
1066
  /**
1160
- * Instrument `target` at most once at a time, tearing down any prior instrumentation of the same object
1161
- * first. Vite HMR re-runs boot code against a router that survives the reload, so without this every
1162
- * cycle appends another listener set that is never removed. Keyed on the object, so a genuinely new
1067
+ * Instruments `target` at most once at a time, tearing down any prior instrumentation of the same
1068
+ * object first. Vite HMR re-runs boot code against a router that survives the reload, so without this
1069
+ * every cycle would add another listener set that is never removed. Keyed on the object, so a new
1163
1070
  * router is unaffected.
1164
1071
  *
1165
- * `install` hands each teardown to `track` as it produces it. A router's own `subscribe` / `on` / guard
1166
- * registration can throw, and `install` runs during the host's bootstrap, so a throw part-way through
1167
- * unwinds what already succeeded (newest first) and stops here rather than reaching the host.
1072
+ * `install` hands each teardown to `track` as it produces it. A router's `subscribe`/`on`/guard call
1073
+ * can throw during the host's bootstrap, so a throw part-way through unwinds what already succeeded
1074
+ * (newest first) instead of reaching the host.
1168
1075
  *
1169
- * @returns the cleanup, or a no-op when the install failed and already unwound itself.
1076
+ * @returns The cleanup function, or a no-op if install failed and already unwound itself.
1170
1077
  */
1171
1078
  function instrumentOnce(target, install) {
1172
1079
  instrumented.get(target)?.();
@@ -1192,13 +1099,6 @@ function instrumentOnce(target, install) {
1192
1099
 
1193
1100
  //#endregion
1194
1101
  //#region src/browser/context/collectBrowserSpanContext.ts
1195
- /**
1196
- * Entry point plus request identity for a pageload/navigation root. Deliberately leaner than the report
1197
- * context. Captured at span start, so a long-lived root reflects the page it represents rather than the
1198
- * page at close.
1199
- *
1200
- * @param hrefOverride destination href for a router that reports where it is going before the URL commits.
1201
- */
1202
1102
  function collectBrowserSpanContext(config, hrefOverride) {
1203
1103
  if (typeof window === "undefined") return {};
1204
1104
  const url = absoluteUrl(hrefOverride);
@@ -1207,10 +1107,6 @@ function collectBrowserSpanContext(config, hrefOverride) {
1207
1107
  ...request(config.urlDenylist, url?.href)
1208
1108
  };
1209
1109
  }
1210
- /**
1211
- * Updates a root's url after a redirect, or when a newer navigation replaces this one. The root opened
1212
- * with the first destination, so without this it reports a page the user never reached.
1213
- */
1214
1110
  function browserSpanUrlAttributes(config, href) {
1215
1111
  if (typeof window === "undefined") return {};
1216
1112
  const resolved = absoluteUrl(href);
@@ -2008,28 +1904,12 @@ const onTTFB = (onReport, opts = {}) => {
2008
1904
 
2009
1905
  //#endregion
2010
1906
  //#region src/tracing/vitals/webVitals.ts
2011
- /**
2012
- * Final the moment they first report, so they can ride the pageload span itself. The other three keep
2013
- * changing until the page goes away, and stamping an early value on the root would leave the root and
2014
- * the later span disagreeing about the same vital.
2015
- */
2016
1907
  const EARLY_VITALS = ["ttfb", "fcp"];
2017
- /** The one place a vital becomes a wire key. Both the pageload stamp and the late span go through it. */
2018
1908
  function vitalAttributes(vitals) {
2019
1909
  const attributes = {};
2020
1910
  for (const [name, value] of Object.entries(vitals)) if (typeof value === "number") attributes[`browser.web_vital.${name}`] = value;
2021
1911
  return attributes;
2022
1912
  }
2023
- /**
2024
- * Turns the leftover values into one zero-duration span. Pure on purpose: the shape is what the backend
2025
- * groups on, and this way it is testable without a tracer or a clock.
2026
- *
2027
- * Both timestamps sit at the pageload root's start. `spans_2` buckets on `start_time_unix_nano`, so
2028
- * stamping the report moment would drop a tab left open for forty minutes into a minute forty minutes
2029
- * after the page actually loaded.
2030
- *
2031
- * Returns null when nothing is left to report, so the caller emits no span at all.
2032
- */
2033
1913
  function buildVitalsSpan(input) {
2034
1914
  const vitals = vitalAttributes(input.vitals);
2035
1915
  if (Object.keys(vitals).length === 0) return null;
@@ -2059,10 +1939,6 @@ function defaultSubscribers() {
2059
1939
  onINP: (cb) => onINP(cb, { reportAllChanges: true })
2060
1940
  };
2061
1941
  }
2062
- /**
2063
- * Subscribes at most once per document: upstream's on* functions return no unsubscribe handle, so a
2064
- * second call would attach a second set of observers with no way to detach either.
2065
- */
2066
1942
  function startWebVitals(subscribers = defaultSubscribers()) {
2067
1943
  recording = true;
2068
1944
  if (subscribed) return;
@@ -2082,21 +1958,10 @@ function record(name, metric) {
2082
1958
  if (!recording || typeof metric?.value !== "number") return;
2083
1959
  collected[name] = metric.value;
2084
1960
  }
2085
- /**
2086
- * Stops recording and drops what was collected. The observers themselves cannot be detached, and both
2087
- * the `subscribed` and `taken` latches deliberately survive: clearing `subscribed` would let a re-enable
2088
- * attach a second set of observers that is just as undetachable as the first, and clearing `taken` would
2089
- * let those surviving observers refill `collected` and ship a second `browser_web_vital` for the same
2090
- * document once the page is re-enabled and later hidden.
2091
- */
2092
1961
  function stopWebVitals() {
2093
1962
  recording = false;
2094
1963
  collected = {};
2095
1964
  }
2096
- /**
2097
- * The vitals that are already final when the pageload root closes, removed from `collected` so the late
2098
- * span cannot report them a second time. Naturally idempotent: a second call finds the keys gone.
2099
- */
2100
1965
  function takeEarlyVitals() {
2101
1966
  if (!recording) return null;
2102
1967
  const taking = {};
@@ -2109,7 +1974,6 @@ function takeEarlyVitals() {
2109
1974
  }
2110
1975
  return Object.keys(taking).length === 0 ? null : taking;
2111
1976
  }
2112
- /** Everything still outstanding, once. Null afterwards, and null when nothing is left. */
2113
1977
  function takeWebVitals() {
2114
1978
  if (taken || !recording || Object.keys(collected).length === 0) return null;
2115
1979
  taken = true;
@@ -2117,29 +1981,76 @@ function takeWebVitals() {
2117
1981
  collected = {};
2118
1982
  return taking;
2119
1983
  }
2120
- /**
2121
- * Undoes a take after the emit failed partway through, so the values are not lost and a later trigger
2122
- * can retry. Safe to assign outright rather than merge: the whole emit runs synchronously between the
2123
- * take and a catch block calling this, so nothing else can have written to `collected` in between.
2124
- */
2125
1984
  function restoreWebVitals(vitals) {
2126
1985
  collected = vitals;
2127
1986
  taken = false;
2128
1987
  }
2129
1988
 
1989
+ //#endregion
1990
+ //#region src/tracing/roots/componentSelfTime.ts
1991
+ /**
1992
+ * Only a parent that never records leaves an entry behind: an async child that registers after its
1993
+ * parent already shipped. The normal path frees every entry the moment the parent records, so this
1994
+ * cap is a backstop, not a working limit.
1995
+ */
1996
+ const MAX_TRACKED_PARENTS = 256;
1997
+ const childIntervals = /* @__PURE__ */ new Map();
1998
+ /** Files a recorded child under its parent, so the parent can subtract it. */
1999
+ function trackChildInterval(parentSpanId, startTimeUnixNano, endTimeUnixNano) {
2000
+ const existing = childIntervals.get(parentSpanId);
2001
+ if (existing) {
2002
+ existing.push([startTimeUnixNano, endTimeUnixNano]);
2003
+ return;
2004
+ }
2005
+ (0, _flareapp_core.evictLruIfNew)(childIntervals, parentSpanId, MAX_TRACKED_PARENTS);
2006
+ childIntervals.set(parentSpanId, [[startTimeUnixNano, endTimeUnixNano]]);
2007
+ }
2008
+ /**
2009
+ * Duration minus the time the component's own children already account for, and consumes those
2010
+ * children. Grandchildren need no handling: they sit inside a child's interval.
2011
+ *
2012
+ * Children that record after their parent are not subtracted, so the value is self time at commit.
2013
+ * That covers async components and `<Suspense>`, and a vue-router layout too: the initial route
2014
+ * resolves after the layout mounted, so the page component's work falls outside the layout's window
2015
+ * and the layout keeps its full duration.
2016
+ */
2017
+ function takeSelfTime(spanId, startTimeUnixNano, endTimeUnixNano) {
2018
+ const children = childIntervals.get(spanId);
2019
+ childIntervals.delete(spanId);
2020
+ const duration = endTimeUnixNano - startTimeUnixNano;
2021
+ if (!children) return Math.max(0, duration);
2022
+ return Math.max(0, duration - coveredTime(children, startTimeUnixNano, endTimeUnixNano));
2023
+ }
2024
+ function resetComponentSelfTime() {
2025
+ childIntervals.clear();
2026
+ }
2027
+ /**
2028
+ * The union of the intervals, clamped to the parent window. A sum would double-count: React starts
2029
+ * every component during render and ends them all during commit, so siblings overlap, and summing
2030
+ * their durations exceeds the parent's own.
2031
+ */
2032
+ function coveredTime(intervals, start, end) {
2033
+ intervals.sort((a, b) => a[0] - b[0]);
2034
+ let covered = 0;
2035
+ let cursor = start;
2036
+ for (const [childStart, childEnd] of intervals) {
2037
+ const from = Math.max(childStart, cursor);
2038
+ const to = Math.min(childEnd, end);
2039
+ if (to > from) {
2040
+ covered += to - from;
2041
+ cursor = to;
2042
+ }
2043
+ }
2044
+ return covered;
2045
+ }
2046
+
2130
2047
  //#endregion
2131
2048
  //#region src/tracing/roots/IdleRootController.ts
2132
- /** Browser defaults for the three idle-root timeouts, in ms. Overridable per Config. */
2133
2049
  const DEFAULT_IDLE_TIMEOUTS = {
2134
2050
  idleTimeout: 1e3,
2135
2051
  finalTimeout: 3e4,
2136
2052
  childSpanTimeout: 15e3
2137
2053
  };
2138
- /**
2139
- * Owns one root span's idle lifecycle: open while child spans in its trace are active, closing after
2140
- * `idleTimeout` with no open children, or on the `finalTimeout` / `childSpanTimeout` backstops. Deps are
2141
- * injected so this is testable without real timers or a real tracer.
2142
- */
2143
2054
  var IdleRootController = class {
2144
2055
  openChildren = 0;
2145
2056
  lastChildEndTime = null;
@@ -2164,17 +2075,9 @@ var IdleRootController = class {
2164
2075
  get isEnded() {
2165
2076
  return this.ended;
2166
2077
  }
2167
- /** For a route change or pagehide. */
2168
2078
  endNow() {
2169
2079
  this.finish(this.openChildren > 0 || this.held ? this.deps.now() : this.trimmedEnd());
2170
2080
  }
2171
- /**
2172
- * Records the settle moment as a close floor and hands the root back to the normal idle lifecycle. It
2173
- * deliberately does not close here: a router settles before the framework mounts the new route
2174
- * component (vue-router runs `afterEach` in the route-update tick, Vue mounts on the next flush), so
2175
- * closing at settle cleared the active root ahead of every post-navigation mount: every component span
2176
- * read a null root, and a trailing fetch opened a root of its own.
2177
- */
2178
2081
  releaseHold() {
2179
2082
  if (this.ended || !this.held) return;
2180
2083
  this.held = false;
@@ -2211,8 +2114,6 @@ var IdleRootController = class {
2211
2114
  this.finish(this.trimmedEnd());
2212
2115
  }, this.timeouts.idleTimeout);
2213
2116
  }
2214
- /** The latest of the floor, a released hold's settle moment, and the last child's end, so a root
2215
- * covers its children without ever padding out to `now()`. */
2216
2117
  trimmedEnd() {
2217
2118
  return Math.max(this.deps.endFloor(), this.settleTime ?? 0, this.lastChildEndTime ?? 0);
2218
2119
  }
@@ -2250,21 +2151,14 @@ var IdleRootController = class {
2250
2151
 
2251
2152
  //#endregion
2252
2153
  //#region src/tracing/roots/navigationTiming.ts
2253
- /** Split out so the timestamp maths is testable without a Navigation Timing entry. */
2254
2154
  function computePageloadStartNano(timeOriginMs, startTimeMs) {
2255
2155
  return Math.round((timeOriginMs + (startTimeMs ?? 0)) * 1e6);
2256
2156
  }
2257
- /**
2258
- * Choose the pageload root's start time: navigation start while that window is still open,
2259
- * otherwise `now`. Starting at `now` (when tracing began after the final cap, or the pageload was
2260
- * already traced) avoids a backdated root reporting a bogus multi-second duration.
2261
- */
2262
2157
  function resolvePageloadStartNano(backdatedNano, nowNano, finalTimeoutNano, alreadyTraced) {
2263
2158
  if (alreadyTraced) return nowNano;
2264
2159
  if (nowNano - backdatedNano > finalTimeoutNano) return nowNano;
2265
2160
  return backdatedNano;
2266
2161
  }
2267
- /** The Navigation Timing API, or null where it is missing or only partly implemented. */
2268
2162
  function navigationTiming() {
2269
2163
  const perf = globalThis.performance;
2270
2164
  if (!perf || typeof perf.getEntriesByType !== "function" || typeof perf.timeOrigin !== "number") return null;
@@ -2273,10 +2167,6 @@ function navigationTiming() {
2273
2167
  function navigationEntry(perf) {
2274
2168
  return perf.getEntriesByType("navigation")[0];
2275
2169
  }
2276
- /**
2277
- * The pageload root's start time in unix nanoseconds, backdated to navigation start via the
2278
- * Navigation Timing entry. Falls back to the tracer's clock when the API is unavailable.
2279
- */
2280
2170
  function pageloadStartNano() {
2281
2171
  const perf = navigationTiming();
2282
2172
  if (!perf) return (0, _flareapp_core.defaultNowNano)();
@@ -2287,13 +2177,6 @@ function computePageloadEndNano(timeOriginMs, loadEventEndMs, domContentLoadedEv
2287
2177
  if (!endMs) return nowNano;
2288
2178
  return Math.round((timeOriginMs + endMs) * 1e6);
2289
2179
  }
2290
- /**
2291
- * The pageload root's end time in unix nanoseconds, taken from the Navigation
2292
- * Timing `loadEventEnd` (the browser's own "page finished loading" mark), falling
2293
- * back to `domContentLoadedEventEnd`, then the tracer's clock when neither has
2294
- * fired yet or the API is unavailable. Used as the pageload root's close floor so a
2295
- * childless pageload reports its real load duration rather than idle-timeout padding.
2296
- */
2297
2180
  function pageloadEndNano() {
2298
2181
  const perf = navigationTiming();
2299
2182
  if (!perf) return (0, _flareapp_core.defaultNowNano)();
@@ -2322,8 +2205,6 @@ function resolveTimeouts(config) {
2322
2205
  childSpanTimeout: config.childSpanTimeout ?? DEFAULT_IDLE_TIMEOUTS.childSpanTimeout
2323
2206
  };
2324
2207
  }
2325
- /** No-ops once the controller has ended: it can close itself asynchronously via a timer, before this
2326
- * module's `controller` reference is cleared. */
2327
2208
  function withLiveController(fn) {
2328
2209
  if (!controller || controller.isEnded) return;
2329
2210
  try {
@@ -2332,13 +2213,13 @@ function withLiveController(fn) {
2332
2213
  if (activeFlare?.config.debug) console.error("Flare: browser tracing controller callback failed", error);
2333
2214
  }
2334
2215
  }
2335
- /** Run `fn` only while the current root is still open. Swallows a throw, like `withLiveController`. */
2336
2216
  function ifRootLive(fn) {
2337
2217
  withLiveController(() => fn());
2338
2218
  }
2339
2219
  function startRoot(flare, options) {
2340
2220
  const { spanType, startTimeUnixNano, name = location.pathname, urlOverride, hold, backdated } = options;
2341
2221
  let root;
2222
+ resetComponentSelfTime();
2342
2223
  try {
2343
2224
  const context = collectBrowserSpanContext(flare.config, urlOverride);
2344
2225
  root = flare.startSpan(name, {
@@ -2400,14 +2281,6 @@ function openNavigationRoot(flare, opts) {
2400
2281
  hold: opts.hold
2401
2282
  });
2402
2283
  }
2403
- /**
2404
- * Writes the already-final vitals onto the pageload root itself, from `IdleRootController`'s beforeEnd
2405
- * hook. Whatever has not reported yet stays in `collected` and rides the later `browser_web_vital`
2406
- * span instead, so no vital is ever sent twice and none is lost.
2407
- *
2408
- * Swallows its own failures: this runs inside the root's close path, and a throw here would leave the
2409
- * root open forever.
2410
- */
2411
2284
  function stampEarlyVitals(root, flare) {
2412
2285
  try {
2413
2286
  const early = takeEarlyVitals();
@@ -2417,19 +2290,6 @@ function stampEarlyVitals(root, flare) {
2417
2290
  if (flare.config.debug) console.error("Flare: failed to stamp web vitals on the pageload root", error);
2418
2291
  }
2419
2292
  }
2420
- /**
2421
- * Emits whatever the pageload root could not carry as one zero-duration `browser_web_vital` span,
2422
- * parented to that root. Page hide only, and once per document.
2423
- *
2424
- * Deliberately NOT on navigation: LCP, CLS and INP keep moving all document long, so emitting at the
2425
- * first route change froze them a second after load, and a session whose first action was a nav click
2426
- * reported no INP at all. One span per document is a backend constraint, so the emit waits for the last
2427
- * moment we get instead. The cost is a page whose hide event never fires reports no vitals.
2428
- *
2429
- * `pageloadRoot` has usually ended by now; reading `traceId` and `spanId` off an ended span is fine,
2430
- * and passing the `Span` rather than a `{ traceId, spanId }` pair is what makes sampling inherit:
2431
- * `resolveTrace()` reads `parent.isRecording` instead of re-rolling the sampler.
2432
- */
2433
2293
  function emitWebVitals(flare) {
2434
2294
  const root = pageloadRoot;
2435
2295
  const route = pageloadRoute;
@@ -2457,7 +2317,6 @@ function emitWebVitals(flare) {
2457
2317
  if (flare.config.debug) console.error("Flare: failed to emit web vitals", error);
2458
2318
  }
2459
2319
  }
2460
- /** Opens a backdated pageload root, then a navigation root per History change. No-op outside a browser. Idempotent. */
2461
2320
  function startBrowserTracing(flare) {
2462
2321
  if (typeof window === "undefined" || typeof history === "undefined" || typeof location === "undefined") return;
2463
2322
  if (uninstall) return;
@@ -2517,7 +2376,6 @@ function startBrowserTracing(flare) {
2517
2376
  document.removeEventListener("visibilitychange", onVisibilityChange);
2518
2377
  };
2519
2378
  }
2520
- /** Idempotent. */
2521
2379
  function stopBrowserTracing() {
2522
2380
  withLiveController((live) => live.endNow());
2523
2381
  controller = null;
@@ -2532,15 +2390,12 @@ function stopBrowserTracing() {
2532
2390
  pendingRouteName = null;
2533
2391
  pendingRouteNameOwner = null;
2534
2392
  stopWebVitals();
2393
+ resetComponentSelfTime();
2535
2394
  pageloadRoot = null;
2536
2395
  pageloadRootStartNano = 0;
2537
2396
  pageloadRoute = null;
2538
2397
  pageloadContext = {};
2539
2398
  }
2540
- /**
2541
- * Computes the url attributes a route rename carries, or null when there is nothing to add. Guarded
2542
- * so the pin below (which runs outside ifRootLive's own try/catch) cannot throw into the host.
2543
- */
2544
2399
  function urlAttributesFor(route) {
2545
2400
  if (route.url === void 0 || !activeFlare) return null;
2546
2401
  try {
@@ -2549,12 +2404,6 @@ function urlAttributesFor(route) {
2549
2404
  return null;
2550
2405
  }
2551
2406
  }
2552
- /**
2553
- * Rename the current root and update the attributes that go with the name, and pin the pageload's route
2554
- * for the vitals emit. No-op once it closed; the pin is NOT gated the same way, see below.
2555
- * With no root yet the name is held for the pageload root that opens next, rather than dropped.
2556
- * `owner` stamps who is holding it, so a stale or superseded source cannot land its name later.
2557
- */
2558
2407
  function applyRouteName(route, owner) {
2559
2408
  const root = currentRoot;
2560
2409
  if (!root) {
@@ -2590,8 +2439,8 @@ function activeTracingFlare() {
2590
2439
  /** Unix nanos on the same clock the tracer uses for span timestamps. */
2591
2440
  const nowNano = _flareapp_core.defaultNowNano;
2592
2441
  /**
2593
- * Reserved up front so descendants can point at a span before it is recorded. Null when the trace is at
2594
- * its span cap: descendants record before this span does, so an id the cap will refuse orphans them.
2442
+ * Reserved up front so descendants can point at a span before it is recorded. Null when the trace
2443
+ * is at its span cap, since descendants record before this span does and would be orphaned.
2595
2444
  */
2596
2445
  function reserveSpanId(traceId) {
2597
2446
  if (traceId !== void 0 && !activeTracingFlare()?.tracer.claimSpanSlot(traceId)) return null;
@@ -2611,18 +2460,18 @@ function activeComponentRoot() {
2611
2460
  }
2612
2461
  }
2613
2462
  /**
2614
- * An ancestor's context is only usable while it still belongs to the live trace. A profiled component
2615
- * that survives a navigation (a layout around a swapped page body) froze its context under the pageload
2616
- * trace, and `recordComponentSpan` would drop anything pointing at that closed root.
2463
+ * An ancestor's context is only usable while it still belongs to the live trace. A component that
2464
+ * survives a navigation (a layout around a swapped page body) froze its context under the old
2465
+ * pageload trace, and `recordComponentSpan` would drop anything pointing at that closed root.
2617
2466
  */
2618
2467
  function resolveComponentParent(inherited, live) {
2619
2468
  if (inherited && live && inherited.traceId === live.traceId) return inherited;
2620
2469
  return live;
2621
2470
  }
2622
2471
  /**
2623
- * Records only while the reserved root is still the live recording root, and drops the span otherwise.
2624
- * Dropping avoids starting a fresh TraceState for a dead trace, which would re-run the sampler, and
2625
- * avoids adding a child to a root that already shipped.
2472
+ * Records only while the reserved root is still the live recording root, and drops the span
2473
+ * otherwise. Dropping avoids re-running the sampler for a dead trace and avoids adding a child to
2474
+ * a root that already shipped.
2626
2475
  */
2627
2476
  function recordComponentSpan(record) {
2628
2477
  try {
@@ -2630,6 +2479,7 @@ function recordComponentSpan(record) {
2630
2479
  if (!flare) return;
2631
2480
  const root = flare.tracer.getActiveSpan();
2632
2481
  if (!root || root.traceId !== record.parent.traceId || !root.isRecording) return;
2482
+ const selfTime = takeSelfTime(record.spanId, record.startTimeUnixNano, record.endTimeUnixNano);
2633
2483
  flare.startSpan(record.name, {
2634
2484
  spanId: record.spanId,
2635
2485
  parent: {
@@ -2640,19 +2490,17 @@ function recordComponentSpan(record) {
2640
2490
  startTimeUnixNano: record.startTimeUnixNano,
2641
2491
  attributes: {
2642
2492
  ...record.attributes,
2643
- "flare.component.name": record.name
2493
+ "flare.component.name": record.name,
2494
+ "flare.component.self_time_ns": selfTime
2644
2495
  },
2645
2496
  claimed: true
2646
2497
  }).end(record.endTimeUnixNano);
2498
+ trackChildInterval(record.parent.parentSpanId, record.startTimeUnixNano, record.endTimeUnixNano);
2647
2499
  } catch {}
2648
2500
  }
2649
2501
 
2650
2502
  //#endregion
2651
2503
  //#region src/createFlareResolver.ts
2652
- /**
2653
- * `process.env.NODE_ENV` is replaced inline by bundlers. The try/catch keeps a process-less
2654
- * environment safe: treat "undetermined" as production (warn, never crash).
2655
- */
2656
2504
  function isDevMode() {
2657
2505
  try {
2658
2506
  return process.env.NODE_ENV !== "production";
@@ -2661,11 +2509,10 @@ function isDevMode() {
2661
2509
  }
2662
2510
  }
2663
2511
  /**
2664
- * Builds a per-package Flare resolver: `registerDefaultFlare` (wired once by the web entry) and
2665
- * `resolveFlare` (called at wiring time). Each call holds its own default-provider state. The check
2666
- * that warns about the Electron `__flare` bridge uses `packageName` in its message;
2667
- * `injectInstruction` replaces the closing hint for packages whose advice differs (for example
2668
- * svelte, which points at the preprocessor's importSource).
2512
+ * Builds a per-package Flare resolver: `registerDefaultFlare` (set once by the web entry) and
2513
+ * `resolveFlare` (called at wiring time). Each call keeps its own default-provider state.
2514
+ * `packageName` names the package in the Electron-bridge warning; `injectInstruction` overrides the
2515
+ * closing hint for packages with different advice (for example Svelte's preprocessor importSource).
2669
2516
  */
2670
2517
  function createFlareResolver(config) {
2671
2518
  const { packageName } = config;