@flareapp/js 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.cjs +42 -29
- package/dist/browser.d.cts +5 -2
- package/dist/browser.d.mts +5 -2
- package/dist/browser.mjs +18 -6
- package/dist/{componentProfiler-CoH2m13n.mjs → catchWindowErrors-C0gSVGiF.mjs} +1181 -882
- package/dist/{componentProfiler-C6qY96Bt.cjs → catchWindowErrors-cR2D7kju.cjs} +1192 -887
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{spanTypes-CjF1_Uqi.d.mts → spanTypes-7-nEycOc.d.mts} +33 -39
- package/dist/{spanTypes-iA1RdfLw.d.cts → spanTypes-TMgJEZ7G.d.cts} +32 -38
- package/package.json +2 -2
|
@@ -1,126 +1,138 @@
|
|
|
1
1
|
let _flareapp_core = require("@flareapp/core");
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (document.visibilityState === "hidden") flush({ keepalive: true });
|
|
9
|
-
});
|
|
10
|
-
if (typeof window !== "undefined" && window) window.addEventListener("pagehide", () => flush({ keepalive: true }));
|
|
11
|
-
}
|
|
12
|
-
};
|
|
3
|
+
//#region src/breadcrumbs/utils/documentEvent.ts
|
|
4
|
+
function onDocumentEvent(name, handle) {
|
|
5
|
+
document.addEventListener(name, handle, true);
|
|
6
|
+
return () => document.removeEventListener(name, handle, true);
|
|
7
|
+
}
|
|
13
8
|
|
|
14
9
|
//#endregion
|
|
15
|
-
//#region src/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
10
|
+
//#region src/breadcrumbs/utils/elementSelector.ts
|
|
11
|
+
const INTERACTIVE_HTML_ELEMENTS = "button, a, input, select, textarea, label, [role], [tabindex], [onclick]";
|
|
12
|
+
const MAX_ANCESTOR_DEPTH = 5;
|
|
13
|
+
function interactiveTarget(target) {
|
|
14
|
+
let element = target;
|
|
15
|
+
for (let depth = 0; element && depth < MAX_ANCESTOR_DEPTH; depth++) {
|
|
16
|
+
if (element.matches?.(INTERACTIVE_HTML_ELEMENTS)) return element;
|
|
17
|
+
element = element.parentElement;
|
|
18
|
+
}
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
function elementSelector(element) {
|
|
22
|
+
let selector = element.tagName.toLowerCase();
|
|
23
|
+
if (element.id) selector += `#${element.id}`;
|
|
24
|
+
for (const className of element.classList) selector += `.${className}`;
|
|
25
|
+
return selector;
|
|
26
|
+
}
|
|
27
|
+
function elementTestId(element) {
|
|
28
|
+
return element.getAttribute?.("data-testid") ?? void 0;
|
|
29
|
+
}
|
|
30
|
+
function elementAttributes(element) {
|
|
31
|
+
const attributes = { "browser.element.selector": elementSelector(element) };
|
|
32
|
+
const testId = elementTestId(element);
|
|
33
|
+
if (testId) attributes["browser.element.test_id"] = testId;
|
|
34
|
+
return attributes;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
//#endregion
|
|
37
|
-
//#region src/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
//#region src/breadcrumbs/ClickRecorder.ts
|
|
39
|
+
var ClickRecorder = class {
|
|
40
|
+
type = _flareapp_core.BrowserSpanEventType.Click;
|
|
41
|
+
constructor(host) {
|
|
42
|
+
this.host = host;
|
|
43
|
+
this.onClick = this.onClick.bind(this);
|
|
44
|
+
}
|
|
45
|
+
install() {
|
|
46
|
+
return onDocumentEvent("click", this.onClick);
|
|
47
|
+
}
|
|
48
|
+
onClick(event) {
|
|
49
|
+
const target = event.target;
|
|
50
|
+
if (!(target instanceof Element)) return;
|
|
51
|
+
this.host.record(this.type, elementAttributes(interactiveTarget(target)), (0, _flareapp_core.defaultNowNano)());
|
|
52
|
+
}
|
|
53
|
+
};
|
|
51
54
|
|
|
52
55
|
//#endregion
|
|
53
|
-
//#region src/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
//#region src/breadcrumbs/FormChangeRecorder.ts
|
|
57
|
+
var FormChangeRecorder = class {
|
|
58
|
+
type = _flareapp_core.BrowserSpanEventType.Input;
|
|
59
|
+
constructor(host) {
|
|
60
|
+
this.host = host;
|
|
61
|
+
this.onChange = this.onChange.bind(this);
|
|
62
|
+
}
|
|
63
|
+
install() {
|
|
64
|
+
return onDocumentEvent("change", this.onChange);
|
|
65
|
+
}
|
|
66
|
+
onChange(event) {
|
|
67
|
+
const target = event.target;
|
|
68
|
+
if (!(target instanceof Element)) return;
|
|
69
|
+
this.host.record(this.type, elementAttributes(target), (0, _flareapp_core.defaultNowNano)());
|
|
66
70
|
}
|
|
67
|
-
return attrs;
|
|
68
|
-
}
|
|
69
|
-
const collectBrowser = (config) => {
|
|
70
|
-
const attrs = { ...browserEntryPoint(config) };
|
|
71
|
-
if (typeof window === "undefined") return attrs;
|
|
72
|
-
if (window?.location?.hostname) attrs["host.name"] = window.location.hostname;
|
|
73
|
-
Object.assign(attrs, request(config.urlDenylist));
|
|
74
|
-
Object.assign(attrs, cookie(config.urlDenylist));
|
|
75
|
-
return attrs;
|
|
76
71
|
};
|
|
77
72
|
|
|
78
73
|
//#endregion
|
|
79
|
-
//#region src/tracing/
|
|
74
|
+
//#region src/tracing/utils/absoluteHref.ts
|
|
80
75
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* not the app's traffic, so tracing them puts a span in the customer's waterfall for a request
|
|
84
|
-
* their code never made, and propagating a `traceparent` on them is just as wrong.
|
|
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.
|
|
85
78
|
*
|
|
86
|
-
*
|
|
87
|
-
* are known up front. A snippet fetch targets the customer's own asset, so only the caller knows.
|
|
79
|
+
* Undefined outside a browser or for an unparseable href, so the caller can leave its attribute alone.
|
|
88
80
|
*/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
81
|
+
function absoluteUrl(href) {
|
|
82
|
+
if (href == null || typeof window === "undefined") return;
|
|
83
|
+
try {
|
|
84
|
+
return new URL(href, window.location.href);
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
96
88
|
}
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
/**
|
|
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.
|
|
93
|
+
*/
|
|
94
|
+
function absoluteHref(href) {
|
|
95
|
+
return absoluteUrl(href)?.href;
|
|
99
96
|
}
|
|
100
97
|
|
|
101
98
|
//#endregion
|
|
102
|
-
//#region src/
|
|
99
|
+
//#region src/instrumentation/navigation/utils.ts
|
|
100
|
+
function currentPath() {
|
|
101
|
+
return typeof location !== "undefined" ? location.pathname : "";
|
|
102
|
+
}
|
|
103
|
+
function currentHref() {
|
|
104
|
+
return typeof location !== "undefined" ? location.href : "";
|
|
105
|
+
}
|
|
106
|
+
function routeName(derive, fallbackPath, url) {
|
|
107
|
+
try {
|
|
108
|
+
const name = derive();
|
|
109
|
+
if (name) return {
|
|
110
|
+
name,
|
|
111
|
+
source: "route",
|
|
112
|
+
url
|
|
113
|
+
};
|
|
114
|
+
} catch {}
|
|
115
|
+
return {
|
|
116
|
+
name: fallbackPath,
|
|
117
|
+
source: "url",
|
|
118
|
+
url
|
|
119
|
+
};
|
|
120
|
+
}
|
|
103
121
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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`.
|
|
107
125
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
//#endregion
|
|
119
|
-
//#region src/env/index.ts
|
|
120
|
-
const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.8.0" : "?";
|
|
126
|
+
function resolveHref(build, fallbackHref) {
|
|
127
|
+
let href = fallbackHref;
|
|
128
|
+
try {
|
|
129
|
+
href = build() ?? fallbackHref;
|
|
130
|
+
} catch {}
|
|
131
|
+
return absoluteHref(href);
|
|
132
|
+
}
|
|
121
133
|
|
|
122
134
|
//#endregion
|
|
123
|
-
//#region src/tracing/fill.ts
|
|
135
|
+
//#region src/tracing/utils/fill.ts
|
|
124
136
|
/**
|
|
125
137
|
* Replace `source[name]` with `replacer(original)`, tagging the wrapper with a
|
|
126
138
|
* non-enumerable `__flare_original__` so the patch is idempotent and reversible.
|
|
@@ -147,275 +159,237 @@ function unfill(source, name) {
|
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
//#endregion
|
|
150
|
-
//#region src/
|
|
162
|
+
//#region src/instrumentation/navigation/navigationBus.ts
|
|
163
|
+
const subscribers$1 = /* @__PURE__ */ new Set();
|
|
164
|
+
let source = null;
|
|
165
|
+
let currentRoute = null;
|
|
166
|
+
let lastPath = "";
|
|
167
|
+
let uninstallHistory = null;
|
|
168
|
+
function broadcast(callback) {
|
|
169
|
+
for (const subscriber of subscribers$1) try {
|
|
170
|
+
callback(subscriber);
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
function onHistoryChange() {
|
|
174
|
+
if (!uninstallHistory) return;
|
|
175
|
+
const path = currentPath();
|
|
176
|
+
if (path === lastPath) return;
|
|
177
|
+
lastPath = path;
|
|
178
|
+
if (source) return;
|
|
179
|
+
broadcast((subscriber) => subscriber.onUrlChanged?.(path));
|
|
180
|
+
}
|
|
181
|
+
function installHistory() {
|
|
182
|
+
if (uninstallHistory) return;
|
|
183
|
+
if (typeof window === "undefined" || typeof history === "undefined" || typeof location === "undefined") return;
|
|
184
|
+
lastPath = currentPath();
|
|
185
|
+
function wrapHistoryMethod(original) {
|
|
186
|
+
return function(...args) {
|
|
187
|
+
const result = original.apply(this, args);
|
|
188
|
+
onHistoryChange();
|
|
189
|
+
return result;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
fill(history, "pushState", wrapHistoryMethod);
|
|
193
|
+
fill(history, "replaceState", wrapHistoryMethod);
|
|
194
|
+
window.addEventListener("popstate", onHistoryChange);
|
|
195
|
+
uninstallHistory = () => {
|
|
196
|
+
unfill(history, "pushState");
|
|
197
|
+
unfill(history, "replaceState");
|
|
198
|
+
window.removeEventListener("popstate", onHistoryChange);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function subscribeToNavigation(subscriber) {
|
|
202
|
+
if (subscribers$1.size === 0) installHistory();
|
|
203
|
+
subscribers$1.add(subscriber);
|
|
204
|
+
if (currentRoute) try {
|
|
205
|
+
subscriber.onRouteName?.(currentRoute.route, currentRoute.owner);
|
|
206
|
+
} catch {}
|
|
207
|
+
let removed = false;
|
|
208
|
+
return () => {
|
|
209
|
+
if (removed) return;
|
|
210
|
+
removed = true;
|
|
211
|
+
subscribers$1.delete(subscriber);
|
|
212
|
+
if (subscribers$1.size === 0) {
|
|
213
|
+
uninstallHistory?.();
|
|
214
|
+
uninstallHistory = null;
|
|
215
|
+
lastPath = "";
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
151
219
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* Target is passed per call, not captured, because callers look it up fresh (`globalThis.fetch` may not
|
|
156
|
-
* exist yet under SSR).
|
|
220
|
+
* Hands navigation to a framework router: while registered, the built-in History detection stays
|
|
221
|
+
* quiet and the router drives every step through the returned handle. The newest registration wins
|
|
222
|
+
* and a stale handle no-ops, because HMR can replace a router that still holds one.
|
|
157
223
|
*/
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
224
|
+
function registerNavigationSource() {
|
|
225
|
+
const token = {};
|
|
226
|
+
if (source) console.debug("Flare: navigation source replaced");
|
|
227
|
+
source = token;
|
|
228
|
+
const active = () => source === token;
|
|
161
229
|
return {
|
|
162
|
-
|
|
163
|
-
return
|
|
230
|
+
startNavigation(opts) {
|
|
231
|
+
if (!active()) return;
|
|
232
|
+
const path = opts?.path ?? currentPath();
|
|
233
|
+
lastPath = path;
|
|
234
|
+
broadcast((subscriber) => subscriber.onNavigationStart?.({
|
|
235
|
+
path,
|
|
236
|
+
url: opts?.url,
|
|
237
|
+
hold: opts?.hold
|
|
238
|
+
}));
|
|
164
239
|
},
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
for (const name of names) applyOne(name);
|
|
173
|
-
installed = true;
|
|
240
|
+
setActiveRouteName(route) {
|
|
241
|
+
if (!active()) return;
|
|
242
|
+
currentRoute = {
|
|
243
|
+
route,
|
|
244
|
+
owner: token
|
|
245
|
+
};
|
|
246
|
+
broadcast((subscriber) => subscriber.onRouteName?.(route, token));
|
|
174
247
|
},
|
|
175
|
-
|
|
176
|
-
if (!
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
248
|
+
settleNavigation(route) {
|
|
249
|
+
if (!active()) return;
|
|
250
|
+
currentRoute = {
|
|
251
|
+
route,
|
|
252
|
+
owner: token
|
|
253
|
+
};
|
|
254
|
+
broadcast((subscriber) => subscriber.onNavigationSettle?.(route, token));
|
|
255
|
+
},
|
|
256
|
+
unregister() {
|
|
257
|
+
if (!active()) return;
|
|
258
|
+
broadcast((subscriber) => subscriber.onSourceUnregistered?.());
|
|
259
|
+
source = null;
|
|
260
|
+
currentRoute = null;
|
|
261
|
+
lastPath = currentPath();
|
|
183
262
|
}
|
|
184
263
|
};
|
|
185
264
|
}
|
|
265
|
+
/** A name from an earlier call is only valid while that source is still registered. */
|
|
266
|
+
function isActiveNavigationSource(token) {
|
|
267
|
+
return token !== null && source === token;
|
|
268
|
+
}
|
|
186
269
|
|
|
187
270
|
//#endregion
|
|
188
|
-
//#region src/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
271
|
+
//#region src/breadcrumbs/NavigationRecorder.ts
|
|
272
|
+
var NavigationRecorder = class {
|
|
273
|
+
type = _flareapp_core.BrowserSpanEventType.RouteChange;
|
|
274
|
+
previousHref = "";
|
|
275
|
+
constructor(host) {
|
|
276
|
+
this.host = host;
|
|
277
|
+
this.onUrlChanged = this.onUrlChanged.bind(this);
|
|
278
|
+
this.onNavigationSettle = this.onNavigationSettle.bind(this);
|
|
194
279
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
280
|
+
install() {
|
|
281
|
+
this.record(currentHref());
|
|
282
|
+
return subscribeToNavigation({
|
|
283
|
+
onUrlChanged: this.onUrlChanged,
|
|
284
|
+
onNavigationSettle: this.onNavigationSettle
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
onUrlChanged() {
|
|
288
|
+
this.record(currentHref());
|
|
289
|
+
}
|
|
290
|
+
onNavigationSettle(route) {
|
|
291
|
+
this.record(route.url ?? currentHref(), route);
|
|
292
|
+
}
|
|
293
|
+
record(href, route) {
|
|
294
|
+
const attributes = { "browser.route.to": this.clean(href) };
|
|
295
|
+
if (this.previousHref) attributes["browser.route.from"] = this.clean(this.previousHref);
|
|
296
|
+
if (route?.source === "route") {
|
|
297
|
+
attributes["flare.entry_point.handler.identifier"] = route.name;
|
|
298
|
+
attributes["flare.route.source"] = route.source;
|
|
207
299
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
300
|
+
this.previousHref = href;
|
|
301
|
+
this.host.record(this.type, attributes, (0, _flareapp_core.defaultNowNano)());
|
|
302
|
+
}
|
|
303
|
+
clean(href) {
|
|
304
|
+
return (0, _flareapp_core.breadcrumbUrl)(href, this.host.config().urlDenylist);
|
|
211
305
|
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/instrumentation/requests/requestBus.ts
|
|
310
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
311
|
+
let mutator = null;
|
|
312
|
+
function subscribeToRequests(subscriber) {
|
|
313
|
+
subscribers.add(subscriber);
|
|
314
|
+
return () => {
|
|
315
|
+
subscribers.delete(subscriber);
|
|
316
|
+
};
|
|
212
317
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
318
|
+
function claimRequestMutation(owner) {
|
|
319
|
+
if (mutator !== null) console.warn(`%c FLARE %c
|
|
320
|
+
|
|
321
|
+
What: two things tried to add headers to outgoing requests. Only one can.
|
|
322
|
+
|
|
323
|
+
Why it matters: the first one stopped. Requests can now go out without a traceparent header, so Flare cannot link a browser request to its server trace.
|
|
324
|
+
|
|
325
|
+
How to fix: use one Flare instance, and check your bundle for two copies of @flareapp/js.`, "background:#e11d48;color:#fff;font-weight:bold;font-size:14px;padding:2px 6px", "color:#e11d48;font-size:13px");
|
|
326
|
+
mutator = owner;
|
|
327
|
+
return () => {
|
|
328
|
+
if (mutator === owner) mutator = null;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function hasRequestSubscribers() {
|
|
332
|
+
return subscribers.size > 0 || mutator !== null;
|
|
216
333
|
}
|
|
217
334
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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.
|
|
222
339
|
*/
|
|
223
|
-
function
|
|
224
|
-
const
|
|
340
|
+
function publishRequestStart(start) {
|
|
341
|
+
const handlers = [];
|
|
342
|
+
for (const subscriber of subscribers) try {
|
|
343
|
+
const handler = subscriber(start);
|
|
344
|
+
if (handler) handlers.push(handler);
|
|
345
|
+
} catch {}
|
|
346
|
+
let init = start.init;
|
|
225
347
|
let headers;
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (pairs === null) headers = source;
|
|
236
|
-
else if (pairs.some(([k]) => k.toLowerCase() === "traceparent")) return init;
|
|
237
|
-
else headers = [...pairs, ["traceparent", traceparent]];
|
|
238
|
-
} else if (source) {
|
|
239
|
-
if (Object.keys(source).some((k) => k.toLowerCase() === "traceparent")) return init;
|
|
240
|
-
headers = {
|
|
241
|
-
...source,
|
|
242
|
-
traceparent
|
|
243
|
-
};
|
|
244
|
-
} else headers = { traceparent };
|
|
245
|
-
const result = { headers };
|
|
246
|
-
if (init) {
|
|
247
|
-
const descriptors = Object.getOwnPropertyDescriptors(init);
|
|
248
|
-
delete descriptors.headers;
|
|
249
|
-
Object.defineProperties(result, descriptors);
|
|
250
|
-
}
|
|
251
|
-
if (result.duplex === void 0 && typeof Request !== "undefined" && input instanceof Request && input.body != null) result.duplex = "half";
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region src/tracing/httpRequestSpan.ts
|
|
257
|
-
const INLINE_SCHEMES = new Set(["data:", "blob:"]);
|
|
258
|
-
/** The real browser context. Falls back to the origin where there is no document (SSR, tests). */
|
|
259
|
-
function browserUrlContext() {
|
|
260
|
-
const origin = globalThis.location?.origin ?? "";
|
|
261
|
-
return {
|
|
262
|
-
origin,
|
|
263
|
-
base: () => globalThis.document?.baseURI || origin
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
/** Resolve `url` to an absolute URL against `base`, or null if it cannot be parsed. */
|
|
267
|
-
function safeAbsolute(url, base) {
|
|
268
|
-
try {
|
|
269
|
-
return new URL(url, base || void 0);
|
|
270
|
-
} catch {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
let ingestCacheKey = null;
|
|
275
|
-
let ingestCacheHrefs = [];
|
|
276
|
-
function resolvedIngestHrefs(config, base) {
|
|
277
|
-
const raw = [
|
|
278
|
-
config.ingestUrl,
|
|
279
|
-
config.logsIngestUrl,
|
|
280
|
-
config.tracesIngestUrl
|
|
281
|
-
];
|
|
282
|
-
const key = `${base} ${raw.join(" ")}`;
|
|
283
|
-
if (key !== ingestCacheKey) {
|
|
284
|
-
ingestCacheKey = key;
|
|
285
|
-
ingestCacheHrefs = raw.filter((u) => typeof u === "string" && u.length > 0).map((u) => safeAbsolute(u, base)).filter((u) => u !== null).map((u) => u.href);
|
|
286
|
-
}
|
|
287
|
-
return ingestCacheHrefs;
|
|
288
|
-
}
|
|
289
|
-
function matchesIngestHref(href, ingestHref) {
|
|
290
|
-
if (!href.startsWith(ingestHref)) return false;
|
|
291
|
-
const next = href.charAt(ingestHref.length);
|
|
292
|
-
return next === "" || next === "/" || next === "?" || next === "#";
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* True when `resolved` targets one of Flare's own ingest endpoints (never traced). The configured
|
|
296
|
-
* URLs are resolved against `base` first: a relative one (a customer proxying ingest through their
|
|
297
|
-
* own origin) would otherwise never match, so every flush POST would open a span that arms the next
|
|
298
|
-
* flush, forever.
|
|
299
|
-
*/
|
|
300
|
-
function isFlareIngestUrl(resolved, config, base) {
|
|
301
|
-
if (!resolved) return false;
|
|
302
|
-
return resolvedIngestHrefs(config, base).some((ingestHref) => matchesIngestHref(resolved.href, ingestHref));
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Shared request-span attributes for a fetch/XHR call. The `url.*` attributes are redacted the same
|
|
306
|
-
* way as error reports, so tokens and reset codes never leak.
|
|
307
|
-
*/
|
|
308
|
-
function requestSpanAttributes(method, resolved, url, config) {
|
|
348
|
+
if (mutator) try {
|
|
349
|
+
const handler = mutator(start);
|
|
350
|
+
if (handler) {
|
|
351
|
+
handlers.push(handler);
|
|
352
|
+
if (handler.init !== void 0) init = handler.init;
|
|
353
|
+
headers = handler.headers;
|
|
354
|
+
}
|
|
355
|
+
} catch {}
|
|
356
|
+
if (handlers.length === 0 && init === start.init && headers === void 0) return null;
|
|
309
357
|
return {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
358
|
+
init,
|
|
359
|
+
headers,
|
|
360
|
+
settle(result) {
|
|
361
|
+
for (const handler of handlers) try {
|
|
362
|
+
handler.onSettle?.(result);
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
314
365
|
};
|
|
315
366
|
}
|
|
367
|
+
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/tracing/requests/internalRequest.ts
|
|
316
370
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*/
|
|
322
|
-
function endHttpRequestSpan(span, status, opts) {
|
|
323
|
-
span.setAttribute("http.response.status_code", status);
|
|
324
|
-
if (status >= 500 || opts?.zeroIsError && status === 0) span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
|
|
325
|
-
span.end();
|
|
326
|
-
}
|
|
327
|
-
function finishHttpSpanError(span, error) {
|
|
328
|
-
span.setStatus({
|
|
329
|
-
code: _flareapp_core.SpanStatusCode.Error,
|
|
330
|
-
message: error instanceof Error ? error.message : String(error)
|
|
331
|
-
});
|
|
332
|
-
span.end();
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Propagation gate plus `traceparent` build shared by fetch and XHR. Returns null when
|
|
336
|
-
* `shouldPropagate` rejects the URL (caller then skips header injection).
|
|
337
|
-
*/
|
|
338
|
-
function traceparentFor(span, resolved, url, origin, config) {
|
|
339
|
-
if (!shouldPropagate(resolved ? resolved.href : url, resolved, origin, config.tracePropagationTargets)) return null;
|
|
340
|
-
return (0, _flareapp_core.buildTraceparent)(span.traceId, span.spanId, span.isRecording);
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Open a request span for one outgoing fetch or XHR call. Null means the URL is one of Flare's own
|
|
344
|
-
* ingest endpoints, so the caller passes the request through untraced.
|
|
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.
|
|
345
375
|
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
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.
|
|
348
378
|
*/
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const base = urls.base();
|
|
353
|
-
const resolved = safeAbsolute(url, base);
|
|
354
|
-
if (isFlareIngestUrl(resolved, config, base)) return null;
|
|
355
|
-
if (resolved && INLINE_SCHEMES.has(resolved.protocol)) return null;
|
|
356
|
-
const pathname = resolved ? resolved.pathname : url;
|
|
379
|
+
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
|
+
function internalRequestInit(init) {
|
|
357
382
|
return {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
attributes: requestSpanAttributes(method, resolved, url, config)
|
|
361
|
-
}),
|
|
362
|
-
absoluteUrl: resolved
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
//#endregion
|
|
367
|
-
//#region src/tracing/instrumentationGuard.ts
|
|
368
|
-
/** For a callback the host invokes: a router guard, a store subscriber, ... */
|
|
369
|
-
function insulate(fn) {
|
|
370
|
-
return (...args) => {
|
|
371
|
-
try {
|
|
372
|
-
fn(...args);
|
|
373
|
-
} catch {}
|
|
383
|
+
...init,
|
|
384
|
+
[INTERNAL_REQUEST_KEY]: true
|
|
374
385
|
};
|
|
375
386
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
fn?.();
|
|
380
|
-
} catch {}
|
|
381
|
-
}
|
|
382
|
-
const instrumented = /* @__PURE__ */ new WeakMap();
|
|
383
|
-
/**
|
|
384
|
-
* Instrument `target` at most once at a time, tearing down any prior instrumentation of the same object
|
|
385
|
-
* first. Vite HMR re-runs boot code against a router that survives the reload, so without this every
|
|
386
|
-
* cycle appends another listener set that is never removed. Keyed on the object, so a genuinely new
|
|
387
|
-
* router is unaffected.
|
|
388
|
-
*
|
|
389
|
-
* `install` hands each teardown to `track` as it produces it. A router's own `subscribe` / `on` / guard
|
|
390
|
-
* registration can throw, and `install` runs during the host's bootstrap, so a throw part-way through
|
|
391
|
-
* unwinds what already succeeded (newest first) and stops here rather than reaching the host.
|
|
392
|
-
*
|
|
393
|
-
* @returns the cleanup, or a no-op when the install failed and already unwound itself.
|
|
394
|
-
*/
|
|
395
|
-
function instrumentOnce(target, install) {
|
|
396
|
-
instrumented.get(target)?.();
|
|
397
|
-
const teardowns = [];
|
|
398
|
-
function unwind() {
|
|
399
|
-
for (let i = teardowns.length - 1; i >= 0; i--) safeInvoke(teardowns[i]);
|
|
400
|
-
}
|
|
401
|
-
try {
|
|
402
|
-
install((teardown) => {
|
|
403
|
-
teardowns.push(teardown);
|
|
404
|
-
});
|
|
405
|
-
} catch {
|
|
406
|
-
unwind();
|
|
407
|
-
return () => {};
|
|
408
|
-
}
|
|
409
|
-
function cleanup() {
|
|
410
|
-
unwind();
|
|
411
|
-
if (instrumented.get(target) === cleanup) instrumented.delete(target);
|
|
412
|
-
}
|
|
413
|
-
instrumented.set(target, cleanup);
|
|
414
|
-
return cleanup;
|
|
387
|
+
function isInternalRequest(init) {
|
|
388
|
+
return init?.[INTERNAL_REQUEST_KEY] === true;
|
|
415
389
|
}
|
|
416
390
|
|
|
417
391
|
//#endregion
|
|
418
|
-
//#region src/tracing/supportsNativeFetch.ts
|
|
392
|
+
//#region src/tracing/requests/supportsNativeFetch.ts
|
|
419
393
|
/** True if `fn` is the browser's native fetch (not a polyfill/wrapper). */
|
|
420
394
|
function isNativeFetch(fn) {
|
|
421
395
|
return typeof fn === "function" && /native code/.test(Function.prototype.toString.call(fn));
|
|
@@ -452,7 +426,42 @@ function supportsNativeFetch() {
|
|
|
452
426
|
}
|
|
453
427
|
|
|
454
428
|
//#endregion
|
|
455
|
-
//#region src/tracing/
|
|
429
|
+
//#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
|
+
function createPatcher() {
|
|
435
|
+
let installed = false;
|
|
436
|
+
let names = [];
|
|
437
|
+
return {
|
|
438
|
+
get installed() {
|
|
439
|
+
return installed;
|
|
440
|
+
},
|
|
441
|
+
install(target, patches) {
|
|
442
|
+
if (installed) return;
|
|
443
|
+
function applyOne(name) {
|
|
444
|
+
const wrap = patches[name];
|
|
445
|
+
if (wrap) fill(target, name, wrap);
|
|
446
|
+
}
|
|
447
|
+
names = Object.keys(patches);
|
|
448
|
+
for (const name of names) applyOne(name);
|
|
449
|
+
installed = true;
|
|
450
|
+
},
|
|
451
|
+
uninstall(target) {
|
|
452
|
+
if (!installed) return;
|
|
453
|
+
if (!names.every((name) => {
|
|
454
|
+
const current = target[name];
|
|
455
|
+
return typeof current !== "function" || Boolean(current.__flare_original__);
|
|
456
|
+
})) return;
|
|
457
|
+
for (const name of names) unfill(target, name);
|
|
458
|
+
installed = false;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/instrumentation/requests/instrumentFetch.ts
|
|
456
465
|
function resolveRequest(input, init) {
|
|
457
466
|
let url;
|
|
458
467
|
let method = init?.method;
|
|
@@ -465,113 +474,79 @@ function resolveRequest(input, init) {
|
|
|
465
474
|
url
|
|
466
475
|
};
|
|
467
476
|
}
|
|
468
|
-
|
|
469
|
-
* Build a fetch replacement that opens a `browser_fetch` span per call, injects `traceparent` on
|
|
470
|
-
* propagation-eligible URLs, and ends the span on settle. Pure factory: `urls` is injected (node
|
|
471
|
-
* test env has no `location` or `document`), so this is unit-testable without a browser.
|
|
472
|
-
*/
|
|
473
|
-
function createFetchWrapper(tracer, original, urls) {
|
|
477
|
+
function createFetchWrapper(original) {
|
|
474
478
|
return function(input, init) {
|
|
475
479
|
const call = (i) => original.call(this, input, i);
|
|
476
|
-
let
|
|
477
|
-
let url = "";
|
|
478
|
-
let passthrough = false;
|
|
480
|
+
let watched = null;
|
|
479
481
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
spanType: _flareapp_core.BrowserSpanType.Fetch
|
|
482
|
+
if (hasRequestSubscribers() && !isInternalRequest(init)) {
|
|
483
|
+
const request = resolveRequest(input, init);
|
|
484
|
+
watched = publishRequestStart({
|
|
485
|
+
kind: "fetch",
|
|
486
|
+
method: request.method,
|
|
487
|
+
url: request.url,
|
|
488
|
+
input,
|
|
489
|
+
init
|
|
489
490
|
});
|
|
490
491
|
}
|
|
491
492
|
} catch {
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
if (passthrough || !started) return call(init);
|
|
495
|
-
const { span, absoluteUrl } = started;
|
|
496
|
-
let finalInit = init;
|
|
497
|
-
try {
|
|
498
|
-
const traceparent = traceparentFor(span, absoluteUrl, url, urls.origin, tracer.config);
|
|
499
|
-
if (traceparent) finalInit = mergeTraceparentHeader(input, init, traceparent);
|
|
500
|
-
} catch {
|
|
501
|
-
finalInit = init;
|
|
493
|
+
watched = null;
|
|
502
494
|
}
|
|
503
|
-
|
|
504
|
-
const
|
|
495
|
+
if (!watched) return call(init);
|
|
496
|
+
const settle = watched.settle;
|
|
505
497
|
const finishError = (error) => {
|
|
506
|
-
|
|
498
|
+
settle({ error });
|
|
507
499
|
return Promise.reject(error);
|
|
508
500
|
};
|
|
509
501
|
let promise;
|
|
510
502
|
try {
|
|
511
|
-
promise = call(
|
|
503
|
+
promise = call(watched.init);
|
|
512
504
|
} catch (error) {
|
|
513
505
|
return finishError(error);
|
|
514
506
|
}
|
|
515
507
|
return promise.then((response) => {
|
|
516
|
-
|
|
508
|
+
settle({ status: response.status });
|
|
517
509
|
return response;
|
|
518
510
|
}, finishError);
|
|
519
511
|
};
|
|
520
512
|
}
|
|
521
513
|
const patcher$1 = createPatcher();
|
|
522
|
-
|
|
523
|
-
* Patch the global `fetch` so outgoing requests are traced. No-op when there is no `fetch` or it
|
|
524
|
-
* is not native (a polyfilled/XHR-backed fetch is left for the XHR patch). Idempotent via `fill`.
|
|
525
|
-
* Reversible via `unpatchFetch`.
|
|
526
|
-
*/
|
|
527
|
-
function instrumentFetch(tracer) {
|
|
514
|
+
function instrumentFetch() {
|
|
528
515
|
if (patcher$1.installed) return;
|
|
529
516
|
const globals = globalThis;
|
|
530
517
|
if (typeof globals.fetch !== "function") return;
|
|
531
518
|
if (!supportsNativeFetch()) return;
|
|
532
|
-
|
|
533
|
-
patcher$1.install(globals, { fetch: (original) => createFetchWrapper(tracer, original, urls) });
|
|
519
|
+
patcher$1.install(globals, { fetch: (original) => createFetchWrapper(original) });
|
|
534
520
|
}
|
|
535
|
-
/** Restore the original global `fetch`. Safe if never patched. */
|
|
536
521
|
function unpatchFetch() {
|
|
537
522
|
patcher$1.uninstall(globalThis);
|
|
538
523
|
}
|
|
539
524
|
|
|
540
525
|
//#endregion
|
|
541
|
-
//#region src/
|
|
526
|
+
//#region src/instrumentation/requests/instrumentXHR.ts
|
|
542
527
|
const XHR_DONE = 4;
|
|
543
528
|
const xhrState = /* @__PURE__ */ new WeakMap();
|
|
544
|
-
/**
|
|
545
|
-
* Drop the span and listener references once a request is done with them. The entry itself stays in
|
|
546
|
-
* the WeakMap for the re-send `ended` guard, so without this it would keep the Span and the listener
|
|
547
|
-
* closure alive for as long as the app holds on to the XHR.
|
|
548
|
-
*/
|
|
549
529
|
function releaseRequestRefs(state) {
|
|
550
|
-
state.
|
|
530
|
+
state.watched = void 0;
|
|
551
531
|
state.onDone = void 0;
|
|
552
532
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
533
|
+
function settleOnce(state, result) {
|
|
534
|
+
state.ended = true;
|
|
535
|
+
state.watched?.settle(result);
|
|
536
|
+
releaseRequestRefs(state);
|
|
537
|
+
}
|
|
558
538
|
function createXHROpen(original) {
|
|
559
539
|
return function(method, url, ...rest) {
|
|
560
540
|
const prior = xhrState.get(this);
|
|
561
|
-
if (prior && prior.
|
|
562
|
-
prior.ended = true;
|
|
541
|
+
if (prior && prior.watched && !prior.ended) {
|
|
563
542
|
if (prior.onDone) this.removeEventListener("readystatechange", prior.onDone);
|
|
564
|
-
|
|
565
|
-
prior.span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
|
|
566
|
-
prior.span.end();
|
|
567
|
-
} catch {}
|
|
568
|
-
releaseRequestRefs(prior);
|
|
543
|
+
settleOnce(prior, { aborted: true });
|
|
569
544
|
}
|
|
570
545
|
try {
|
|
571
546
|
if (method && url != null) xhrState.set(this, {
|
|
572
547
|
method: String(method).toUpperCase(),
|
|
573
548
|
url: String(url),
|
|
574
|
-
|
|
549
|
+
appHeaders: /* @__PURE__ */ new Set(),
|
|
575
550
|
ended: false
|
|
576
551
|
});
|
|
577
552
|
else xhrState.delete(this);
|
|
@@ -585,69 +560,50 @@ function createXHROpen(original) {
|
|
|
585
560
|
]);
|
|
586
561
|
};
|
|
587
562
|
}
|
|
588
|
-
/**
|
|
589
|
-
* Patch `setRequestHeader` to note when the app sets its own `traceparent`.
|
|
590
|
-
* There is no `getRequestHeader`, so this is the only way to avoid emitting a
|
|
591
|
-
* second `traceparent` (repeat calls merge into one malformed header).
|
|
592
|
-
*/
|
|
593
563
|
function createXHRSetRequestHeader(original) {
|
|
594
564
|
return function(name, value) {
|
|
595
565
|
original.call(this, name, value);
|
|
596
|
-
if (typeof name === "string"
|
|
597
|
-
const state = xhrState.get(this);
|
|
598
|
-
if (state) state.hasAppTraceparent = true;
|
|
599
|
-
}
|
|
566
|
+
if (typeof name === "string") xhrState.get(this)?.appHeaders.add(name.toLowerCase());
|
|
600
567
|
};
|
|
601
568
|
}
|
|
602
|
-
function
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
569
|
+
function applyHeaders(xhr, state) {
|
|
570
|
+
const headers = state.watched?.headers;
|
|
571
|
+
if (!headers) return;
|
|
572
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
573
|
+
if (state.appHeaders.has(name.toLowerCase())) continue;
|
|
574
|
+
try {
|
|
575
|
+
xhr.setRequestHeader(name, value);
|
|
576
|
+
} catch {}
|
|
577
|
+
}
|
|
607
578
|
}
|
|
608
|
-
|
|
609
|
-
function createXHRSend(tracer, original, urls) {
|
|
579
|
+
function createXHRSend(original) {
|
|
610
580
|
return function(body) {
|
|
611
581
|
const send = () => original.call(this, body);
|
|
612
|
-
const config = tracer.config;
|
|
613
582
|
const state = xhrState.get(this);
|
|
614
|
-
if (!
|
|
583
|
+
if (!state || !hasRequestSubscribers()) return send();
|
|
615
584
|
if (state.ended) return send();
|
|
616
|
-
let
|
|
585
|
+
let watched = null;
|
|
617
586
|
try {
|
|
618
|
-
|
|
587
|
+
watched = publishRequestStart({
|
|
588
|
+
kind: "xhr",
|
|
619
589
|
method: state.method,
|
|
620
|
-
url: state.url
|
|
621
|
-
urls,
|
|
622
|
-
spanType: _flareapp_core.BrowserSpanType.Xhr
|
|
590
|
+
url: state.url
|
|
623
591
|
});
|
|
624
592
|
} catch {
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
if (!started) return send();
|
|
628
|
-
const { span, absoluteUrl } = started;
|
|
629
|
-
state.span = span;
|
|
630
|
-
if (!state.hasAppTraceparent) {
|
|
631
|
-
let traceparent = null;
|
|
632
|
-
try {
|
|
633
|
-
traceparent = traceparentFor(span, absoluteUrl, state.url, urls.origin, config);
|
|
634
|
-
} catch {}
|
|
635
|
-
setTraceparentHeader(this, traceparent);
|
|
593
|
+
watched = null;
|
|
636
594
|
}
|
|
595
|
+
if (!watched) return send();
|
|
596
|
+
state.watched = watched;
|
|
597
|
+
applyHeaders(this, state);
|
|
637
598
|
const onDone = () => {
|
|
638
599
|
if (this.readyState !== XHR_DONE) return;
|
|
639
600
|
this.removeEventListener("readystatechange", onDone);
|
|
640
601
|
if (state.ended) return;
|
|
641
|
-
state.ended = true;
|
|
642
602
|
let status = 0;
|
|
643
603
|
try {
|
|
644
604
|
status = this.status;
|
|
645
605
|
} catch {}
|
|
646
|
-
|
|
647
|
-
const zeroIsError = absoluteUrl !== null && (absoluteUrl.protocol === "http:" || absoluteUrl.protocol === "https:");
|
|
648
|
-
endHttpRequestSpan(span, status, { zeroIsError });
|
|
649
|
-
} catch {}
|
|
650
|
-
releaseRequestRefs(state);
|
|
606
|
+
settleOnce(state, { status });
|
|
651
607
|
};
|
|
652
608
|
this.addEventListener("readystatechange", onDone);
|
|
653
609
|
state.onDone = onDone;
|
|
@@ -655,35 +611,24 @@ function createXHRSend(tracer, original, urls) {
|
|
|
655
611
|
return send();
|
|
656
612
|
} catch (error) {
|
|
657
613
|
this.removeEventListener("readystatechange", onDone);
|
|
658
|
-
state
|
|
659
|
-
try {
|
|
660
|
-
finishHttpSpanError(span, error);
|
|
661
|
-
} catch {}
|
|
662
|
-
releaseRequestRefs(state);
|
|
614
|
+
settleOnce(state, { error });
|
|
663
615
|
throw error;
|
|
664
616
|
}
|
|
665
617
|
};
|
|
666
618
|
}
|
|
667
619
|
const patcher = createPatcher();
|
|
668
620
|
let patchedPrototype = null;
|
|
669
|
-
|
|
670
|
-
* Patch `XMLHttpRequest.prototype` (`open`, `setRequestHeader`, `send`) so outgoing
|
|
671
|
-
* XHR requests are traced. No-op where `XMLHttpRequest` is absent (SSR). Idempotent
|
|
672
|
-
* via `fill`. Reversible via `unpatchXHR`.
|
|
673
|
-
*/
|
|
674
|
-
function instrumentXHR(tracer) {
|
|
621
|
+
function instrumentXHR() {
|
|
675
622
|
if (patcher.installed) return;
|
|
676
623
|
const xhrConstructor = globalThis.XMLHttpRequest;
|
|
677
624
|
if (typeof xhrConstructor !== "function" || !xhrConstructor.prototype) return;
|
|
678
|
-
const urls = browserUrlContext();
|
|
679
625
|
patcher.install(xhrConstructor.prototype, {
|
|
680
626
|
open: (original) => createXHROpen(original),
|
|
681
627
|
setRequestHeader: (original) => createXHRSetRequestHeader(original),
|
|
682
|
-
send: (original) => createXHRSend(
|
|
628
|
+
send: (original) => createXHRSend(original)
|
|
683
629
|
});
|
|
684
630
|
patchedPrototype = xhrConstructor.prototype;
|
|
685
631
|
}
|
|
686
|
-
/** Restore the original `XMLHttpRequest.prototype` methods. Safe if never patched. */
|
|
687
632
|
function unpatchXHR() {
|
|
688
633
|
if (!patchedPrototype) return;
|
|
689
634
|
patcher.uninstall(patchedPrototype);
|
|
@@ -691,286 +636,522 @@ function unpatchXHR() {
|
|
|
691
636
|
}
|
|
692
637
|
|
|
693
638
|
//#endregion
|
|
694
|
-
//#region src/
|
|
639
|
+
//#region src/instrumentation/requests/requestPatches.ts
|
|
640
|
+
let subscriptions = 0;
|
|
695
641
|
/**
|
|
696
|
-
*
|
|
697
|
-
*
|
|
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.
|
|
698
644
|
*
|
|
699
|
-
*
|
|
645
|
+
* @param subscribe registers one subscriber and returns its own teardown
|
|
700
646
|
*/
|
|
701
|
-
function
|
|
702
|
-
if (
|
|
647
|
+
function withRequestPatches(subscribe) {
|
|
648
|
+
if (subscriptions === 0) {
|
|
649
|
+
instrumentFetch();
|
|
650
|
+
instrumentXHR();
|
|
651
|
+
}
|
|
652
|
+
subscriptions++;
|
|
653
|
+
const unsubscribe = subscribe();
|
|
654
|
+
let removed = false;
|
|
655
|
+
return () => {
|
|
656
|
+
if (removed) return;
|
|
657
|
+
removed = true;
|
|
658
|
+
unsubscribe();
|
|
659
|
+
subscriptions--;
|
|
660
|
+
if (subscriptions === 0) {
|
|
661
|
+
unpatchFetch();
|
|
662
|
+
unpatchXHR();
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/tracing/requests/propagation.ts
|
|
669
|
+
/** Follows OTel/Sentry `tracePropagationTargets`: same-origin by default, `[]` disables all. */
|
|
670
|
+
function shouldPropagate(url, absoluteUrl, currentOrigin, targets) {
|
|
671
|
+
if (targets) {
|
|
672
|
+
if (targets.length === 0) return false;
|
|
673
|
+
return targets.some((t) => typeof t === "string" ? url.includes(t) : t.test(url));
|
|
674
|
+
}
|
|
675
|
+
return absoluteUrl !== null && absoluteUrl.origin === currentOrigin;
|
|
676
|
+
}
|
|
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
|
+
function headerPairsFrom(source) {
|
|
703
680
|
try {
|
|
704
|
-
|
|
681
|
+
const pairs = [];
|
|
682
|
+
for (const entry of source) {
|
|
683
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
684
|
+
const pair = Array.from(entry);
|
|
685
|
+
if (pair.length !== 2) return null;
|
|
686
|
+
pairs.push([String(pair[0]), String(pair[1])]);
|
|
687
|
+
}
|
|
688
|
+
return pairs;
|
|
705
689
|
} catch {
|
|
706
|
-
return;
|
|
690
|
+
return null;
|
|
707
691
|
}
|
|
708
692
|
}
|
|
693
|
+
/** Fetch accepts any iterable of string pairs as HeadersInit (Map, URLSearchParams, cross-realm Headers). */
|
|
694
|
+
function isIterable(value) {
|
|
695
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.iterator] === "function";
|
|
696
|
+
}
|
|
709
697
|
/**
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
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.
|
|
713
702
|
*/
|
|
714
|
-
function
|
|
715
|
-
|
|
703
|
+
function mergeTraceparentHeader(input, init, traceparent) {
|
|
704
|
+
const source = init?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
|
|
705
|
+
let headers;
|
|
706
|
+
if (source instanceof Headers) {
|
|
707
|
+
if (source.has("traceparent")) return init;
|
|
708
|
+
headers = new Headers(source);
|
|
709
|
+
headers.set("traceparent", traceparent);
|
|
710
|
+
} else if (Array.isArray(source)) {
|
|
711
|
+
if (source.some(([k]) => String(k).toLowerCase() === "traceparent")) return init;
|
|
712
|
+
headers = [...source, ["traceparent", traceparent]];
|
|
713
|
+
} else if (isIterable(source)) {
|
|
714
|
+
const pairs = headerPairsFrom(source);
|
|
715
|
+
if (pairs === null) headers = source;
|
|
716
|
+
else if (pairs.some(([k]) => k.toLowerCase() === "traceparent")) return init;
|
|
717
|
+
else headers = [...pairs, ["traceparent", traceparent]];
|
|
718
|
+
} else if (source) {
|
|
719
|
+
if (Object.keys(source).some((k) => k.toLowerCase() === "traceparent")) return init;
|
|
720
|
+
headers = {
|
|
721
|
+
...source,
|
|
722
|
+
traceparent
|
|
723
|
+
};
|
|
724
|
+
} else headers = { traceparent };
|
|
725
|
+
const result = { headers };
|
|
726
|
+
if (init) {
|
|
727
|
+
const descriptors = Object.getOwnPropertyDescriptors(init);
|
|
728
|
+
delete descriptors.headers;
|
|
729
|
+
Object.defineProperties(result, descriptors);
|
|
730
|
+
}
|
|
731
|
+
if (result.duplex === void 0 && typeof Request !== "undefined" && input instanceof Request && input.body != null) result.duplex = "half";
|
|
732
|
+
return result;
|
|
716
733
|
}
|
|
717
734
|
|
|
718
735
|
//#endregion
|
|
719
|
-
//#region src/
|
|
736
|
+
//#region src/tracing/requests/httpRequestSpan.ts
|
|
737
|
+
const REQUEST_SPAN_TYPES = {
|
|
738
|
+
fetch: _flareapp_core.BrowserSpanType.Fetch,
|
|
739
|
+
xhr: _flareapp_core.BrowserSpanType.Xhr
|
|
740
|
+
};
|
|
741
|
+
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
|
+
function browserUrlContext() {
|
|
744
|
+
const origin = globalThis.location?.origin ?? "";
|
|
745
|
+
return {
|
|
746
|
+
origin,
|
|
747
|
+
base: () => globalThis.document?.baseURI || origin
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/** Resolve `url` to an absolute URL against `base`, or null if it cannot be parsed. */
|
|
751
|
+
function safeAbsolute(url, base) {
|
|
752
|
+
try {
|
|
753
|
+
return new URL(url, base || void 0);
|
|
754
|
+
} catch {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
let ingestCacheKey = null;
|
|
759
|
+
let ingestCacheHrefs = [];
|
|
760
|
+
function resolvedIngestHrefs(config, base) {
|
|
761
|
+
const raw = [
|
|
762
|
+
config.ingestUrl,
|
|
763
|
+
config.logsIngestUrl,
|
|
764
|
+
config.tracesIngestUrl
|
|
765
|
+
];
|
|
766
|
+
const key = `${base} ${raw.join(" ")}`;
|
|
767
|
+
if (key !== ingestCacheKey) {
|
|
768
|
+
ingestCacheKey = key;
|
|
769
|
+
ingestCacheHrefs = raw.filter((u) => typeof u === "string" && u.length > 0).map((u) => safeAbsolute(u, base)).filter((u) => u !== null).map((u) => u.href);
|
|
770
|
+
}
|
|
771
|
+
return ingestCacheHrefs;
|
|
772
|
+
}
|
|
773
|
+
function matchesIngestHref(href, ingestHref) {
|
|
774
|
+
if (!href.startsWith(ingestHref)) return false;
|
|
775
|
+
const next = href.charAt(ingestHref.length);
|
|
776
|
+
return next === "" || next === "/" || next === "?" || next === "#";
|
|
777
|
+
}
|
|
720
778
|
/**
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* @param hrefOverride destination href for a router that reports where it is going before the URL
|
|
726
|
-
* commits. Only the URL-derived keys come from it; the rest always reflect the live document. An
|
|
727
|
-
* unparseable override falls back to the live location instead of throwing into root creation.
|
|
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.
|
|
728
783
|
*/
|
|
729
|
-
function
|
|
730
|
-
if (
|
|
731
|
-
|
|
784
|
+
function isFlareIngestUrl(resolved, config, base) {
|
|
785
|
+
if (!resolved) return false;
|
|
786
|
+
return resolvedIngestHrefs(config, base).some((ingestHref) => matchesIngestHref(resolved.href, ingestHref));
|
|
787
|
+
}
|
|
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
|
+
function requestSpanAttributes(method, resolved, url, config) {
|
|
732
793
|
return {
|
|
733
|
-
|
|
734
|
-
...
|
|
794
|
+
"http.request.method": method,
|
|
795
|
+
...(0, _flareapp_core.urlAttributes)(resolved ? resolved.href : url, config.urlDenylist),
|
|
796
|
+
...resolved ? { "server.address": resolved.hostname } : {},
|
|
797
|
+
...resolved && resolved.port ? { "server.port": Number(resolved.port) } : {}
|
|
735
798
|
};
|
|
736
799
|
}
|
|
737
800
|
/**
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
|
|
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
|
+
function endHttpRequestSpan(span, status, opts) {
|
|
807
|
+
span.setAttribute("http.response.status_code", status);
|
|
808
|
+
if (status >= 500 || opts?.zeroIsError && status === 0) span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
|
|
809
|
+
span.end();
|
|
810
|
+
}
|
|
811
|
+
function finishHttpSpanError(span, error) {
|
|
812
|
+
span.setStatus({
|
|
813
|
+
code: _flareapp_core.SpanStatusCode.Error,
|
|
814
|
+
message: error instanceof Error ? error.message : String(error)
|
|
815
|
+
});
|
|
816
|
+
span.end();
|
|
817
|
+
}
|
|
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
|
+
function traceparentFor(span, resolved, url, origin, config) {
|
|
823
|
+
if (!shouldPropagate(resolved ? resolved.href : url, resolved, origin, config.tracePropagationTargets)) return null;
|
|
824
|
+
return (0, _flareapp_core.buildTraceparent)(span.traceId, span.spanId, span.isRecording);
|
|
825
|
+
}
|
|
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.
|
|
743
829
|
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
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.
|
|
746
832
|
*/
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
const
|
|
833
|
+
function startHttpRequestSpan(tracer, request) {
|
|
834
|
+
const { method, url, urls, spanType } = request;
|
|
835
|
+
const config = tracer.config;
|
|
836
|
+
const base = urls.base();
|
|
837
|
+
const resolved = safeAbsolute(url, base);
|
|
838
|
+
if (isFlareIngestUrl(resolved, config, base)) return null;
|
|
839
|
+
if (resolved && INLINE_SCHEMES.has(resolved.protocol)) return null;
|
|
840
|
+
const pathname = resolved ? resolved.pathname : url;
|
|
752
841
|
return {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
842
|
+
span: tracer.startSpan(`${method} ${pathname}`, {
|
|
843
|
+
spanType,
|
|
844
|
+
attributes: requestSpanAttributes(method, resolved, url, config)
|
|
845
|
+
}),
|
|
846
|
+
absoluteUrl: resolved
|
|
756
847
|
};
|
|
757
848
|
}
|
|
758
849
|
|
|
759
850
|
//#endregion
|
|
760
|
-
//#region src/tracing/
|
|
761
|
-
/** Browser defaults for the three idle-root timeouts, in ms. Overridable per Config. */
|
|
762
|
-
const DEFAULT_IDLE_TIMEOUTS = {
|
|
763
|
-
idleTimeout: 1e3,
|
|
764
|
-
finalTimeout: 3e4,
|
|
765
|
-
childSpanTimeout: 15e3
|
|
766
|
-
};
|
|
851
|
+
//#region src/tracing/requests/traceRequests.ts
|
|
767
852
|
/**
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
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.
|
|
771
857
|
*/
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Records the settle moment as a close floor and hands the root back to the normal idle lifecycle. It
|
|
802
|
-
* deliberately does not close here: a router settles before the framework mounts the new route
|
|
803
|
-
* component (vue-router runs `afterEach` in the route-update tick, Vue mounts on the next flush), so
|
|
804
|
-
* closing at settle cleared the active root ahead of every post-navigation mount: every component span
|
|
805
|
-
* read a null root, and a trailing fetch opened a root of its own.
|
|
806
|
-
*/
|
|
807
|
-
releaseHold() {
|
|
808
|
-
if (this.ended || !this.held) return;
|
|
809
|
-
this.held = false;
|
|
810
|
-
if (this.openChildren === 0) this.settleTime = this.deps.now();
|
|
811
|
-
this.armIdle();
|
|
812
|
-
}
|
|
813
|
-
onSpanEvent(phase, span) {
|
|
814
|
-
if (this.ended) return;
|
|
815
|
-
if (span === this.deps.root) return;
|
|
816
|
-
if (span.traceId !== this.deps.root.traceId) return;
|
|
817
|
-
if (phase === "start") {
|
|
818
|
-
this.onChildStarted();
|
|
819
|
-
return;
|
|
858
|
+
function zeroIsError(absoluteUrl) {
|
|
859
|
+
return absoluteUrl !== null && (absoluteUrl.protocol === "http:" || absoluteUrl.protocol === "https:");
|
|
860
|
+
}
|
|
861
|
+
function propagate(span, absoluteUrl, start, urls, tracer) {
|
|
862
|
+
const traceparent = traceparentFor(span, absoluteUrl, start.url, urls.origin, tracer.config);
|
|
863
|
+
if (!traceparent) return {};
|
|
864
|
+
if (start.kind === "xhr") return { headers: { traceparent } };
|
|
865
|
+
if (start.input === void 0) return {};
|
|
866
|
+
return { init: mergeTraceparentHeader(start.input, start.init, traceparent) };
|
|
867
|
+
}
|
|
868
|
+
/** Tracing takes the mutation slot, not a plain subscription, because it adds a `traceparent` header. */
|
|
869
|
+
function traceRequests(tracer, urls) {
|
|
870
|
+
return claimRequestMutation((start) => {
|
|
871
|
+
if (!tracer.config.enableTracing) return;
|
|
872
|
+
const started = startHttpRequestSpan(tracer, {
|
|
873
|
+
method: start.method,
|
|
874
|
+
url: start.url,
|
|
875
|
+
urls,
|
|
876
|
+
spanType: REQUEST_SPAN_TYPES[start.kind]
|
|
877
|
+
});
|
|
878
|
+
if (!started) return;
|
|
879
|
+
const { span, absoluteUrl } = started;
|
|
880
|
+
let mutated = {};
|
|
881
|
+
try {
|
|
882
|
+
mutated = propagate(span, absoluteUrl, start, urls, tracer);
|
|
883
|
+
} catch {
|
|
884
|
+
mutated = {};
|
|
820
885
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
886
|
+
return {
|
|
887
|
+
...mutated,
|
|
888
|
+
onSettle({ status, error, aborted }) {
|
|
889
|
+
if (aborted) {
|
|
890
|
+
span.setStatus({ code: _flareapp_core.SpanStatusCode.Error });
|
|
891
|
+
span.end();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (error !== void 0) {
|
|
895
|
+
finishHttpSpanError(span, error);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (start.kind === "xhr") {
|
|
899
|
+
endHttpRequestSpan(span, status ?? 0, { zeroIsError: zeroIsError(absoluteUrl) });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
endHttpRequestSpan(span, status ?? 0);
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/breadcrumbs/RequestRecorder.ts
|
|
910
|
+
var RequestRecorder = class {
|
|
911
|
+
type = "browser_request";
|
|
912
|
+
constructor(host) {
|
|
913
|
+
this.host = host;
|
|
914
|
+
this.subscribe = this.subscribe.bind(this);
|
|
915
|
+
this.onStart = this.onStart.bind(this);
|
|
916
|
+
this.onSettle = this.onSettle.bind(this);
|
|
827
917
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
this.lastChildEndTime = span.endTimeUnixNano || this.deps.now();
|
|
831
|
-
if (this.openChildren > 0) return;
|
|
832
|
-
this.clearChildTimeout();
|
|
833
|
-
this.armIdle();
|
|
918
|
+
install() {
|
|
919
|
+
return withRequestPatches(this.subscribe);
|
|
834
920
|
}
|
|
835
|
-
|
|
836
|
-
this.
|
|
837
|
-
if (this.held) return;
|
|
838
|
-
this.idleTimer = this.deps.setTimeout(() => {
|
|
839
|
-
if (this.openChildren > 0) return;
|
|
840
|
-
this.finish(this.trimmedEnd());
|
|
841
|
-
}, this.timeouts.idleTimeout);
|
|
921
|
+
subscribe() {
|
|
922
|
+
return subscribeToRequests(this.onStart);
|
|
842
923
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
924
|
+
onStart(start) {
|
|
925
|
+
const base = browserUrlContext().base();
|
|
926
|
+
const absolute = safeAbsolute(start.url, base);
|
|
927
|
+
if (isFlareIngestUrl(absolute, this.host.config(), base)) return;
|
|
928
|
+
return { onSettle: this.onSettle.bind(this, start, absolute) };
|
|
847
929
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
930
|
+
onSettle(start, absolute, settle) {
|
|
931
|
+
const url = absolute ? absolute.href : start.url;
|
|
932
|
+
const attributes = {
|
|
933
|
+
"http.request.method": start.method,
|
|
934
|
+
"url.full": (0, _flareapp_core.breadcrumbUrl)(url, this.host.config().urlDenylist)
|
|
935
|
+
};
|
|
936
|
+
if (absolute?.hostname) attributes["server.address"] = absolute.hostname;
|
|
937
|
+
if (settle.status !== void 0) attributes["http.response.status_code"] = settle.status;
|
|
938
|
+
this.host.record(REQUEST_SPAN_TYPES[start.kind], attributes, (0, _flareapp_core.defaultNowNano)());
|
|
853
939
|
}
|
|
854
|
-
|
|
855
|
-
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/breadcrumbs/index.ts
|
|
944
|
+
/** Starts every recorder, returns one teardown. A recorder that fails to install is skipped. */
|
|
945
|
+
function startBreadcrumbs(host) {
|
|
946
|
+
if (typeof document === "undefined") return () => {};
|
|
947
|
+
const recorders = [
|
|
948
|
+
new ClickRecorder(host),
|
|
949
|
+
new FormChangeRecorder(host),
|
|
950
|
+
new RequestRecorder(host),
|
|
951
|
+
new NavigationRecorder(host)
|
|
952
|
+
];
|
|
953
|
+
const teardowns = [];
|
|
954
|
+
for (const recorder of recorders) try {
|
|
955
|
+
teardowns.push(recorder.install());
|
|
956
|
+
} catch {}
|
|
957
|
+
return () => {
|
|
958
|
+
for (const teardown of teardowns) try {
|
|
959
|
+
teardown();
|
|
960
|
+
} catch {}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/browser/BrowserFlushScheduler.ts
|
|
966
|
+
var BrowserFlushScheduler = class {
|
|
967
|
+
register(flush) {
|
|
968
|
+
if (typeof document === "undefined" || !document) return;
|
|
969
|
+
document.addEventListener("visibilitychange", () => {
|
|
970
|
+
if (document.visibilityState === "hidden") flush({ keepalive: true });
|
|
971
|
+
});
|
|
972
|
+
if (typeof window !== "undefined" && window) window.addEventListener("pagehide", () => flush({ keepalive: true }));
|
|
856
973
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
//#endregion
|
|
977
|
+
//#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
|
+
function cookie(denylist) {
|
|
983
|
+
if (!window.document.cookie) return {};
|
|
984
|
+
const cookies = Object.create(null);
|
|
985
|
+
window.document.cookie.split("; ").forEach((rawCookie) => {
|
|
986
|
+
const idx = rawCookie.indexOf("=");
|
|
987
|
+
if (idx === -1) {
|
|
988
|
+
cookies[rawCookie] = denylist.test(rawCookie) ? "[redacted]" : "";
|
|
989
|
+
return;
|
|
861
990
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
991
|
+
const name = rawCookie.slice(0, idx);
|
|
992
|
+
const value = rawCookie.slice(idx + 1);
|
|
993
|
+
cookies[name] = denylist.test(name) ? "[redacted]" : value;
|
|
994
|
+
});
|
|
995
|
+
return { "http.request.cookies": cookies };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
//#endregion
|
|
999
|
+
//#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
|
+
function request(urlDenylist, hrefOverride) {
|
|
1006
|
+
return {
|
|
1007
|
+
...(0, _flareapp_core.urlAttributes)(hrefOverride ?? window.location.href, urlDenylist),
|
|
1008
|
+
"user_agent.original": window.navigator.userAgent,
|
|
1009
|
+
"http.request.referrer": (0, _flareapp_core.redactUrlQuery)(window.document.referrer, urlDenylist),
|
|
1010
|
+
"document.ready_state": window.document.readyState
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/browser/context/collectBrowser.ts
|
|
1016
|
+
function browserEntryPoint(config, urlOverride) {
|
|
1017
|
+
if (typeof window === "undefined") return { "flare.entry_point.type": "web" };
|
|
1018
|
+
const attrs = { "flare.entry_point.type": "web" };
|
|
1019
|
+
const href = urlOverride ? urlOverride.href : window?.location?.href;
|
|
1020
|
+
if (href) {
|
|
1021
|
+
attrs["flare.entry_point.value"] = (0, _flareapp_core.redactUrlQuery)(href, config.urlDenylist);
|
|
1022
|
+
const pathname = urlOverride ? urlOverride.pathname : window?.location?.pathname;
|
|
1023
|
+
if (pathname) {
|
|
1024
|
+
attrs["flare.entry_point.handler.identifier"] = pathname;
|
|
1025
|
+
attrs["http.route"] = pathname;
|
|
1026
|
+
attrs["flare.entry_point.handler.type"] = "browser";
|
|
872
1027
|
}
|
|
873
|
-
this.unsubscribe();
|
|
874
|
-
this.deps.beforeEnd?.();
|
|
875
|
-
this.deps.root.end(atTimeNano);
|
|
876
|
-
this.deps.setActiveRoot(void 0);
|
|
877
1028
|
}
|
|
1029
|
+
return attrs;
|
|
1030
|
+
}
|
|
1031
|
+
const collectBrowser = (config) => {
|
|
1032
|
+
const attrs = { ...browserEntryPoint(config) };
|
|
1033
|
+
if (typeof window === "undefined") return attrs;
|
|
1034
|
+
if (window?.location?.hostname) attrs["host.name"] = window.location.hostname;
|
|
1035
|
+
Object.assign(attrs, request(config.urlDenylist));
|
|
1036
|
+
Object.assign(attrs, cookie(config.urlDenylist));
|
|
1037
|
+
return attrs;
|
|
878
1038
|
};
|
|
879
1039
|
|
|
880
1040
|
//#endregion
|
|
881
|
-
//#region src/
|
|
882
|
-
/** The path the address bar is on, or '' outside a browser. */
|
|
883
|
-
function currentPath() {
|
|
884
|
-
return typeof location !== "undefined" ? location.pathname : "";
|
|
885
|
-
}
|
|
1041
|
+
//#region src/browser/FetchFileReader.ts
|
|
886
1042
|
/**
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
1043
|
+
* Fetches source files so the stack-trace builder can render a snippet around the offending line.
|
|
1044
|
+
* Only http(s) is fetched: other schemes (chrome-extension://, file://, blob:, data:) would cross a
|
|
1045
|
+
* privilege boundary or hit a CORS/CSP wall for nothing. Returns null on any failure, never throws.
|
|
890
1046
|
*/
|
|
891
|
-
|
|
1047
|
+
var FetchFileReader = class {
|
|
1048
|
+
read(url) {
|
|
1049
|
+
if (!/^https?:\/\//i.test(url)) return Promise.resolve(null);
|
|
1050
|
+
return fetch(url, internalRequestInit()).then((response) => {
|
|
1051
|
+
if (response.status !== 200) return null;
|
|
1052
|
+
return response.text();
|
|
1053
|
+
}).catch(() => null);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/env/index.ts
|
|
1059
|
+
const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.9.0" : "?";
|
|
1060
|
+
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/tracing/utils/instrumentationGuard.ts
|
|
1063
|
+
/** For a callback the host invokes: a router guard, a store subscriber, ... */
|
|
1064
|
+
function insulate(fn) {
|
|
1065
|
+
return (...args) => {
|
|
1066
|
+
try {
|
|
1067
|
+
fn(...args);
|
|
1068
|
+
} catch {}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/** Invoke a teardown fn now (if present), swallowing any throw. For cleanup chains. */
|
|
1072
|
+
function safeInvoke(fn) {
|
|
892
1073
|
try {
|
|
893
|
-
|
|
894
|
-
if (name) return {
|
|
895
|
-
name,
|
|
896
|
-
source: "route",
|
|
897
|
-
url
|
|
898
|
-
};
|
|
1074
|
+
fn?.();
|
|
899
1075
|
} catch {}
|
|
900
|
-
return {
|
|
901
|
-
name: fallbackPath,
|
|
902
|
-
source: "url",
|
|
903
|
-
url
|
|
904
|
-
};
|
|
905
1076
|
}
|
|
1077
|
+
const instrumented = /* @__PURE__ */ new WeakMap();
|
|
906
1078
|
/**
|
|
907
|
-
* `
|
|
908
|
-
*
|
|
909
|
-
*
|
|
1079
|
+
* Instrument `target` at most once at a time, tearing down any prior instrumentation of the same object
|
|
1080
|
+
* first. Vite HMR re-runs boot code against a router that survives the reload, so without this every
|
|
1081
|
+
* cycle appends another listener set that is never removed. Keyed on the object, so a genuinely new
|
|
1082
|
+
* router is unaffected.
|
|
1083
|
+
*
|
|
1084
|
+
* `install` hands each teardown to `track` as it produces it. A router's own `subscribe` / `on` / guard
|
|
1085
|
+
* registration can throw, and `install` runs during the host's bootstrap, so a throw part-way through
|
|
1086
|
+
* unwinds what already succeeded (newest first) and stops here rather than reaching the host.
|
|
1087
|
+
*
|
|
1088
|
+
* @returns the cleanup, or a no-op when the install failed and already unwound itself.
|
|
910
1089
|
*/
|
|
911
|
-
function
|
|
912
|
-
|
|
1090
|
+
function instrumentOnce(target, install) {
|
|
1091
|
+
instrumented.get(target)?.();
|
|
1092
|
+
const teardowns = [];
|
|
1093
|
+
function unwind() {
|
|
1094
|
+
for (let i = teardowns.length - 1; i >= 0; i--) safeInvoke(teardowns[i]);
|
|
1095
|
+
}
|
|
913
1096
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1097
|
+
install((teardown) => {
|
|
1098
|
+
teardowns.push(teardown);
|
|
1099
|
+
});
|
|
1100
|
+
} catch {
|
|
1101
|
+
unwind();
|
|
1102
|
+
return () => {};
|
|
1103
|
+
}
|
|
1104
|
+
function cleanup() {
|
|
1105
|
+
unwind();
|
|
1106
|
+
if (instrumented.get(target) === cleanup) instrumented.delete(target);
|
|
1107
|
+
}
|
|
1108
|
+
instrumented.set(target, cleanup);
|
|
1109
|
+
return cleanup;
|
|
917
1110
|
}
|
|
918
1111
|
|
|
919
1112
|
//#endregion
|
|
920
|
-
//#region src/
|
|
921
|
-
/** Split out so the timestamp maths is testable without a Navigation Timing entry. */
|
|
922
|
-
function computePageloadStartNano(timeOriginMs, startTimeMs) {
|
|
923
|
-
return Math.round((timeOriginMs + (startTimeMs ?? 0)) * 1e6);
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Choose the pageload root's start time: navigation start while that window is still open,
|
|
927
|
-
* otherwise `now`. Starting at `now` (when tracing began after the final cap, or the pageload was
|
|
928
|
-
* already traced) avoids a backdated root reporting a bogus multi-second duration.
|
|
929
|
-
*/
|
|
930
|
-
function resolvePageloadStartNano(backdatedNano, nowNano, finalTimeoutNano, alreadyTraced) {
|
|
931
|
-
if (alreadyTraced) return nowNano;
|
|
932
|
-
if (nowNano - backdatedNano > finalTimeoutNano) return nowNano;
|
|
933
|
-
return backdatedNano;
|
|
934
|
-
}
|
|
935
|
-
/** The Navigation Timing API, or null where it is missing or only partly implemented. */
|
|
936
|
-
function navigationTiming() {
|
|
937
|
-
const perf = globalThis.performance;
|
|
938
|
-
if (!perf || typeof perf.getEntriesByType !== "function" || typeof perf.timeOrigin !== "number") return null;
|
|
939
|
-
return perf;
|
|
940
|
-
}
|
|
941
|
-
function navigationEntry(perf) {
|
|
942
|
-
return perf.getEntriesByType("navigation")[0];
|
|
943
|
-
}
|
|
1113
|
+
//#region src/browser/context/collectBrowserSpanContext.ts
|
|
944
1114
|
/**
|
|
945
|
-
*
|
|
946
|
-
*
|
|
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.
|
|
947
1122
|
*/
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (!endMs) return nowNano;
|
|
956
|
-
return Math.round((timeOriginMs + endMs) * 1e6);
|
|
1123
|
+
function collectBrowserSpanContext(config, hrefOverride) {
|
|
1124
|
+
if (typeof window === "undefined") return {};
|
|
1125
|
+
const url = absoluteUrl(hrefOverride);
|
|
1126
|
+
return {
|
|
1127
|
+
...browserEntryPoint(config, url),
|
|
1128
|
+
...request(config.urlDenylist, url?.href)
|
|
1129
|
+
};
|
|
957
1130
|
}
|
|
958
1131
|
/**
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
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.
|
|
964
1140
|
*/
|
|
965
|
-
function
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1141
|
+
function browserSpanUrlAttributes(config, href) {
|
|
1142
|
+
if (typeof window === "undefined") return {};
|
|
1143
|
+
const resolved = absoluteUrl(href);
|
|
1144
|
+
if (!resolved) return {};
|
|
1145
|
+
const attributes = (0, _flareapp_core.urlAttributes)(resolved.href, config.urlDenylist);
|
|
1146
|
+
return {
|
|
1147
|
+
"url.query": "",
|
|
1148
|
+
...attributes,
|
|
1149
|
+
"flare.entry_point.value": attributes["url.full"]
|
|
1150
|
+
};
|
|
970
1151
|
}
|
|
971
1152
|
|
|
972
1153
|
//#endregion
|
|
973
|
-
//#region src/tracing/webvitals/lib/bfcache.ts
|
|
1154
|
+
//#region src/tracing/vitals/webvitals/lib/bfcache.ts
|
|
974
1155
|
let bfcacheRestoreTime = -1;
|
|
975
1156
|
const getBFCacheRestoreTime = () => bfcacheRestoreTime;
|
|
976
1157
|
const onBFCacheRestore = (cb) => {
|
|
@@ -983,7 +1164,7 @@ const onBFCacheRestore = (cb) => {
|
|
|
983
1164
|
};
|
|
984
1165
|
|
|
985
1166
|
//#endregion
|
|
986
|
-
//#region src/tracing/webvitals/lib/bindReporter.ts
|
|
1167
|
+
//#region src/tracing/vitals/webvitals/lib/bindReporter.ts
|
|
987
1168
|
const getRating = (value, thresholds) => {
|
|
988
1169
|
if (value > thresholds[1]) return "poor";
|
|
989
1170
|
if (value > thresholds[0]) return "needs-improvement";
|
|
@@ -1008,26 +1189,26 @@ const bindReporter = (callback, metric, thresholds, reportAllChanges) => {
|
|
|
1008
1189
|
};
|
|
1009
1190
|
|
|
1010
1191
|
//#endregion
|
|
1011
|
-
//#region src/tracing/webvitals/lib/doubleRAF.ts
|
|
1192
|
+
//#region src/tracing/vitals/webvitals/lib/doubleRAF.ts
|
|
1012
1193
|
const doubleRAF = (cb) => {
|
|
1013
1194
|
requestAnimationFrame(() => requestAnimationFrame(cb));
|
|
1014
1195
|
};
|
|
1015
1196
|
|
|
1016
1197
|
//#endregion
|
|
1017
|
-
//#region src/tracing/webvitals/lib/getNavigationEntry.ts
|
|
1198
|
+
//#region src/tracing/vitals/webvitals/lib/getNavigationEntry.ts
|
|
1018
1199
|
const getNavigationEntry = () => {
|
|
1019
1200
|
const navigationEntry = performance.getEntriesByType("navigation")[0];
|
|
1020
1201
|
if (navigationEntry && navigationEntry.responseStart > 0 && navigationEntry.responseStart < performance.now()) return navigationEntry;
|
|
1021
1202
|
};
|
|
1022
1203
|
|
|
1023
1204
|
//#endregion
|
|
1024
|
-
//#region src/tracing/webvitals/lib/getActivationStart.ts
|
|
1205
|
+
//#region src/tracing/vitals/webvitals/lib/getActivationStart.ts
|
|
1025
1206
|
const getActivationStart = () => {
|
|
1026
1207
|
return getNavigationEntry()?.activationStart ?? 0;
|
|
1027
1208
|
};
|
|
1028
1209
|
|
|
1029
1210
|
//#endregion
|
|
1030
|
-
//#region src/tracing/webvitals/lib/getVisibilityWatcher.ts
|
|
1211
|
+
//#region src/tracing/vitals/webvitals/lib/getVisibilityWatcher.ts
|
|
1031
1212
|
let firstHiddenTime = -1;
|
|
1032
1213
|
const onHiddenFunctions = /* @__PURE__ */ new Set();
|
|
1033
1214
|
const initHiddenTime = () => {
|
|
@@ -1066,7 +1247,7 @@ const getVisibilityWatcher = (reset = false) => {
|
|
|
1066
1247
|
};
|
|
1067
1248
|
|
|
1068
1249
|
//#endregion
|
|
1069
|
-
//#region src/tracing/webvitals/lib/generateUniqueID.ts
|
|
1250
|
+
//#region src/tracing/vitals/webvitals/lib/generateUniqueID.ts
|
|
1070
1251
|
/**
|
|
1071
1252
|
* Performantly generate a unique, 30-char string by combining a version
|
|
1072
1253
|
* number, the current timestamp with a 13-digit number integer.
|
|
@@ -1077,7 +1258,7 @@ const generateUniqueID = () => {
|
|
|
1077
1258
|
};
|
|
1078
1259
|
|
|
1079
1260
|
//#endregion
|
|
1080
|
-
//#region src/tracing/webvitals/lib/initMetric.ts
|
|
1261
|
+
//#region src/tracing/vitals/webvitals/lib/initMetric.ts
|
|
1081
1262
|
const initMetric = (name, value = -1, navigationType, navigationId = 0, navigationInteractionId, navigationURL, navigationStartTime) => {
|
|
1082
1263
|
const hardNavEntry = getNavigationEntry();
|
|
1083
1264
|
const hardNavId = hardNavEntry?.navigationId || 0;
|
|
@@ -1105,7 +1286,7 @@ const initMetric = (name, value = -1, navigationType, navigationId = 0, navigati
|
|
|
1105
1286
|
};
|
|
1106
1287
|
|
|
1107
1288
|
//#endregion
|
|
1108
|
-
//#region src/tracing/webvitals/lib/initUnique.ts
|
|
1289
|
+
//#region src/tracing/vitals/webvitals/lib/initUnique.ts
|
|
1109
1290
|
const instanceMap = /* @__PURE__ */ new WeakMap();
|
|
1110
1291
|
/**
|
|
1111
1292
|
* A function that accepts and identity object and a class object and returns
|
|
@@ -1123,7 +1304,7 @@ function initUnique(identityObj, ClassObj) {
|
|
|
1123
1304
|
}
|
|
1124
1305
|
|
|
1125
1306
|
//#endregion
|
|
1126
|
-
//#region src/tracing/webvitals/lib/LayoutShiftManager.ts
|
|
1307
|
+
//#region src/tracing/vitals/webvitals/lib/LayoutShiftManager.ts
|
|
1127
1308
|
var LayoutShiftManager = class {
|
|
1128
1309
|
_onAfterProcessingUnexpectedShift;
|
|
1129
1310
|
_sessionValue = 0;
|
|
@@ -1144,7 +1325,7 @@ var LayoutShiftManager = class {
|
|
|
1144
1325
|
};
|
|
1145
1326
|
|
|
1146
1327
|
//#endregion
|
|
1147
|
-
//#region src/tracing/webvitals/lib/observe.ts
|
|
1328
|
+
//#region src/tracing/vitals/webvitals/lib/observe.ts
|
|
1148
1329
|
/**
|
|
1149
1330
|
* Takes a performance entry type and a callback function, and creates a
|
|
1150
1331
|
* `PerformanceObserver` instance that will observe the specified entry type
|
|
@@ -1177,7 +1358,7 @@ const observe = (types, callback, opts = {}) => {
|
|
|
1177
1358
|
};
|
|
1178
1359
|
|
|
1179
1360
|
//#endregion
|
|
1180
|
-
//#region src/tracing/webvitals/lib/softNavs.ts
|
|
1361
|
+
//#region src/tracing/vitals/webvitals/lib/softNavs.ts
|
|
1181
1362
|
const checkSoftNavsEnabled = (opts) => {
|
|
1182
1363
|
return globalThis.PerformanceObserver?.supportedEntryTypes.includes("soft-navigation") && typeof globalThis.PerformanceSoftNavigation?.prototype?.getLargestInteractionContentfulPaint === "function" && opts && opts.reportSoftNavs;
|
|
1183
1364
|
};
|
|
@@ -1190,7 +1371,7 @@ const storeSoftNavEntry = (map, entry) => {
|
|
|
1190
1371
|
};
|
|
1191
1372
|
|
|
1192
1373
|
//#endregion
|
|
1193
|
-
//#region src/tracing/webvitals/lib/runOnce.ts
|
|
1374
|
+
//#region src/tracing/vitals/webvitals/lib/runOnce.ts
|
|
1194
1375
|
const runOnce = (cb) => {
|
|
1195
1376
|
let called = false;
|
|
1196
1377
|
return () => {
|
|
@@ -1202,20 +1383,20 @@ const runOnce = (cb) => {
|
|
|
1202
1383
|
};
|
|
1203
1384
|
|
|
1204
1385
|
//#endregion
|
|
1205
|
-
//#region src/tracing/webvitals/lib/FCPEntryManager.ts
|
|
1386
|
+
//#region src/tracing/vitals/webvitals/lib/FCPEntryManager.ts
|
|
1206
1387
|
var FCPEntryManager = class {
|
|
1207
1388
|
_softNavigationEntryMap;
|
|
1208
1389
|
};
|
|
1209
1390
|
|
|
1210
1391
|
//#endregion
|
|
1211
|
-
//#region src/tracing/webvitals/lib/whenActivated.ts
|
|
1392
|
+
//#region src/tracing/vitals/webvitals/lib/whenActivated.ts
|
|
1212
1393
|
const whenActivated = (callback) => {
|
|
1213
1394
|
if (document.prerendering) addEventListener("prerenderingchange", callback, true);
|
|
1214
1395
|
else callback();
|
|
1215
1396
|
};
|
|
1216
1397
|
|
|
1217
1398
|
//#endregion
|
|
1218
|
-
//#region src/tracing/webvitals/onFCP.ts
|
|
1399
|
+
//#region src/tracing/vitals/webvitals/onFCP.ts
|
|
1219
1400
|
/** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */
|
|
1220
1401
|
const FCPThresholds = [1800, 3e3];
|
|
1221
1402
|
/**
|
|
@@ -1269,7 +1450,7 @@ const onFCP = (onReport, opts = {}) => {
|
|
|
1269
1450
|
};
|
|
1270
1451
|
|
|
1271
1452
|
//#endregion
|
|
1272
|
-
//#region src/tracing/webvitals/onCLS.ts
|
|
1453
|
+
//#region src/tracing/vitals/webvitals/onCLS.ts
|
|
1273
1454
|
/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
|
|
1274
1455
|
const CLSThresholds = [.1, .25];
|
|
1275
1456
|
/**
|
|
@@ -1344,7 +1525,7 @@ const onCLS = (onReport, opts = {}) => {
|
|
|
1344
1525
|
};
|
|
1345
1526
|
|
|
1346
1527
|
//#endregion
|
|
1347
|
-
//#region src/tracing/webvitals/lib/polyfills/interactionCountPolyfill.ts
|
|
1528
|
+
//#region src/tracing/vitals/webvitals/lib/polyfills/interactionCountPolyfill.ts
|
|
1348
1529
|
let interactionCountEstimate = 0;
|
|
1349
1530
|
let minKnownInteractionId = Infinity;
|
|
1350
1531
|
let maxKnownInteractionId = 0;
|
|
@@ -1372,7 +1553,7 @@ const initInteractionCountPolyfill = () => {
|
|
|
1372
1553
|
};
|
|
1373
1554
|
|
|
1374
1555
|
//#endregion
|
|
1375
|
-
//#region src/tracing/webvitals/lib/InteractionManager.ts
|
|
1556
|
+
//#region src/tracing/vitals/webvitals/lib/InteractionManager.ts
|
|
1376
1557
|
const MAX_INTERACTIONS_TO_CONSIDER = 10;
|
|
1377
1558
|
let prevInteractionCount = 0;
|
|
1378
1559
|
/**
|
|
@@ -1452,7 +1633,7 @@ var InteractionManager = class {
|
|
|
1452
1633
|
};
|
|
1453
1634
|
|
|
1454
1635
|
//#endregion
|
|
1455
|
-
//#region src/tracing/webvitals/lib/whenIdleOrHidden.ts
|
|
1636
|
+
//#region src/tracing/vitals/webvitals/lib/whenIdleOrHidden.ts
|
|
1456
1637
|
/**
|
|
1457
1638
|
* Runs the passed callback during the next idle period, or immediately
|
|
1458
1639
|
* if the browser's visibility state is (or becomes) hidden.
|
|
@@ -1481,7 +1662,7 @@ const whenIdleOrHidden = (cb) => {
|
|
|
1481
1662
|
};
|
|
1482
1663
|
|
|
1483
1664
|
//#endregion
|
|
1484
|
-
//#region src/tracing/webvitals/onINP.ts
|
|
1665
|
+
//#region src/tracing/vitals/webvitals/onINP.ts
|
|
1485
1666
|
/** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */
|
|
1486
1667
|
const INPThresholds = [200, 500];
|
|
1487
1668
|
const DEFAULT_DURATION_THRESHOLD = 40;
|
|
@@ -1572,7 +1753,7 @@ const onINP = (onReport, opts = {}) => {
|
|
|
1572
1753
|
};
|
|
1573
1754
|
|
|
1574
1755
|
//#endregion
|
|
1575
|
-
//#region src/tracing/webvitals/lib/LCPEntryManager.ts
|
|
1756
|
+
//#region src/tracing/vitals/webvitals/lib/LCPEntryManager.ts
|
|
1576
1757
|
var LCPEntryManager = class {
|
|
1577
1758
|
_onBeforeProcessingEntry;
|
|
1578
1759
|
_softNavigationEntryMap;
|
|
@@ -1582,7 +1763,7 @@ var LCPEntryManager = class {
|
|
|
1582
1763
|
};
|
|
1583
1764
|
|
|
1584
1765
|
//#endregion
|
|
1585
|
-
//#region src/tracing/webvitals/onLCP.ts
|
|
1766
|
+
//#region src/tracing/vitals/webvitals/onLCP.ts
|
|
1586
1767
|
/** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */
|
|
1587
1768
|
const LCPThresholds = [2500, 4e3];
|
|
1588
1769
|
/**
|
|
@@ -1692,7 +1873,7 @@ const onLCP = (onReport, opts = {}) => {
|
|
|
1692
1873
|
};
|
|
1693
1874
|
|
|
1694
1875
|
//#endregion
|
|
1695
|
-
//#region src/tracing/webvitals/onTTFB.ts
|
|
1876
|
+
//#region src/tracing/vitals/webvitals/onTTFB.ts
|
|
1696
1877
|
/** Thresholds for TTFB. See https://web.dev/articles/ttfb#what_is_a_good_ttfb_score */
|
|
1697
1878
|
const TTFBThresholds = [800, 1800];
|
|
1698
1879
|
/**
|
|
@@ -1753,7 +1934,7 @@ const onTTFB = (onReport, opts = {}) => {
|
|
|
1753
1934
|
};
|
|
1754
1935
|
|
|
1755
1936
|
//#endregion
|
|
1756
|
-
//#region src/tracing/webVitals.ts
|
|
1937
|
+
//#region src/tracing/vitals/webVitals.ts
|
|
1757
1938
|
/**
|
|
1758
1939
|
* Final the moment they first report, so they can ride the pageload span itself. The other three keep
|
|
1759
1940
|
* changing until the page goes away, and stamping an early value on the root would leave the root and
|
|
@@ -1829,57 +2010,230 @@ function record(name, metric) {
|
|
|
1829
2010
|
collected[name] = metric.value;
|
|
1830
2011
|
}
|
|
1831
2012
|
/**
|
|
1832
|
-
* Stops recording and drops what was collected. The observers themselves cannot be detached, and both
|
|
1833
|
-
* the `subscribed` and `taken` latches deliberately survive: clearing `subscribed` would let a re-enable
|
|
1834
|
-
* attach a second set of observers that is just as undetachable as the first, and clearing `taken` would
|
|
1835
|
-
* let those surviving observers refill `collected` and ship a second `browser_web_vital` for the same
|
|
1836
|
-
* document once the page is re-enabled and later hidden.
|
|
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
|
+
function stopWebVitals() {
|
|
2020
|
+
recording = false;
|
|
2021
|
+
collected = {};
|
|
2022
|
+
}
|
|
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
|
+
function takeEarlyVitals() {
|
|
2028
|
+
if (!recording) return null;
|
|
2029
|
+
const taking = {};
|
|
2030
|
+
for (const name of EARLY_VITALS) {
|
|
2031
|
+
const value = collected[name];
|
|
2032
|
+
if (value !== void 0) {
|
|
2033
|
+
taking[name] = value;
|
|
2034
|
+
delete collected[name];
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return Object.keys(taking).length === 0 ? null : taking;
|
|
2038
|
+
}
|
|
2039
|
+
/** Everything still outstanding, once. Null afterwards, and null when nothing is left. */
|
|
2040
|
+
function takeWebVitals() {
|
|
2041
|
+
if (taken || !recording || Object.keys(collected).length === 0) return null;
|
|
2042
|
+
taken = true;
|
|
2043
|
+
const taking = collected;
|
|
2044
|
+
collected = {};
|
|
2045
|
+
return taking;
|
|
2046
|
+
}
|
|
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
|
+
function restoreWebVitals(vitals) {
|
|
2053
|
+
collected = vitals;
|
|
2054
|
+
taken = false;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
//#endregion
|
|
2058
|
+
//#region src/tracing/roots/IdleRootController.ts
|
|
2059
|
+
/** Browser defaults for the three idle-root timeouts, in ms. Overridable per Config. */
|
|
2060
|
+
const DEFAULT_IDLE_TIMEOUTS = {
|
|
2061
|
+
idleTimeout: 1e3,
|
|
2062
|
+
finalTimeout: 3e4,
|
|
2063
|
+
childSpanTimeout: 15e3
|
|
2064
|
+
};
|
|
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
|
+
var IdleRootController = class {
|
|
2071
|
+
openChildren = 0;
|
|
2072
|
+
lastChildEndTime = null;
|
|
2073
|
+
settleTime = null;
|
|
2074
|
+
idleTimer = null;
|
|
2075
|
+
finalTimer = null;
|
|
2076
|
+
childTimer = null;
|
|
2077
|
+
ended = false;
|
|
2078
|
+
held = false;
|
|
2079
|
+
unsubscribe;
|
|
2080
|
+
constructor(deps, timeouts) {
|
|
2081
|
+
this.deps = deps;
|
|
2082
|
+
this.timeouts = timeouts;
|
|
2083
|
+
deps.setActiveRoot(deps.root);
|
|
2084
|
+
this.unsubscribe = deps.addSpanListener((e) => this.onSpanEvent(e.phase, e.span));
|
|
2085
|
+
const elapsedMs = Math.max(0, (deps.now() - deps.rootStartTime) / 1e6);
|
|
2086
|
+
const remainingMs = Math.max(0, timeouts.finalTimeout - elapsedMs);
|
|
2087
|
+
this.finalTimer = deps.setTimeout(() => this.finish(deps.now()), remainingMs);
|
|
2088
|
+
this.held = !!deps.held;
|
|
2089
|
+
this.armIdle();
|
|
2090
|
+
}
|
|
2091
|
+
get isEnded() {
|
|
2092
|
+
return this.ended;
|
|
2093
|
+
}
|
|
2094
|
+
/** For a route change or pagehide. */
|
|
2095
|
+
endNow() {
|
|
2096
|
+
this.finish(this.openChildren > 0 || this.held ? this.deps.now() : this.trimmedEnd());
|
|
2097
|
+
}
|
|
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
|
+
releaseHold() {
|
|
2106
|
+
if (this.ended || !this.held) return;
|
|
2107
|
+
this.held = false;
|
|
2108
|
+
if (this.openChildren === 0) this.settleTime = this.deps.now();
|
|
2109
|
+
this.armIdle();
|
|
2110
|
+
}
|
|
2111
|
+
onSpanEvent(phase, span) {
|
|
2112
|
+
if (this.ended) return;
|
|
2113
|
+
if (span === this.deps.root) return;
|
|
2114
|
+
if (span.traceId !== this.deps.root.traceId) return;
|
|
2115
|
+
if (phase === "start") {
|
|
2116
|
+
this.onChildStarted();
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
this.onChildEnded(span);
|
|
2120
|
+
}
|
|
2121
|
+
onChildStarted() {
|
|
2122
|
+
this.openChildren++;
|
|
2123
|
+
this.clearIdle();
|
|
2124
|
+
if (this.openChildren === 1) this.armChildTimeout();
|
|
2125
|
+
}
|
|
2126
|
+
onChildEnded(span) {
|
|
2127
|
+
this.openChildren = Math.max(0, this.openChildren - 1);
|
|
2128
|
+
this.lastChildEndTime = span.endTimeUnixNano || this.deps.now();
|
|
2129
|
+
if (this.openChildren > 0) return;
|
|
2130
|
+
this.clearChildTimeout();
|
|
2131
|
+
this.armIdle();
|
|
2132
|
+
}
|
|
2133
|
+
armIdle() {
|
|
2134
|
+
this.clearIdle();
|
|
2135
|
+
if (this.held) return;
|
|
2136
|
+
this.idleTimer = this.deps.setTimeout(() => {
|
|
2137
|
+
if (this.openChildren > 0) return;
|
|
2138
|
+
this.finish(this.trimmedEnd());
|
|
2139
|
+
}, this.timeouts.idleTimeout);
|
|
2140
|
+
}
|
|
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
|
+
trimmedEnd() {
|
|
2144
|
+
return Math.max(this.deps.endFloor(), this.settleTime ?? 0, this.lastChildEndTime ?? 0);
|
|
2145
|
+
}
|
|
2146
|
+
clearIdle() {
|
|
2147
|
+
if (this.idleTimer !== null) {
|
|
2148
|
+
this.deps.clearTimeout(this.idleTimer);
|
|
2149
|
+
this.idleTimer = null;
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
armChildTimeout() {
|
|
2153
|
+
this.childTimer = this.deps.setTimeout(() => this.finish(this.deps.now()), this.timeouts.childSpanTimeout);
|
|
2154
|
+
}
|
|
2155
|
+
clearChildTimeout() {
|
|
2156
|
+
if (this.childTimer !== null) {
|
|
2157
|
+
this.deps.clearTimeout(this.childTimer);
|
|
2158
|
+
this.childTimer = null;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
finish(atTimeNano) {
|
|
2162
|
+
if (this.ended) return;
|
|
2163
|
+
this.ended = true;
|
|
2164
|
+
atTimeNano = Math.max(atTimeNano, this.deps.rootStartTime);
|
|
2165
|
+
this.clearIdle();
|
|
2166
|
+
this.clearChildTimeout();
|
|
2167
|
+
if (this.finalTimer !== null) {
|
|
2168
|
+
this.deps.clearTimeout(this.finalTimer);
|
|
2169
|
+
this.finalTimer = null;
|
|
2170
|
+
}
|
|
2171
|
+
this.unsubscribe();
|
|
2172
|
+
this.deps.beforeEnd?.();
|
|
2173
|
+
this.deps.root.end(atTimeNano);
|
|
2174
|
+
this.deps.setActiveRoot(void 0);
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
|
|
2178
|
+
//#endregion
|
|
2179
|
+
//#region src/tracing/roots/navigationTiming.ts
|
|
2180
|
+
/** Split out so the timestamp maths is testable without a Navigation Timing entry. */
|
|
2181
|
+
function computePageloadStartNano(timeOriginMs, startTimeMs) {
|
|
2182
|
+
return Math.round((timeOriginMs + (startTimeMs ?? 0)) * 1e6);
|
|
2183
|
+
}
|
|
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.
|
|
1837
2188
|
*/
|
|
1838
|
-
function
|
|
1839
|
-
|
|
1840
|
-
|
|
2189
|
+
function resolvePageloadStartNano(backdatedNano, nowNano, finalTimeoutNano, alreadyTraced) {
|
|
2190
|
+
if (alreadyTraced) return nowNano;
|
|
2191
|
+
if (nowNano - backdatedNano > finalTimeoutNano) return nowNano;
|
|
2192
|
+
return backdatedNano;
|
|
2193
|
+
}
|
|
2194
|
+
/** The Navigation Timing API, or null where it is missing or only partly implemented. */
|
|
2195
|
+
function navigationTiming() {
|
|
2196
|
+
const perf = globalThis.performance;
|
|
2197
|
+
if (!perf || typeof perf.getEntriesByType !== "function" || typeof perf.timeOrigin !== "number") return null;
|
|
2198
|
+
return perf;
|
|
2199
|
+
}
|
|
2200
|
+
function navigationEntry(perf) {
|
|
2201
|
+
return perf.getEntriesByType("navigation")[0];
|
|
1841
2202
|
}
|
|
1842
2203
|
/**
|
|
1843
|
-
* The
|
|
1844
|
-
*
|
|
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.
|
|
1845
2206
|
*/
|
|
1846
|
-
function
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
const value = collected[name];
|
|
1851
|
-
if (value !== void 0) {
|
|
1852
|
-
taking[name] = value;
|
|
1853
|
-
delete collected[name];
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
return Object.keys(taking).length === 0 ? null : taking;
|
|
2207
|
+
function pageloadStartNano() {
|
|
2208
|
+
const perf = navigationTiming();
|
|
2209
|
+
if (!perf) return (0, _flareapp_core.defaultNowNano)();
|
|
2210
|
+
return computePageloadStartNano(perf.timeOrigin, navigationEntry(perf)?.startTime);
|
|
1857
2211
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
if (
|
|
1861
|
-
|
|
1862
|
-
const taking = collected;
|
|
1863
|
-
collected = {};
|
|
1864
|
-
return taking;
|
|
2212
|
+
function computePageloadEndNano(timeOriginMs, loadEventEndMs, domContentLoadedEventEndMs, nowNano) {
|
|
2213
|
+
const endMs = loadEventEndMs || domContentLoadedEventEndMs || 0;
|
|
2214
|
+
if (!endMs) return nowNano;
|
|
2215
|
+
return Math.round((timeOriginMs + endMs) * 1e6);
|
|
1865
2216
|
}
|
|
1866
2217
|
/**
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
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.
|
|
1870
2223
|
*/
|
|
1871
|
-
function
|
|
1872
|
-
|
|
1873
|
-
|
|
2224
|
+
function pageloadEndNano() {
|
|
2225
|
+
const perf = navigationTiming();
|
|
2226
|
+
if (!perf) return (0, _flareapp_core.defaultNowNano)();
|
|
2227
|
+
const entry = navigationEntry(perf);
|
|
2228
|
+
return computePageloadEndNano(perf.timeOrigin, entry?.loadEventEnd, entry?.domContentLoadedEventEnd, (0, _flareapp_core.defaultNowNano)());
|
|
1874
2229
|
}
|
|
1875
2230
|
|
|
1876
2231
|
//#endregion
|
|
1877
|
-
//#region src/tracing/browserTracing.ts
|
|
2232
|
+
//#region src/tracing/roots/browserTracing.ts
|
|
1878
2233
|
let controller = null;
|
|
1879
2234
|
let uninstall = null;
|
|
1880
|
-
let
|
|
2235
|
+
let removeNavigationSubscription = null;
|
|
1881
2236
|
let pageloadTraced = false;
|
|
1882
|
-
let navSource = null;
|
|
1883
2237
|
let activeFlare = null;
|
|
1884
2238
|
let currentRoot = null;
|
|
1885
2239
|
let pendingRouteName = null;
|
|
@@ -1963,16 +2317,14 @@ function startRoot(flare, options) {
|
|
|
1963
2317
|
if (flare.config.debug) console.error("Flare: failed to start browser tracing root", error);
|
|
1964
2318
|
}
|
|
1965
2319
|
}
|
|
1966
|
-
function
|
|
1967
|
-
const path = location.pathname;
|
|
1968
|
-
if (path === lastPath) return;
|
|
1969
|
-
lastPath = path;
|
|
1970
|
-
if (navSource) return;
|
|
2320
|
+
function openNavigationRoot(flare, opts) {
|
|
1971
2321
|
withLiveController((live) => live.endNow());
|
|
1972
2322
|
startRoot(flare, {
|
|
1973
2323
|
spanType: _flareapp_core.BrowserSpanType.Navigation,
|
|
1974
2324
|
startTimeUnixNano: (0, _flareapp_core.defaultNowNano)(),
|
|
1975
|
-
name: path
|
|
2325
|
+
name: opts.path,
|
|
2326
|
+
urlOverride: opts.url,
|
|
2327
|
+
hold: opts.hold
|
|
1976
2328
|
});
|
|
1977
2329
|
}
|
|
1978
2330
|
/**
|
|
@@ -2037,7 +2389,20 @@ function startBrowserTracing(flare) {
|
|
|
2037
2389
|
if (typeof window === "undefined" || typeof history === "undefined" || typeof location === "undefined") return;
|
|
2038
2390
|
if (uninstall) return;
|
|
2039
2391
|
activeFlare = flare;
|
|
2040
|
-
|
|
2392
|
+
removeNavigationSubscription = subscribeToNavigation({
|
|
2393
|
+
onUrlChanged: (path) => openNavigationRoot(flare, { path }),
|
|
2394
|
+
onNavigationStart: (opts) => openNavigationRoot(flare, opts),
|
|
2395
|
+
onRouteName: (route, owner) => applyRouteName(route, owner),
|
|
2396
|
+
onNavigationSettle: (route, owner) => {
|
|
2397
|
+
applyRouteName(route, owner);
|
|
2398
|
+
withLiveController((live) => live.releaseHold());
|
|
2399
|
+
},
|
|
2400
|
+
onSourceUnregistered: () => {
|
|
2401
|
+
withLiveController((live) => live.releaseHold());
|
|
2402
|
+
pendingRouteName = null;
|
|
2403
|
+
pendingRouteNameOwner = null;
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2041
2406
|
const finalTimeoutNano = resolveTimeouts(flare.config).finalTimeout * 1e6;
|
|
2042
2407
|
const navigationStart = pageloadStartNano();
|
|
2043
2408
|
const pageloadStart = resolvePageloadStartNano(navigationStart, (0, _flareapp_core.defaultNowNano)(), finalTimeoutNano, pageloadTraced);
|
|
@@ -2053,26 +2418,8 @@ function startBrowserTracing(flare) {
|
|
|
2053
2418
|
const owner = pendingRouteNameOwner;
|
|
2054
2419
|
pendingRouteName = null;
|
|
2055
2420
|
pendingRouteNameOwner = null;
|
|
2056
|
-
if (owner
|
|
2421
|
+
if (isActiveNavigationSource(owner)) applyRouteName(route);
|
|
2057
2422
|
}
|
|
2058
|
-
const handle = () => {
|
|
2059
|
-
if (!uninstall) return;
|
|
2060
|
-
try {
|
|
2061
|
-
onUrlChanged(flare);
|
|
2062
|
-
} catch (error) {
|
|
2063
|
-
if (flare.config.debug) console.error("Flare: browser tracing navigation handler failed", error);
|
|
2064
|
-
}
|
|
2065
|
-
};
|
|
2066
|
-
function wrapHistoryMethod(original) {
|
|
2067
|
-
return function(...args) {
|
|
2068
|
-
const result = original.apply(this, args);
|
|
2069
|
-
handle();
|
|
2070
|
-
return result;
|
|
2071
|
-
};
|
|
2072
|
-
}
|
|
2073
|
-
fill(history, "pushState", wrapHistoryMethod);
|
|
2074
|
-
fill(history, "replaceState", wrapHistoryMethod);
|
|
2075
|
-
window.addEventListener("popstate", handle);
|
|
2076
2423
|
function endRootAndFlush() {
|
|
2077
2424
|
if (controller && !controller.isEnded) try {
|
|
2078
2425
|
controller.endNow();
|
|
@@ -2093,9 +2440,6 @@ function startBrowserTracing(flare) {
|
|
|
2093
2440
|
window.addEventListener("pagehide", onPageHide);
|
|
2094
2441
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2095
2442
|
uninstall = () => {
|
|
2096
|
-
unfill(history, "pushState");
|
|
2097
|
-
unfill(history, "replaceState");
|
|
2098
|
-
window.removeEventListener("popstate", handle);
|
|
2099
2443
|
window.removeEventListener("pagehide", onPageHide);
|
|
2100
2444
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2101
2445
|
};
|
|
@@ -2108,11 +2452,12 @@ function stopBrowserTracing() {
|
|
|
2108
2452
|
uninstall();
|
|
2109
2453
|
uninstall = null;
|
|
2110
2454
|
}
|
|
2455
|
+
removeNavigationSubscription?.();
|
|
2456
|
+
removeNavigationSubscription = null;
|
|
2111
2457
|
activeFlare = null;
|
|
2112
2458
|
currentRoot = null;
|
|
2113
2459
|
pendingRouteName = null;
|
|
2114
2460
|
pendingRouteNameOwner = null;
|
|
2115
|
-
lastPath = "";
|
|
2116
2461
|
stopWebVitals();
|
|
2117
2462
|
pageloadRoot = null;
|
|
2118
2463
|
pageloadRootStartNano = 0;
|
|
@@ -2163,54 +2508,70 @@ function applyRouteName(route, owner) {
|
|
|
2163
2508
|
...urlAttrs
|
|
2164
2509
|
};
|
|
2165
2510
|
}
|
|
2511
|
+
function activeTracingFlare() {
|
|
2512
|
+
return activeFlare;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
//#endregion
|
|
2516
|
+
//#region src/tracing/roots/componentProfiler.ts
|
|
2517
|
+
/** Unix nanos on the same clock the tracer uses for span timestamps. */
|
|
2518
|
+
const nowNano = _flareapp_core.defaultNowNano;
|
|
2166
2519
|
/**
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2169
|
-
* cannot tear down a newer registration.
|
|
2520
|
+
* Reserved up front so descendants can point at a span before it is recorded. Null when the trace is at
|
|
2521
|
+
* its span cap: descendants record before this span does, so an id the cap will refuse orphans them.
|
|
2170
2522
|
*/
|
|
2171
|
-
function
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2523
|
+
function reserveSpanId(traceId) {
|
|
2524
|
+
if (traceId !== void 0 && !activeTracingFlare()?.tracer.claimSpanSlot(traceId)) return null;
|
|
2525
|
+
return (0, _flareapp_core.spanId)();
|
|
2526
|
+
}
|
|
2527
|
+
/** The root a top-level component nests under. Null when tracing is off or no root is recording. */
|
|
2528
|
+
function activeComponentRoot() {
|
|
2529
|
+
try {
|
|
2530
|
+
const root = activeTracingFlare()?.tracer.getActiveSpan();
|
|
2531
|
+
if (!root || !root.isRecording) return null;
|
|
2532
|
+
return {
|
|
2533
|
+
traceId: root.traceId,
|
|
2534
|
+
parentSpanId: root.spanId
|
|
2535
|
+
};
|
|
2536
|
+
} catch {
|
|
2537
|
+
return null;
|
|
2177
2538
|
}
|
|
2178
|
-
return {
|
|
2179
|
-
startNavigation(opts) {
|
|
2180
|
-
if (!active() || !activeFlare) return;
|
|
2181
|
-
const path = opts?.path ?? currentPath();
|
|
2182
|
-
lastPath = path;
|
|
2183
|
-
withLiveController((live) => live.endNow());
|
|
2184
|
-
startRoot(activeFlare, {
|
|
2185
|
-
spanType: _flareapp_core.BrowserSpanType.Navigation,
|
|
2186
|
-
startTimeUnixNano: (0, _flareapp_core.defaultNowNano)(),
|
|
2187
|
-
name: path,
|
|
2188
|
-
urlOverride: opts?.url,
|
|
2189
|
-
hold: opts?.hold
|
|
2190
|
-
});
|
|
2191
|
-
},
|
|
2192
|
-
setActiveRouteName(route) {
|
|
2193
|
-
if (!active()) return;
|
|
2194
|
-
applyRouteName(route, token);
|
|
2195
|
-
},
|
|
2196
|
-
settleNavigation(route) {
|
|
2197
|
-
if (!active()) return;
|
|
2198
|
-
applyRouteName(route, token);
|
|
2199
|
-
withLiveController((live) => live.releaseHold());
|
|
2200
|
-
},
|
|
2201
|
-
unregister() {
|
|
2202
|
-
if (!active()) return;
|
|
2203
|
-
withLiveController((live) => live.releaseHold());
|
|
2204
|
-
navSource = null;
|
|
2205
|
-
lastPath = currentPath();
|
|
2206
|
-
pendingRouteName = null;
|
|
2207
|
-
pendingRouteNameOwner = null;
|
|
2208
|
-
}
|
|
2209
|
-
};
|
|
2210
2539
|
}
|
|
2211
|
-
/**
|
|
2212
|
-
|
|
2213
|
-
|
|
2540
|
+
/**
|
|
2541
|
+
* An ancestor's context is only usable while it still belongs to the live trace. A profiled component
|
|
2542
|
+
* that survives a navigation (a layout around a swapped page body) froze its context under the pageload
|
|
2543
|
+
* trace, and `recordComponentSpan` would drop anything pointing at that closed root.
|
|
2544
|
+
*/
|
|
2545
|
+
function resolveComponentParent(inherited, live) {
|
|
2546
|
+
if (inherited && live && inherited.traceId === live.traceId) return inherited;
|
|
2547
|
+
return live;
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Records only while the reserved root is still the live recording root, and drops the span otherwise.
|
|
2551
|
+
* Dropping avoids starting a fresh TraceState for a dead trace, which would re-run the sampler, and
|
|
2552
|
+
* avoids adding a child to a root that already shipped.
|
|
2553
|
+
*/
|
|
2554
|
+
function recordComponentSpan(record) {
|
|
2555
|
+
try {
|
|
2556
|
+
const flare = activeTracingFlare();
|
|
2557
|
+
if (!flare) return;
|
|
2558
|
+
const root = flare.tracer.getActiveSpan();
|
|
2559
|
+
if (!root || root.traceId !== record.parent.traceId || !root.isRecording) return;
|
|
2560
|
+
flare.startSpan(record.name, {
|
|
2561
|
+
spanId: record.spanId,
|
|
2562
|
+
parent: {
|
|
2563
|
+
traceId: record.parent.traceId,
|
|
2564
|
+
spanId: record.parent.parentSpanId
|
|
2565
|
+
},
|
|
2566
|
+
spanType: _flareapp_core.BrowserSpanType.Component,
|
|
2567
|
+
startTimeUnixNano: record.startTimeUnixNano,
|
|
2568
|
+
attributes: {
|
|
2569
|
+
...record.attributes,
|
|
2570
|
+
"flare.component.name": record.name
|
|
2571
|
+
},
|
|
2572
|
+
claimed: true
|
|
2573
|
+
}).end(record.endTimeUnixNano);
|
|
2574
|
+
} catch {}
|
|
2214
2575
|
}
|
|
2215
2576
|
|
|
2216
2577
|
//#endregion
|
|
@@ -2276,68 +2637,6 @@ function catchWindowErrors() {
|
|
|
2276
2637
|
});
|
|
2277
2638
|
}
|
|
2278
2639
|
|
|
2279
|
-
//#endregion
|
|
2280
|
-
//#region src/tracing/componentProfiler.ts
|
|
2281
|
-
/** Unix nanos on the same clock the tracer uses for span timestamps. */
|
|
2282
|
-
const nowNano = _flareapp_core.defaultNowNano;
|
|
2283
|
-
/**
|
|
2284
|
-
* Reserved up front so descendants can point at a span before it is recorded. Null when the trace is at
|
|
2285
|
-
* its span cap: descendants record before this span does, so an id the cap will refuse orphans them.
|
|
2286
|
-
*/
|
|
2287
|
-
function reserveSpanId(traceId) {
|
|
2288
|
-
if (traceId !== void 0 && !activeTracingFlare()?.tracer.claimSpanSlot(traceId)) return null;
|
|
2289
|
-
return (0, _flareapp_core.spanId)();
|
|
2290
|
-
}
|
|
2291
|
-
/** The root a top-level component nests under. Null when tracing is off or no root is recording. */
|
|
2292
|
-
function activeComponentRoot() {
|
|
2293
|
-
try {
|
|
2294
|
-
const root = activeTracingFlare()?.tracer.getActiveSpan();
|
|
2295
|
-
if (!root || !root.isRecording) return null;
|
|
2296
|
-
return {
|
|
2297
|
-
traceId: root.traceId,
|
|
2298
|
-
parentSpanId: root.spanId
|
|
2299
|
-
};
|
|
2300
|
-
} catch {
|
|
2301
|
-
return null;
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
/**
|
|
2305
|
-
* An ancestor's context is only usable while it still belongs to the live trace. A profiled component
|
|
2306
|
-
* that survives a navigation (a layout around a swapped page body) froze its context under the pageload
|
|
2307
|
-
* trace, and `recordComponentSpan` would drop anything pointing at that closed root.
|
|
2308
|
-
*/
|
|
2309
|
-
function resolveComponentParent(inherited, live) {
|
|
2310
|
-
if (inherited && live && inherited.traceId === live.traceId) return inherited;
|
|
2311
|
-
return live;
|
|
2312
|
-
}
|
|
2313
|
-
/**
|
|
2314
|
-
* Records only while the reserved root is still the live recording root, and drops the span otherwise.
|
|
2315
|
-
* Dropping avoids starting a fresh TraceState for a dead trace, which would re-run the sampler, and
|
|
2316
|
-
* avoids adding a child to a root that already shipped.
|
|
2317
|
-
*/
|
|
2318
|
-
function recordComponentSpan(record) {
|
|
2319
|
-
try {
|
|
2320
|
-
const flare = activeTracingFlare();
|
|
2321
|
-
if (!flare) return;
|
|
2322
|
-
const root = flare.tracer.getActiveSpan();
|
|
2323
|
-
if (!root || root.traceId !== record.parent.traceId || !root.isRecording) return;
|
|
2324
|
-
flare.startSpan(record.name, {
|
|
2325
|
-
spanId: record.spanId,
|
|
2326
|
-
parent: {
|
|
2327
|
-
traceId: record.parent.traceId,
|
|
2328
|
-
spanId: record.parent.parentSpanId
|
|
2329
|
-
},
|
|
2330
|
-
spanType: _flareapp_core.BrowserSpanType.Component,
|
|
2331
|
-
startTimeUnixNano: record.startTimeUnixNano,
|
|
2332
|
-
attributes: {
|
|
2333
|
-
...record.attributes,
|
|
2334
|
-
"flare.component.name": record.name
|
|
2335
|
-
},
|
|
2336
|
-
claimed: true
|
|
2337
|
-
}).end(record.endTimeUnixNano);
|
|
2338
|
-
} catch {}
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
2640
|
//#endregion
|
|
2342
2641
|
Object.defineProperty(exports, 'BrowserFlushScheduler', {
|
|
2343
2642
|
enumerable: true,
|
|
@@ -2375,6 +2674,12 @@ Object.defineProperty(exports, 'activeComponentRoot', {
|
|
|
2375
2674
|
return activeComponentRoot;
|
|
2376
2675
|
}
|
|
2377
2676
|
});
|
|
2677
|
+
Object.defineProperty(exports, 'browserUrlContext', {
|
|
2678
|
+
enumerable: true,
|
|
2679
|
+
get: function () {
|
|
2680
|
+
return browserUrlContext;
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2378
2683
|
Object.defineProperty(exports, 'catchWindowErrors', {
|
|
2379
2684
|
enumerable: true,
|
|
2380
2685
|
get: function () {
|
|
@@ -2393,16 +2698,16 @@ Object.defineProperty(exports, 'createFlareResolver', {
|
|
|
2393
2698
|
return createFlareResolver;
|
|
2394
2699
|
}
|
|
2395
2700
|
});
|
|
2396
|
-
Object.defineProperty(exports, '
|
|
2701
|
+
Object.defineProperty(exports, 'currentHref', {
|
|
2397
2702
|
enumerable: true,
|
|
2398
2703
|
get: function () {
|
|
2399
|
-
return
|
|
2704
|
+
return currentHref;
|
|
2400
2705
|
}
|
|
2401
2706
|
});
|
|
2402
|
-
Object.defineProperty(exports, '
|
|
2707
|
+
Object.defineProperty(exports, 'currentPath', {
|
|
2403
2708
|
enumerable: true,
|
|
2404
2709
|
get: function () {
|
|
2405
|
-
return
|
|
2710
|
+
return currentPath;
|
|
2406
2711
|
}
|
|
2407
2712
|
});
|
|
2408
2713
|
Object.defineProperty(exports, 'instrumentOnce', {
|
|
@@ -2411,12 +2716,6 @@ Object.defineProperty(exports, 'instrumentOnce', {
|
|
|
2411
2716
|
return instrumentOnce;
|
|
2412
2717
|
}
|
|
2413
2718
|
});
|
|
2414
|
-
Object.defineProperty(exports, 'instrumentXHR', {
|
|
2415
|
-
enumerable: true,
|
|
2416
|
-
get: function () {
|
|
2417
|
-
return instrumentXHR;
|
|
2418
|
-
}
|
|
2419
|
-
});
|
|
2420
2719
|
Object.defineProperty(exports, 'insulate', {
|
|
2421
2720
|
enumerable: true,
|
|
2422
2721
|
get: function () {
|
|
@@ -2471,6 +2770,12 @@ Object.defineProperty(exports, 'safeInvoke', {
|
|
|
2471
2770
|
return safeInvoke;
|
|
2472
2771
|
}
|
|
2473
2772
|
});
|
|
2773
|
+
Object.defineProperty(exports, 'startBreadcrumbs', {
|
|
2774
|
+
enumerable: true,
|
|
2775
|
+
get: function () {
|
|
2776
|
+
return startBreadcrumbs;
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2474
2779
|
Object.defineProperty(exports, 'startBrowserTracing', {
|
|
2475
2780
|
enumerable: true,
|
|
2476
2781
|
get: function () {
|
|
@@ -2483,15 +2788,15 @@ Object.defineProperty(exports, 'stopBrowserTracing', {
|
|
|
2483
2788
|
return stopBrowserTracing;
|
|
2484
2789
|
}
|
|
2485
2790
|
});
|
|
2486
|
-
Object.defineProperty(exports, '
|
|
2791
|
+
Object.defineProperty(exports, 'traceRequests', {
|
|
2487
2792
|
enumerable: true,
|
|
2488
2793
|
get: function () {
|
|
2489
|
-
return
|
|
2794
|
+
return traceRequests;
|
|
2490
2795
|
}
|
|
2491
2796
|
});
|
|
2492
|
-
Object.defineProperty(exports, '
|
|
2797
|
+
Object.defineProperty(exports, 'withRequestPatches', {
|
|
2493
2798
|
enumerable: true,
|
|
2494
2799
|
get: function () {
|
|
2495
|
-
return
|
|
2800
|
+
return withRequestPatches;
|
|
2496
2801
|
}
|
|
2497
2802
|
});
|