@flareapp/js 2.10.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.
- package/dist/browser.cjs +1 -2
- package/dist/browser.d.cts +1 -2
- package/dist/browser.d.mts +1 -2
- package/dist/browser.mjs +1 -2
- package/dist/{catchWindowErrors-zf246a2G.cjs → catchWindowErrors-BWKVBTkt.cjs} +178 -258
- package/dist/{catchWindowErrors-CkFI5Vwv.mjs → catchWindowErrors-ChRN4Lq1.mjs} +179 -259
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{spanTypes-TMgJEZ7G.d.cts → spanTypes-Cdgtuu6-.d.cts} +35 -32
- package/dist/{spanTypes-7-nEycOc.d.mts → spanTypes-RYeFN_pN.d.mts} +35 -32
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BrowserSpanEventType, BrowserSpanType, BrowserSpanType as BrowserSpanType$1, SpanStatusCode, breadcrumbUrl, buildTraceparent, defaultNowNano, redactUrlQuery, routeRejection, spanId, urlAttributes } from "@flareapp/core";
|
|
1
|
+
import { BrowserSpanEventType, BrowserSpanType, BrowserSpanType as BrowserSpanType$1, SpanStatusCode, breadcrumbUrl, buildTraceparent, defaultNowNano, deviceInfoToAttributes, evictLruIfNew, redactUrlQuery, routeRejection, spanId, urlAttributes } from "@flareapp/core";
|
|
2
2
|
|
|
3
3
|
//#region src/breadcrumbs/utils/documentEvent.ts
|
|
4
4
|
function onDocumentEvent(name, handle) {
|
|
@@ -73,10 +73,11 @@ var FormChangeRecorder = class {
|
|
|
73
73
|
//#endregion
|
|
74
74
|
//#region src/tracing/utils/absoluteHref.ts
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
77
|
-
* wants the pathname
|
|
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
|
-
*
|
|
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
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
123
|
-
* the app's base path and hash prefix
|
|
124
|
-
*
|
|
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: 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: 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 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);
|
|
@@ -995,13 +909,86 @@ function cookie(denylist) {
|
|
|
995
909
|
return { "http.request.cookies": cookies };
|
|
996
910
|
}
|
|
997
911
|
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/browser/context/deviceReaders.ts
|
|
914
|
+
const EFFECTIVE_TYPES = [
|
|
915
|
+
"slow-2g",
|
|
916
|
+
"2g",
|
|
917
|
+
"3g",
|
|
918
|
+
"4g"
|
|
919
|
+
];
|
|
920
|
+
function readScreen() {
|
|
921
|
+
if (typeof screen === "undefined" || typeof screen.width !== "number" || typeof screen.height !== "number") return null;
|
|
922
|
+
const scale = typeof devicePixelRatio === "number" ? devicePixelRatio : void 0;
|
|
923
|
+
return scale != null ? {
|
|
924
|
+
width: screen.width,
|
|
925
|
+
height: screen.height,
|
|
926
|
+
scale
|
|
927
|
+
} : {
|
|
928
|
+
width: screen.width,
|
|
929
|
+
height: screen.height
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function readNetwork(nav) {
|
|
933
|
+
const network = {};
|
|
934
|
+
if (typeof nav.onLine === "boolean") network.online = nav.onLine;
|
|
935
|
+
const connection = nav.connection;
|
|
936
|
+
if (connection) {
|
|
937
|
+
const { effectiveType, downlink, rtt } = connection;
|
|
938
|
+
if (effectiveType && EFFECTIVE_TYPES.includes(effectiveType)) network.effectiveType = effectiveType;
|
|
939
|
+
if (typeof downlink === "number") network.downlinkMbps = downlink;
|
|
940
|
+
if (typeof rtt === "number") network.rttMs = rtt;
|
|
941
|
+
}
|
|
942
|
+
return Object.keys(network).length > 0 ? network : null;
|
|
943
|
+
}
|
|
944
|
+
function readTimezone() {
|
|
945
|
+
try {
|
|
946
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
|
|
947
|
+
} catch {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/browser/context/deviceInfo.ts
|
|
954
|
+
var BrowserDeviceInfoProvider = class {
|
|
955
|
+
staticInfo = null;
|
|
956
|
+
collect() {
|
|
957
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return {};
|
|
958
|
+
const nav = navigator;
|
|
959
|
+
const info = {};
|
|
960
|
+
const staticInfo = this.readStatic(nav);
|
|
961
|
+
const screen = readScreen();
|
|
962
|
+
const device = {
|
|
963
|
+
...staticInfo.device,
|
|
964
|
+
...screen ? { screen } : {}
|
|
965
|
+
};
|
|
966
|
+
if (Object.keys(device).length > 0) info.device = device;
|
|
967
|
+
if (staticInfo.locale) info.locale = staticInfo.locale;
|
|
968
|
+
const network = readNetwork(nav);
|
|
969
|
+
if (network) info.network = network;
|
|
970
|
+
return info;
|
|
971
|
+
}
|
|
972
|
+
readStatic(nav) {
|
|
973
|
+
if (this.staticInfo) return this.staticInfo;
|
|
974
|
+
const device = {};
|
|
975
|
+
if (typeof nav.deviceMemory === "number") device.memoryGb = nav.deviceMemory;
|
|
976
|
+
if (typeof nav.hardwareConcurrency === "number") device.cpuCores = nav.hardwareConcurrency;
|
|
977
|
+
const locale = {};
|
|
978
|
+
if (nav.language) locale.language = nav.language;
|
|
979
|
+
const timezone = readTimezone();
|
|
980
|
+
if (timezone) locale.timezone = timezone;
|
|
981
|
+
this.staticInfo = {
|
|
982
|
+
device: Object.keys(device).length > 0 ? device : void 0,
|
|
983
|
+
locale: Object.keys(locale).length > 0 ? locale : void 0
|
|
984
|
+
};
|
|
985
|
+
return this.staticInfo;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
const browserDeviceInfoProvider = new BrowserDeviceInfoProvider();
|
|
989
|
+
|
|
998
990
|
//#endregion
|
|
999
991
|
//#region src/browser/context/request.ts
|
|
1000
|
-
/**
|
|
1001
|
-
* @param hrefOverride when set, the `url.*` attributes come from it instead of the live
|
|
1002
|
-
* `window.location.href` (a framework navigation root whose router knows the destination
|
|
1003
|
-
* before the URL commits). The override is pre-validated by the caller.
|
|
1004
|
-
*/
|
|
1005
992
|
function request(urlDenylist, hrefOverride) {
|
|
1006
993
|
return {
|
|
1007
994
|
...urlAttributes(hrefOverride ?? window.location.href, urlDenylist),
|
|
@@ -1030,6 +1017,7 @@ function browserEntryPoint(config, urlOverride) {
|
|
|
1030
1017
|
}
|
|
1031
1018
|
const collectBrowser = (config) => {
|
|
1032
1019
|
const attrs = { ...browserEntryPoint(config) };
|
|
1020
|
+
Object.assign(attrs, deviceInfoToAttributes(browserDeviceInfoProvider.collect()));
|
|
1033
1021
|
if (typeof window === "undefined") return attrs;
|
|
1034
1022
|
if (window?.location?.hostname) attrs["host.name"] = window.location.hostname;
|
|
1035
1023
|
Object.assign(attrs, request(config.urlDenylist));
|
|
@@ -1056,7 +1044,7 @@ var FetchFileReader = class {
|
|
|
1056
1044
|
|
|
1057
1045
|
//#endregion
|
|
1058
1046
|
//#region src/env/index.ts
|
|
1059
|
-
const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.
|
|
1047
|
+
const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.12.0" : "?";
|
|
1060
1048
|
|
|
1061
1049
|
//#endregion
|
|
1062
1050
|
//#region src/tracing/utils/instrumentationGuard.ts
|
|
@@ -1076,16 +1064,16 @@ function safeInvoke(fn) {
|
|
|
1076
1064
|
}
|
|
1077
1065
|
const instrumented = /* @__PURE__ */ new WeakMap();
|
|
1078
1066
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
* first. Vite HMR re-runs boot code against a router that survives the reload, so without this
|
|
1081
|
-
* cycle
|
|
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
|
|
1082
1070
|
* router is unaffected.
|
|
1083
1071
|
*
|
|
1084
|
-
* `install` hands each teardown to `track` as it produces it. A router's
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
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.
|
|
1087
1075
|
*
|
|
1088
|
-
* @returns
|
|
1076
|
+
* @returns The cleanup function, or a no-op if install failed and already unwound itself.
|
|
1089
1077
|
*/
|
|
1090
1078
|
function instrumentOnce(target, install) {
|
|
1091
1079
|
instrumented.get(target)?.();
|
|
@@ -1111,15 +1099,6 @@ function instrumentOnce(target, install) {
|
|
|
1111
1099
|
|
|
1112
1100
|
//#endregion
|
|
1113
1101
|
//#region src/browser/context/collectBrowserSpanContext.ts
|
|
1114
|
-
/**
|
|
1115
|
-
* Entry point plus request identity for a pageload/navigation root. Deliberately leaner than the report
|
|
1116
|
-
* context: no cookies, no structured query params, no host.name (that is resource-level). Captured at
|
|
1117
|
-
* span start, so a long-lived root reflects the page it represents rather than the page at close.
|
|
1118
|
-
*
|
|
1119
|
-
* @param hrefOverride destination href for a router that reports where it is going before the URL
|
|
1120
|
-
* commits. Only the URL-derived keys come from it; the rest always reflect the live document. An
|
|
1121
|
-
* unparseable override falls back to the live location instead of throwing into root creation.
|
|
1122
|
-
*/
|
|
1123
1102
|
function collectBrowserSpanContext(config, hrefOverride) {
|
|
1124
1103
|
if (typeof window === "undefined") return {};
|
|
1125
1104
|
const url = absoluteUrl(hrefOverride);
|
|
@@ -1128,16 +1107,6 @@ function collectBrowserSpanContext(config, hrefOverride) {
|
|
|
1128
1107
|
...request(config.urlDenylist, url?.href)
|
|
1129
1108
|
};
|
|
1130
1109
|
}
|
|
1131
|
-
/**
|
|
1132
|
-
* Updates a root's url after a redirect, or when a newer navigation replaces this one. The root opened
|
|
1133
|
-
* with the first destination, so without this it reports a page the user never reached.
|
|
1134
|
-
*
|
|
1135
|
-
* Does not touch `flare.entry_point.handler.identifier` or `http.route`. Those hold the route template,
|
|
1136
|
-
* and reading them back from the href would turn `/product/[id]` into `/product/p01`.
|
|
1137
|
-
*
|
|
1138
|
-
* Always sets `url.query`, even to an empty string. You can overwrite a span attribute but not remove
|
|
1139
|
-
* it, so going from `/a?x=1` to `/b` would otherwise keep the old query.
|
|
1140
|
-
*/
|
|
1141
1110
|
function browserSpanUrlAttributes(config, href) {
|
|
1142
1111
|
if (typeof window === "undefined") return {};
|
|
1143
1112
|
const resolved = absoluteUrl(href);
|
|
@@ -1935,28 +1904,12 @@ const onTTFB = (onReport, opts = {}) => {
|
|
|
1935
1904
|
|
|
1936
1905
|
//#endregion
|
|
1937
1906
|
//#region src/tracing/vitals/webVitals.ts
|
|
1938
|
-
/**
|
|
1939
|
-
* Final the moment they first report, so they can ride the pageload span itself. The other three keep
|
|
1940
|
-
* changing until the page goes away, and stamping an early value on the root would leave the root and
|
|
1941
|
-
* the later span disagreeing about the same vital.
|
|
1942
|
-
*/
|
|
1943
1907
|
const EARLY_VITALS = ["ttfb", "fcp"];
|
|
1944
|
-
/** The one place a vital becomes a wire key. Both the pageload stamp and the late span go through it. */
|
|
1945
1908
|
function vitalAttributes(vitals) {
|
|
1946
1909
|
const attributes = {};
|
|
1947
1910
|
for (const [name, value] of Object.entries(vitals)) if (typeof value === "number") attributes[`browser.web_vital.${name}`] = value;
|
|
1948
1911
|
return attributes;
|
|
1949
1912
|
}
|
|
1950
|
-
/**
|
|
1951
|
-
* Turns the leftover values into one zero-duration span. Pure on purpose: the shape is what the backend
|
|
1952
|
-
* groups on, and this way it is testable without a tracer or a clock.
|
|
1953
|
-
*
|
|
1954
|
-
* Both timestamps sit at the pageload root's start. `spans_2` buckets on `start_time_unix_nano`, so
|
|
1955
|
-
* stamping the report moment would drop a tab left open for forty minutes into a minute forty minutes
|
|
1956
|
-
* after the page actually loaded.
|
|
1957
|
-
*
|
|
1958
|
-
* Returns null when nothing is left to report, so the caller emits no span at all.
|
|
1959
|
-
*/
|
|
1960
1913
|
function buildVitalsSpan(input) {
|
|
1961
1914
|
const vitals = vitalAttributes(input.vitals);
|
|
1962
1915
|
if (Object.keys(vitals).length === 0) return null;
|
|
@@ -1986,10 +1939,6 @@ function defaultSubscribers() {
|
|
|
1986
1939
|
onINP: (cb) => onINP(cb, { reportAllChanges: true })
|
|
1987
1940
|
};
|
|
1988
1941
|
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Subscribes at most once per document: upstream's on* functions return no unsubscribe handle, so a
|
|
1991
|
-
* second call would attach a second set of observers with no way to detach either.
|
|
1992
|
-
*/
|
|
1993
1942
|
function startWebVitals(subscribers = defaultSubscribers()) {
|
|
1994
1943
|
recording = true;
|
|
1995
1944
|
if (subscribed) return;
|
|
@@ -2009,21 +1958,10 @@ function record(name, metric) {
|
|
|
2009
1958
|
if (!recording || typeof metric?.value !== "number") return;
|
|
2010
1959
|
collected[name] = metric.value;
|
|
2011
1960
|
}
|
|
2012
|
-
/**
|
|
2013
|
-
* Stops recording and drops what was collected. The observers themselves cannot be detached, and both
|
|
2014
|
-
* the `subscribed` and `taken` latches deliberately survive: clearing `subscribed` would let a re-enable
|
|
2015
|
-
* attach a second set of observers that is just as undetachable as the first, and clearing `taken` would
|
|
2016
|
-
* let those surviving observers refill `collected` and ship a second `browser_web_vital` for the same
|
|
2017
|
-
* document once the page is re-enabled and later hidden.
|
|
2018
|
-
*/
|
|
2019
1961
|
function stopWebVitals() {
|
|
2020
1962
|
recording = false;
|
|
2021
1963
|
collected = {};
|
|
2022
1964
|
}
|
|
2023
|
-
/**
|
|
2024
|
-
* The vitals that are already final when the pageload root closes, removed from `collected` so the late
|
|
2025
|
-
* span cannot report them a second time. Naturally idempotent: a second call finds the keys gone.
|
|
2026
|
-
*/
|
|
2027
1965
|
function takeEarlyVitals() {
|
|
2028
1966
|
if (!recording) return null;
|
|
2029
1967
|
const taking = {};
|
|
@@ -2036,7 +1974,6 @@ function takeEarlyVitals() {
|
|
|
2036
1974
|
}
|
|
2037
1975
|
return Object.keys(taking).length === 0 ? null : taking;
|
|
2038
1976
|
}
|
|
2039
|
-
/** Everything still outstanding, once. Null afterwards, and null when nothing is left. */
|
|
2040
1977
|
function takeWebVitals() {
|
|
2041
1978
|
if (taken || !recording || Object.keys(collected).length === 0) return null;
|
|
2042
1979
|
taken = true;
|
|
@@ -2044,29 +1981,76 @@ function takeWebVitals() {
|
|
|
2044
1981
|
collected = {};
|
|
2045
1982
|
return taking;
|
|
2046
1983
|
}
|
|
2047
|
-
/**
|
|
2048
|
-
* Undoes a take after the emit failed partway through, so the values are not lost and a later trigger
|
|
2049
|
-
* can retry. Safe to assign outright rather than merge: the whole emit runs synchronously between the
|
|
2050
|
-
* take and a catch block calling this, so nothing else can have written to `collected` in between.
|
|
2051
|
-
*/
|
|
2052
1984
|
function restoreWebVitals(vitals) {
|
|
2053
1985
|
collected = vitals;
|
|
2054
1986
|
taken = false;
|
|
2055
1987
|
}
|
|
2056
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
|
+
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
|
+
|
|
2057
2047
|
//#endregion
|
|
2058
2048
|
//#region src/tracing/roots/IdleRootController.ts
|
|
2059
|
-
/** Browser defaults for the three idle-root timeouts, in ms. Overridable per Config. */
|
|
2060
2049
|
const DEFAULT_IDLE_TIMEOUTS = {
|
|
2061
2050
|
idleTimeout: 1e3,
|
|
2062
2051
|
finalTimeout: 3e4,
|
|
2063
2052
|
childSpanTimeout: 15e3
|
|
2064
2053
|
};
|
|
2065
|
-
/**
|
|
2066
|
-
* Owns one root span's idle lifecycle: open while child spans in its trace are active, closing after
|
|
2067
|
-
* `idleTimeout` with no open children, or on the `finalTimeout` / `childSpanTimeout` backstops. Deps are
|
|
2068
|
-
* injected so this is testable without real timers or a real tracer.
|
|
2069
|
-
*/
|
|
2070
2054
|
var IdleRootController = class {
|
|
2071
2055
|
openChildren = 0;
|
|
2072
2056
|
lastChildEndTime = null;
|
|
@@ -2091,17 +2075,9 @@ var IdleRootController = class {
|
|
|
2091
2075
|
get isEnded() {
|
|
2092
2076
|
return this.ended;
|
|
2093
2077
|
}
|
|
2094
|
-
/** For a route change or pagehide. */
|
|
2095
2078
|
endNow() {
|
|
2096
2079
|
this.finish(this.openChildren > 0 || this.held ? this.deps.now() : this.trimmedEnd());
|
|
2097
2080
|
}
|
|
2098
|
-
/**
|
|
2099
|
-
* Records the settle moment as a close floor and hands the root back to the normal idle lifecycle. It
|
|
2100
|
-
* deliberately does not close here: a router settles before the framework mounts the new route
|
|
2101
|
-
* component (vue-router runs `afterEach` in the route-update tick, Vue mounts on the next flush), so
|
|
2102
|
-
* closing at settle cleared the active root ahead of every post-navigation mount: every component span
|
|
2103
|
-
* read a null root, and a trailing fetch opened a root of its own.
|
|
2104
|
-
*/
|
|
2105
2081
|
releaseHold() {
|
|
2106
2082
|
if (this.ended || !this.held) return;
|
|
2107
2083
|
this.held = false;
|
|
@@ -2138,8 +2114,6 @@ var IdleRootController = class {
|
|
|
2138
2114
|
this.finish(this.trimmedEnd());
|
|
2139
2115
|
}, this.timeouts.idleTimeout);
|
|
2140
2116
|
}
|
|
2141
|
-
/** The latest of the floor, a released hold's settle moment, and the last child's end, so a root
|
|
2142
|
-
* covers its children without ever padding out to `now()`. */
|
|
2143
2117
|
trimmedEnd() {
|
|
2144
2118
|
return Math.max(this.deps.endFloor(), this.settleTime ?? 0, this.lastChildEndTime ?? 0);
|
|
2145
2119
|
}
|
|
@@ -2177,21 +2151,14 @@ var IdleRootController = class {
|
|
|
2177
2151
|
|
|
2178
2152
|
//#endregion
|
|
2179
2153
|
//#region src/tracing/roots/navigationTiming.ts
|
|
2180
|
-
/** Split out so the timestamp maths is testable without a Navigation Timing entry. */
|
|
2181
2154
|
function computePageloadStartNano(timeOriginMs, startTimeMs) {
|
|
2182
2155
|
return Math.round((timeOriginMs + (startTimeMs ?? 0)) * 1e6);
|
|
2183
2156
|
}
|
|
2184
|
-
/**
|
|
2185
|
-
* Choose the pageload root's start time: navigation start while that window is still open,
|
|
2186
|
-
* otherwise `now`. Starting at `now` (when tracing began after the final cap, or the pageload was
|
|
2187
|
-
* already traced) avoids a backdated root reporting a bogus multi-second duration.
|
|
2188
|
-
*/
|
|
2189
2157
|
function resolvePageloadStartNano(backdatedNano, nowNano, finalTimeoutNano, alreadyTraced) {
|
|
2190
2158
|
if (alreadyTraced) return nowNano;
|
|
2191
2159
|
if (nowNano - backdatedNano > finalTimeoutNano) return nowNano;
|
|
2192
2160
|
return backdatedNano;
|
|
2193
2161
|
}
|
|
2194
|
-
/** The Navigation Timing API, or null where it is missing or only partly implemented. */
|
|
2195
2162
|
function navigationTiming() {
|
|
2196
2163
|
const perf = globalThis.performance;
|
|
2197
2164
|
if (!perf || typeof perf.getEntriesByType !== "function" || typeof perf.timeOrigin !== "number") return null;
|
|
@@ -2200,10 +2167,6 @@ function navigationTiming() {
|
|
|
2200
2167
|
function navigationEntry(perf) {
|
|
2201
2168
|
return perf.getEntriesByType("navigation")[0];
|
|
2202
2169
|
}
|
|
2203
|
-
/**
|
|
2204
|
-
* The pageload root's start time in unix nanoseconds, backdated to navigation start via the
|
|
2205
|
-
* Navigation Timing entry. Falls back to the tracer's clock when the API is unavailable.
|
|
2206
|
-
*/
|
|
2207
2170
|
function pageloadStartNano() {
|
|
2208
2171
|
const perf = navigationTiming();
|
|
2209
2172
|
if (!perf) return defaultNowNano();
|
|
@@ -2214,13 +2177,6 @@ function computePageloadEndNano(timeOriginMs, loadEventEndMs, domContentLoadedEv
|
|
|
2214
2177
|
if (!endMs) return nowNano;
|
|
2215
2178
|
return Math.round((timeOriginMs + endMs) * 1e6);
|
|
2216
2179
|
}
|
|
2217
|
-
/**
|
|
2218
|
-
* The pageload root's end time in unix nanoseconds, taken from the Navigation
|
|
2219
|
-
* Timing `loadEventEnd` (the browser's own "page finished loading" mark), falling
|
|
2220
|
-
* back to `domContentLoadedEventEnd`, then the tracer's clock when neither has
|
|
2221
|
-
* fired yet or the API is unavailable. Used as the pageload root's close floor so a
|
|
2222
|
-
* childless pageload reports its real load duration rather than idle-timeout padding.
|
|
2223
|
-
*/
|
|
2224
2180
|
function pageloadEndNano() {
|
|
2225
2181
|
const perf = navigationTiming();
|
|
2226
2182
|
if (!perf) return defaultNowNano();
|
|
@@ -2249,8 +2205,6 @@ function resolveTimeouts(config) {
|
|
|
2249
2205
|
childSpanTimeout: config.childSpanTimeout ?? DEFAULT_IDLE_TIMEOUTS.childSpanTimeout
|
|
2250
2206
|
};
|
|
2251
2207
|
}
|
|
2252
|
-
/** No-ops once the controller has ended: it can close itself asynchronously via a timer, before this
|
|
2253
|
-
* module's `controller` reference is cleared. */
|
|
2254
2208
|
function withLiveController(fn) {
|
|
2255
2209
|
if (!controller || controller.isEnded) return;
|
|
2256
2210
|
try {
|
|
@@ -2259,13 +2213,13 @@ function withLiveController(fn) {
|
|
|
2259
2213
|
if (activeFlare?.config.debug) console.error("Flare: browser tracing controller callback failed", error);
|
|
2260
2214
|
}
|
|
2261
2215
|
}
|
|
2262
|
-
/** Run `fn` only while the current root is still open. Swallows a throw, like `withLiveController`. */
|
|
2263
2216
|
function ifRootLive(fn) {
|
|
2264
2217
|
withLiveController(() => fn());
|
|
2265
2218
|
}
|
|
2266
2219
|
function startRoot(flare, options) {
|
|
2267
2220
|
const { spanType, startTimeUnixNano, name = location.pathname, urlOverride, hold, backdated } = options;
|
|
2268
2221
|
let root;
|
|
2222
|
+
resetComponentSelfTime();
|
|
2269
2223
|
try {
|
|
2270
2224
|
const context = collectBrowserSpanContext(flare.config, urlOverride);
|
|
2271
2225
|
root = flare.startSpan(name, {
|
|
@@ -2327,14 +2281,6 @@ function openNavigationRoot(flare, opts) {
|
|
|
2327
2281
|
hold: opts.hold
|
|
2328
2282
|
});
|
|
2329
2283
|
}
|
|
2330
|
-
/**
|
|
2331
|
-
* Writes the already-final vitals onto the pageload root itself, from `IdleRootController`'s beforeEnd
|
|
2332
|
-
* hook. Whatever has not reported yet stays in `collected` and rides the later `browser_web_vital`
|
|
2333
|
-
* span instead, so no vital is ever sent twice and none is lost.
|
|
2334
|
-
*
|
|
2335
|
-
* Swallows its own failures: this runs inside the root's close path, and a throw here would leave the
|
|
2336
|
-
* root open forever.
|
|
2337
|
-
*/
|
|
2338
2284
|
function stampEarlyVitals(root, flare) {
|
|
2339
2285
|
try {
|
|
2340
2286
|
const early = takeEarlyVitals();
|
|
@@ -2344,19 +2290,6 @@ function stampEarlyVitals(root, flare) {
|
|
|
2344
2290
|
if (flare.config.debug) console.error("Flare: failed to stamp web vitals on the pageload root", error);
|
|
2345
2291
|
}
|
|
2346
2292
|
}
|
|
2347
|
-
/**
|
|
2348
|
-
* Emits whatever the pageload root could not carry as one zero-duration `browser_web_vital` span,
|
|
2349
|
-
* parented to that root. Page hide only, and once per document.
|
|
2350
|
-
*
|
|
2351
|
-
* Deliberately NOT on navigation: LCP, CLS and INP keep moving all document long, so emitting at the
|
|
2352
|
-
* first route change froze them a second after load, and a session whose first action was a nav click
|
|
2353
|
-
* reported no INP at all. One span per document is a backend constraint, so the emit waits for the last
|
|
2354
|
-
* moment we get instead. The cost is a page whose hide event never fires reports no vitals.
|
|
2355
|
-
*
|
|
2356
|
-
* `pageloadRoot` has usually ended by now; reading `traceId` and `spanId` off an ended span is fine,
|
|
2357
|
-
* and passing the `Span` rather than a `{ traceId, spanId }` pair is what makes sampling inherit:
|
|
2358
|
-
* `resolveTrace()` reads `parent.isRecording` instead of re-rolling the sampler.
|
|
2359
|
-
*/
|
|
2360
2293
|
function emitWebVitals(flare) {
|
|
2361
2294
|
const root = pageloadRoot;
|
|
2362
2295
|
const route = pageloadRoute;
|
|
@@ -2384,7 +2317,6 @@ function emitWebVitals(flare) {
|
|
|
2384
2317
|
if (flare.config.debug) console.error("Flare: failed to emit web vitals", error);
|
|
2385
2318
|
}
|
|
2386
2319
|
}
|
|
2387
|
-
/** Opens a backdated pageload root, then a navigation root per History change. No-op outside a browser. Idempotent. */
|
|
2388
2320
|
function startBrowserTracing(flare) {
|
|
2389
2321
|
if (typeof window === "undefined" || typeof history === "undefined" || typeof location === "undefined") return;
|
|
2390
2322
|
if (uninstall) return;
|
|
@@ -2444,7 +2376,6 @@ function startBrowserTracing(flare) {
|
|
|
2444
2376
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2445
2377
|
};
|
|
2446
2378
|
}
|
|
2447
|
-
/** Idempotent. */
|
|
2448
2379
|
function stopBrowserTracing() {
|
|
2449
2380
|
withLiveController((live) => live.endNow());
|
|
2450
2381
|
controller = null;
|
|
@@ -2459,15 +2390,12 @@ function stopBrowserTracing() {
|
|
|
2459
2390
|
pendingRouteName = null;
|
|
2460
2391
|
pendingRouteNameOwner = null;
|
|
2461
2392
|
stopWebVitals();
|
|
2393
|
+
resetComponentSelfTime();
|
|
2462
2394
|
pageloadRoot = null;
|
|
2463
2395
|
pageloadRootStartNano = 0;
|
|
2464
2396
|
pageloadRoute = null;
|
|
2465
2397
|
pageloadContext = {};
|
|
2466
2398
|
}
|
|
2467
|
-
/**
|
|
2468
|
-
* Computes the url attributes a route rename carries, or null when there is nothing to add. Guarded
|
|
2469
|
-
* so the pin below (which runs outside ifRootLive's own try/catch) cannot throw into the host.
|
|
2470
|
-
*/
|
|
2471
2399
|
function urlAttributesFor(route) {
|
|
2472
2400
|
if (route.url === void 0 || !activeFlare) return null;
|
|
2473
2401
|
try {
|
|
@@ -2476,12 +2404,6 @@ function urlAttributesFor(route) {
|
|
|
2476
2404
|
return null;
|
|
2477
2405
|
}
|
|
2478
2406
|
}
|
|
2479
|
-
/**
|
|
2480
|
-
* Rename the current root and update the attributes that go with the name, and pin the pageload's route
|
|
2481
|
-
* for the vitals emit. No-op once it closed; the pin is NOT gated the same way, see below.
|
|
2482
|
-
* With no root yet the name is held for the pageload root that opens next, rather than dropped.
|
|
2483
|
-
* `owner` stamps who is holding it, so a stale or superseded source cannot land its name later.
|
|
2484
|
-
*/
|
|
2485
2407
|
function applyRouteName(route, owner) {
|
|
2486
2408
|
const root = currentRoot;
|
|
2487
2409
|
if (!root) {
|
|
@@ -2517,8 +2439,8 @@ function activeTracingFlare() {
|
|
|
2517
2439
|
/** Unix nanos on the same clock the tracer uses for span timestamps. */
|
|
2518
2440
|
const nowNano = defaultNowNano;
|
|
2519
2441
|
/**
|
|
2520
|
-
* Reserved up front so descendants can point at a span before it is recorded. Null when the trace
|
|
2521
|
-
* its span cap
|
|
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.
|
|
2522
2444
|
*/
|
|
2523
2445
|
function reserveSpanId(traceId) {
|
|
2524
2446
|
if (traceId !== void 0 && !activeTracingFlare()?.tracer.claimSpanSlot(traceId)) return null;
|
|
@@ -2538,18 +2460,18 @@ function activeComponentRoot() {
|
|
|
2538
2460
|
}
|
|
2539
2461
|
}
|
|
2540
2462
|
/**
|
|
2541
|
-
* An ancestor's context is only usable while it still belongs to the live trace. A
|
|
2542
|
-
*
|
|
2543
|
-
* 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.
|
|
2544
2466
|
*/
|
|
2545
2467
|
function resolveComponentParent(inherited, live) {
|
|
2546
2468
|
if (inherited && live && inherited.traceId === live.traceId) return inherited;
|
|
2547
2469
|
return live;
|
|
2548
2470
|
}
|
|
2549
2471
|
/**
|
|
2550
|
-
* Records only while the reserved root is still the live recording root, and drops the span
|
|
2551
|
-
* Dropping avoids
|
|
2552
|
-
*
|
|
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.
|
|
2553
2475
|
*/
|
|
2554
2476
|
function recordComponentSpan(record) {
|
|
2555
2477
|
try {
|
|
@@ -2557,6 +2479,7 @@ function recordComponentSpan(record) {
|
|
|
2557
2479
|
if (!flare) return;
|
|
2558
2480
|
const root = flare.tracer.getActiveSpan();
|
|
2559
2481
|
if (!root || root.traceId !== record.parent.traceId || !root.isRecording) return;
|
|
2482
|
+
const selfTime = takeSelfTime(record.spanId, record.startTimeUnixNano, record.endTimeUnixNano);
|
|
2560
2483
|
flare.startSpan(record.name, {
|
|
2561
2484
|
spanId: record.spanId,
|
|
2562
2485
|
parent: {
|
|
@@ -2567,19 +2490,17 @@ function recordComponentSpan(record) {
|
|
|
2567
2490
|
startTimeUnixNano: record.startTimeUnixNano,
|
|
2568
2491
|
attributes: {
|
|
2569
2492
|
...record.attributes,
|
|
2570
|
-
"flare.component.name": record.name
|
|
2493
|
+
"flare.component.name": record.name,
|
|
2494
|
+
"flare.component.self_time_ns": selfTime
|
|
2571
2495
|
},
|
|
2572
2496
|
claimed: true
|
|
2573
2497
|
}).end(record.endTimeUnixNano);
|
|
2498
|
+
trackChildInterval(record.parent.parentSpanId, record.startTimeUnixNano, record.endTimeUnixNano);
|
|
2574
2499
|
} catch {}
|
|
2575
2500
|
}
|
|
2576
2501
|
|
|
2577
2502
|
//#endregion
|
|
2578
2503
|
//#region src/createFlareResolver.ts
|
|
2579
|
-
/**
|
|
2580
|
-
* `process.env.NODE_ENV` is replaced inline by bundlers. The try/catch keeps a process-less
|
|
2581
|
-
* environment safe: treat "undetermined" as production (warn, never crash).
|
|
2582
|
-
*/
|
|
2583
2504
|
function isDevMode() {
|
|
2584
2505
|
try {
|
|
2585
2506
|
return process.env.NODE_ENV !== "production";
|
|
@@ -2588,11 +2509,10 @@ function isDevMode() {
|
|
|
2588
2509
|
}
|
|
2589
2510
|
}
|
|
2590
2511
|
/**
|
|
2591
|
-
* Builds a per-package Flare resolver: `registerDefaultFlare` (
|
|
2592
|
-
* `resolveFlare` (called at wiring time). Each call
|
|
2593
|
-
*
|
|
2594
|
-
*
|
|
2595
|
-
* 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).
|
|
2596
2516
|
*/
|
|
2597
2517
|
function createFlareResolver(config) {
|
|
2598
2518
|
const { packageName } = config;
|