@monoscopetech/browser 0.8.0 → 0.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/errors.js +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -11
- package/dist/monoscope.min.js +3 -3
- package/dist/monoscope.min.js.map +1 -1
- package/dist/monoscope.umd.js +3 -3
- package/dist/monoscope.umd.js.map +1 -1
- package/dist/router.d.ts +3 -3
- package/dist/router.js +3 -8
- package/dist/tracing.d.ts +33 -6
- package/dist/tracing.js +230 -32
- package/dist/types.d.ts +2 -0
- package/dist/web-vitals.js +5 -0
- package/package.json +1 -1
package/dist/router.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
type
|
|
1
|
+
type NavFn = (from: string, to: string, method: string) => void;
|
|
2
2
|
export declare class SPARouter {
|
|
3
|
-
private
|
|
3
|
+
private onNavigation;
|
|
4
4
|
private currentUrl;
|
|
5
5
|
private _active;
|
|
6
6
|
private origPushState;
|
|
7
7
|
private origReplaceState;
|
|
8
8
|
private popstateHandler;
|
|
9
|
-
constructor(
|
|
9
|
+
constructor(onNavigation: NavFn);
|
|
10
10
|
start(): void;
|
|
11
11
|
stop(): void;
|
|
12
12
|
}
|
package/dist/router.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { addBreadcrumb } from "./breadcrumbs";
|
|
2
2
|
export class SPARouter {
|
|
3
|
-
constructor(
|
|
3
|
+
constructor(onNavigation) {
|
|
4
4
|
this.currentUrl = "";
|
|
5
5
|
this._active = false;
|
|
6
6
|
this.origPushState = null;
|
|
7
7
|
this.origReplaceState = null;
|
|
8
8
|
this.popstateHandler = null;
|
|
9
|
-
this.
|
|
9
|
+
this.onNavigation = onNavigation;
|
|
10
10
|
}
|
|
11
11
|
start() {
|
|
12
12
|
if (typeof window === "undefined" || this._active)
|
|
@@ -23,12 +23,7 @@ export class SPARouter {
|
|
|
23
23
|
return;
|
|
24
24
|
this.currentUrl = to;
|
|
25
25
|
addBreadcrumb({ type: "navigation", message: `${from} → ${to}`, data: { method } });
|
|
26
|
-
this.
|
|
27
|
-
"navigation.from": from,
|
|
28
|
-
"navigation.to": to,
|
|
29
|
-
"navigation.method": method,
|
|
30
|
-
"page.title": document.title,
|
|
31
|
-
});
|
|
26
|
+
this.onNavigation(from, to, method);
|
|
32
27
|
}
|
|
33
28
|
catch (e) {
|
|
34
29
|
try {
|
package/dist/tracing.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { MonoscopeConfig, MonoscopeUser } from "./types";
|
|
2
2
|
import { Span } from "@opentelemetry/api";
|
|
3
|
+
export type MonoscopeKind = "page_load" | "navigation" | "interaction" | "network" | "resource" | "web_vital" | "error" | "long_task" | "custom";
|
|
4
|
+
export declare function shortPath(url: string): string;
|
|
5
|
+
export declare function describeElement(el: EventTarget | Element | null | undefined): string;
|
|
6
|
+
/**
|
|
7
|
+
* RFC4122 v4 id with a fallback for non-secure contexts (HTTP / file:// /
|
|
8
|
+
* older Safari/Edge) where `crypto.randomUUID` is undefined.
|
|
9
|
+
*/
|
|
10
|
+
export declare function newId(): string;
|
|
3
11
|
export declare class OpenTelemetryManager {
|
|
4
12
|
private config;
|
|
5
13
|
private sessionId;
|
|
6
14
|
private tabId;
|
|
15
|
+
private pageviewId;
|
|
7
16
|
private provider;
|
|
8
17
|
private processor;
|
|
9
18
|
private longTaskObserver;
|
|
@@ -11,18 +20,36 @@ export declare class OpenTelemetryManager {
|
|
|
11
20
|
private _enabled;
|
|
12
21
|
private _configured;
|
|
13
22
|
private _firstExportLogged;
|
|
14
|
-
private
|
|
15
|
-
private
|
|
16
|
-
private
|
|
23
|
+
private routeSpan;
|
|
24
|
+
private routeContext;
|
|
25
|
+
private routeIdleTimer;
|
|
26
|
+
private flushOnHideHandler;
|
|
27
|
+
private visibilityHandler;
|
|
17
28
|
onExportStatus: ((ok: boolean) => void) | null;
|
|
29
|
+
onSpanStart: (() => void) | null;
|
|
18
30
|
constructor(config: MonoscopeConfig, sessionId: string, tabId: string);
|
|
19
31
|
private createProvider;
|
|
20
32
|
private commonAttrs;
|
|
21
33
|
private applyCommonAttrs;
|
|
22
34
|
configure(): void;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
getPageviewId(): string;
|
|
36
|
+
rotatePageview(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Open a short-lived route.change root span for an SPA navigation. Closes
|
|
39
|
+
* any previous route span, rotates pageview.id, and publishes the span as
|
|
40
|
+
* the active context so async work started in the same Zone (fetch/XHR)
|
|
41
|
+
* inherits it as parent. Auto-closes after ROUTE_IDLE_MS or on next nav.
|
|
42
|
+
*/
|
|
43
|
+
startRouteChange(from: string, to: string, method: string): void;
|
|
44
|
+
endRouteChange(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Flush pending spans before the JS context is destroyed. Critical for
|
|
47
|
+
* MPAs where every navigation unloads the page, and still valuable for
|
|
48
|
+
* SPAs at tab close. pagehide is preferred over beforeunload (fires for
|
|
49
|
+
* bfcache eviction and mobile backgrounding; beforeunload does not).
|
|
50
|
+
*/
|
|
51
|
+
private installFlushOnHide;
|
|
52
|
+
private withActiveContext;
|
|
26
53
|
emitSpan(name: string, attrs: Record<string, string | number | boolean>, configure?: (span: Span) => void): void;
|
|
27
54
|
private observeLongTasks;
|
|
28
55
|
private observeResourceTiming;
|
package/dist/tracing.js
CHANGED
|
@@ -12,18 +12,71 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
|
12
12
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
13
13
|
import { context, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
14
14
|
const MONOSCOPE_TRACER = "monoscope";
|
|
15
|
+
const ROUTE_IDLE_MS = 3000;
|
|
16
|
+
// Display-label helpers. These derive a human-readable label ("what happened?")
|
|
17
|
+
// from raw span attributes so trace viewers don't have to reinvent the wheel.
|
|
18
|
+
export function shortPath(url) {
|
|
19
|
+
try {
|
|
20
|
+
const u = new URL(url, typeof location !== "undefined" ? location.href : "http://_");
|
|
21
|
+
return u.pathname + (u.search ? "?…" : "");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return url.slice(0, 80);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function describeElement(el) {
|
|
28
|
+
const e = el;
|
|
29
|
+
if (!e || !e.tagName)
|
|
30
|
+
return "?";
|
|
31
|
+
const aria = e.getAttribute?.("aria-label");
|
|
32
|
+
if (aria)
|
|
33
|
+
return aria;
|
|
34
|
+
const text = (e.textContent || "").trim().replace(/\s+/g, " ").slice(0, 40);
|
|
35
|
+
if (text)
|
|
36
|
+
return text;
|
|
37
|
+
const id = e.id ? `#${e.id}` : "";
|
|
38
|
+
const cls = (e.getAttribute?.("class") || "").split(" ").filter(Boolean)[0];
|
|
39
|
+
return `${e.tagName.toLowerCase()}${id}${cls ? `.${cls}` : ""}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* RFC4122 v4 id with a fallback for non-secure contexts (HTTP / file:// /
|
|
43
|
+
* older Safari/Edge) where `crypto.randomUUID` is undefined.
|
|
44
|
+
*/
|
|
45
|
+
export function newId() {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID)
|
|
48
|
+
return crypto.randomUUID();
|
|
49
|
+
}
|
|
50
|
+
catch { /* fall through */ }
|
|
51
|
+
const b = new Uint8Array(16);
|
|
52
|
+
try {
|
|
53
|
+
crypto.getRandomValues(b);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
for (let i = 0; i < 16; i++)
|
|
57
|
+
b[i] = (Math.random() * 256) | 0;
|
|
58
|
+
}
|
|
59
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
60
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
61
|
+
const h = Array.from(b, x => x.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
63
|
+
}
|
|
15
64
|
export class OpenTelemetryManager {
|
|
16
65
|
constructor(config, sessionId, tabId) {
|
|
66
|
+
this.pageviewId = "";
|
|
17
67
|
this.processor = null;
|
|
18
68
|
this.longTaskObserver = null;
|
|
19
69
|
this.resourceObserver = null;
|
|
20
70
|
this._enabled = true;
|
|
21
71
|
this._configured = false;
|
|
22
72
|
this._firstExportLogged = false;
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
73
|
+
this.routeSpan = null;
|
|
74
|
+
this.routeContext = null;
|
|
75
|
+
this.routeIdleTimer = null;
|
|
76
|
+
this.flushOnHideHandler = null;
|
|
77
|
+
this.visibilityHandler = null;
|
|
26
78
|
this.onExportStatus = null;
|
|
79
|
+
this.onSpanStart = null;
|
|
27
80
|
this.config = config;
|
|
28
81
|
this.sessionId = sessionId;
|
|
29
82
|
this.tabId = tabId;
|
|
@@ -66,19 +119,32 @@ export class OpenTelemetryManager {
|
|
|
66
119
|
});
|
|
67
120
|
const processor = new BatchSpanProcessor(wrappedExporter);
|
|
68
121
|
this.processor = processor;
|
|
122
|
+
// Count every span at start — covers auto-instrumentations, internal
|
|
123
|
+
// emitSpan, and manual APIs uniformly. onEnd would undercount dropped
|
|
124
|
+
// spans; onStart reflects telemetry volume the SDK actually observed.
|
|
125
|
+
const countingProcessor = {
|
|
126
|
+
onStart: () => { try {
|
|
127
|
+
self.onSpanStart?.();
|
|
128
|
+
}
|
|
129
|
+
catch { /* never throw from processor */ } },
|
|
130
|
+
onEnd: () => { },
|
|
131
|
+
shutdown: () => Promise.resolve(),
|
|
132
|
+
forceFlush: () => Promise.resolve(),
|
|
133
|
+
};
|
|
69
134
|
return new WebTracerProvider({
|
|
70
135
|
resource: resourceFromAttributes({
|
|
71
136
|
[ATTR_SERVICE_NAME]: serviceName,
|
|
72
137
|
"x-api-key": apiKey,
|
|
73
138
|
...resourceAttributes,
|
|
74
139
|
}),
|
|
75
|
-
spanProcessors: [processor],
|
|
140
|
+
spanProcessors: [processor, countingProcessor],
|
|
76
141
|
});
|
|
77
142
|
}
|
|
78
143
|
commonAttrs() {
|
|
79
144
|
const attrs = {
|
|
80
145
|
"session.id": this.sessionId,
|
|
81
146
|
"tab.id": this.tabId,
|
|
147
|
+
"pageview.id": this.pageviewId,
|
|
82
148
|
"page.url": location.href,
|
|
83
149
|
"page.title": document.title,
|
|
84
150
|
};
|
|
@@ -100,13 +166,29 @@ export class OpenTelemetryManager {
|
|
|
100
166
|
if (typeof window === "undefined" || this._configured)
|
|
101
167
|
return;
|
|
102
168
|
this._configured = true;
|
|
169
|
+
// Sticky per-tab sampling decision so MPA navigations and SPA reloads don't
|
|
170
|
+
// produce half-traced sessions. sessionStorage is tab-scoped.
|
|
103
171
|
const rate = Math.max(0, Math.min(1, this.config.sampleRate ?? 1));
|
|
104
|
-
|
|
172
|
+
let sampled;
|
|
173
|
+
try {
|
|
174
|
+
const cached = sessionStorage.getItem("monoscope-sampled");
|
|
175
|
+
if (cached === "1" || cached === "0")
|
|
176
|
+
sampled = cached === "1";
|
|
177
|
+
else {
|
|
178
|
+
sampled = Math.random() < rate;
|
|
179
|
+
sessionStorage.setItem("monoscope-sampled", sampled ? "1" : "0");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
sampled = Math.random() < rate;
|
|
184
|
+
}
|
|
185
|
+
if (!sampled) {
|
|
105
186
|
this._enabled = false;
|
|
106
187
|
if (this.config.debug)
|
|
107
188
|
console.log("MonoscopeOTel: sampled out");
|
|
108
189
|
return;
|
|
109
190
|
}
|
|
191
|
+
this.pageviewId = newId();
|
|
110
192
|
this.provider.register({
|
|
111
193
|
contextManager: new ZoneContextManager(),
|
|
112
194
|
propagator: new W3CTraceContextPropagator(),
|
|
@@ -118,45 +200,155 @@ export class OpenTelemetryManager {
|
|
|
118
200
|
/^https?:\/\/(?:[^\/]+\.)?monoscope\.tech\//,
|
|
119
201
|
];
|
|
120
202
|
const addAttrs = (span) => this.applyCommonAttrs(span);
|
|
203
|
+
const stamp = (span, kind, label) => {
|
|
204
|
+
this.applyCommonAttrs(span);
|
|
205
|
+
span.setAttribute("monoscope.kind", kind);
|
|
206
|
+
if (label)
|
|
207
|
+
span.setAttribute("monoscope.display.label", label);
|
|
208
|
+
};
|
|
209
|
+
// User interactions: default to click/submit only. Tracing every keydown/
|
|
210
|
+
// mouseover would flood the backend; consumers can override via
|
|
211
|
+
// instrumentations config to opt into more.
|
|
212
|
+
const userInteraction = this.config.enableUserInteraction !== false
|
|
213
|
+
? [new UserInteractionInstrumentation({
|
|
214
|
+
eventNames: ["click", "submit"],
|
|
215
|
+
shouldPreventSpanCreation: (eventType, element, span) => {
|
|
216
|
+
stamp(span, "interaction", `${eventType} · ${describeElement(element)}`);
|
|
217
|
+
return false;
|
|
218
|
+
},
|
|
219
|
+
})]
|
|
220
|
+
: [];
|
|
121
221
|
registerInstrumentations({
|
|
122
222
|
tracerProvider: this.provider,
|
|
123
223
|
instrumentations: [
|
|
124
224
|
...(this.config.instrumentations || []),
|
|
125
225
|
new DocumentLoadInstrumentation({
|
|
126
226
|
ignoreNetworkEvents: !this.config.enableNetworkEvents,
|
|
127
|
-
applyCustomAttributesOnSpan: {
|
|
227
|
+
applyCustomAttributesOnSpan: {
|
|
228
|
+
documentLoad: (span) => stamp(span, "page_load", `Page · ${shortPath(location.href)}`),
|
|
229
|
+
resourceFetch: (span) => stamp(span, "resource"),
|
|
230
|
+
},
|
|
128
231
|
}),
|
|
129
232
|
new XMLHttpRequestInstrumentation({
|
|
130
|
-
propagateTraceHeaderCorsUrls: headerUrls, ignoreUrls,
|
|
233
|
+
propagateTraceHeaderCorsUrls: headerUrls, ignoreUrls,
|
|
234
|
+
applyCustomAttributesOnSpan: (span, xhr) => {
|
|
235
|
+
const url = xhr.responseURL;
|
|
236
|
+
stamp(span, "network", url ? `XHR · ${shortPath(url)}` : undefined);
|
|
237
|
+
},
|
|
131
238
|
}),
|
|
132
239
|
new FetchInstrumentation({
|
|
133
|
-
propagateTraceHeaderCorsUrls: headerUrls, ignoreUrls,
|
|
240
|
+
propagateTraceHeaderCorsUrls: headerUrls, ignoreUrls,
|
|
241
|
+
applyCustomAttributesOnSpan: (span, request) => {
|
|
242
|
+
let url;
|
|
243
|
+
let method = "GET";
|
|
244
|
+
if (typeof request === "string")
|
|
245
|
+
url = request;
|
|
246
|
+
else if (request && typeof request.url === "string") {
|
|
247
|
+
url = request.url;
|
|
248
|
+
method = request.method || "GET";
|
|
249
|
+
}
|
|
250
|
+
else if (request && typeof request.method === "string") {
|
|
251
|
+
method = request.method;
|
|
252
|
+
}
|
|
253
|
+
stamp(span, "network", url ? `${method} · ${shortPath(url)}` : undefined);
|
|
254
|
+
},
|
|
134
255
|
}),
|
|
135
|
-
...
|
|
256
|
+
...userInteraction,
|
|
136
257
|
],
|
|
137
258
|
});
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
this.
|
|
259
|
+
if (this.config.captureLongTasks !== false)
|
|
260
|
+
this.observeLongTasks();
|
|
261
|
+
if (this.config.captureResourceTiming)
|
|
262
|
+
this.observeResourceTiming();
|
|
263
|
+
this.installFlushOnHide();
|
|
141
264
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
265
|
+
getPageviewId() { return this.pageviewId; }
|
|
266
|
+
rotatePageview() {
|
|
267
|
+
this.pageviewId = newId();
|
|
268
|
+
return this.pageviewId;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Open a short-lived route.change root span for an SPA navigation. Closes
|
|
272
|
+
* any previous route span, rotates pageview.id, and publishes the span as
|
|
273
|
+
* the active context so async work started in the same Zone (fetch/XHR)
|
|
274
|
+
* inherits it as parent. Auto-closes after ROUTE_IDLE_MS or on next nav.
|
|
275
|
+
*/
|
|
276
|
+
startRouteChange(from, to, method) {
|
|
277
|
+
try {
|
|
278
|
+
this.endRouteChange();
|
|
279
|
+
this.rotatePageview();
|
|
280
|
+
const tracer = trace.getTracer(MONOSCOPE_TRACER);
|
|
281
|
+
const span = tracer.startSpan("route.change", {
|
|
282
|
+
attributes: {
|
|
283
|
+
"navigation.from": from,
|
|
284
|
+
"navigation.to": to,
|
|
285
|
+
"navigation.method": method,
|
|
286
|
+
"monoscope.kind": "navigation",
|
|
287
|
+
"monoscope.display.label": `Nav · ${shortPath(from)} → ${shortPath(to)}`,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
this.applyCommonAttrs(span);
|
|
291
|
+
this.routeSpan = span;
|
|
292
|
+
this.routeContext = trace.setSpan(context.active(), span);
|
|
293
|
+
this.routeIdleTimer = setTimeout(() => {
|
|
294
|
+
try {
|
|
295
|
+
this.endRouteChange();
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
if (this.config.debug)
|
|
299
|
+
console.warn("Monoscope: route idle close failed", e);
|
|
300
|
+
}
|
|
301
|
+
}, ROUTE_IDLE_MS);
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
if (this.config.debug)
|
|
305
|
+
console.warn("Monoscope: startRouteChange failed", e);
|
|
306
|
+
this.routeSpan = null;
|
|
307
|
+
this.routeContext = null;
|
|
308
|
+
this.routeIdleTimer = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
endRouteChange() {
|
|
312
|
+
if (this.routeIdleTimer) {
|
|
313
|
+
clearTimeout(this.routeIdleTimer);
|
|
314
|
+
this.routeIdleTimer = null;
|
|
315
|
+
}
|
|
316
|
+
this.routeSpan?.end();
|
|
317
|
+
this.routeSpan = null;
|
|
318
|
+
this.routeContext = null;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Flush pending spans before the JS context is destroyed. Critical for
|
|
322
|
+
* MPAs where every navigation unloads the page, and still valuable for
|
|
323
|
+
* SPAs at tab close. pagehide is preferred over beforeunload (fires for
|
|
324
|
+
* bfcache eviction and mobile backgrounding; beforeunload does not).
|
|
325
|
+
*/
|
|
326
|
+
installFlushOnHide() {
|
|
327
|
+
const flush = () => {
|
|
328
|
+
try {
|
|
329
|
+
this.endRouteChange();
|
|
330
|
+
this.processor?.forceFlush().catch(() => { });
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
if (this.config.debug)
|
|
334
|
+
console.warn("Monoscope: flush on hide failed", e);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
this.flushOnHideHandler = flush;
|
|
338
|
+
this.visibilityHandler = () => { if (document.visibilityState === "hidden")
|
|
339
|
+
flush(); };
|
|
340
|
+
window.addEventListener("pagehide", this.flushOnHideHandler);
|
|
341
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
149
342
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return context.with(this.pageContext, fn);
|
|
343
|
+
withActiveContext(fn) {
|
|
344
|
+
if (this.routeContext)
|
|
345
|
+
return context.with(this.routeContext, fn);
|
|
154
346
|
return fn();
|
|
155
347
|
}
|
|
156
348
|
emitSpan(name, attrs, configure) {
|
|
157
349
|
try {
|
|
158
350
|
const tracer = trace.getTracer(MONOSCOPE_TRACER);
|
|
159
|
-
this.
|
|
351
|
+
this.withActiveContext(() => tracer.startActiveSpan(name, (span) => {
|
|
160
352
|
this.applyCommonAttrs(span);
|
|
161
353
|
for (const [k, v] of Object.entries(attrs))
|
|
162
354
|
span.setAttribute(k, v);
|
|
@@ -181,6 +373,8 @@ export class OpenTelemetryManager {
|
|
|
181
373
|
const attrs = {
|
|
182
374
|
"longtask.duration": entry.duration,
|
|
183
375
|
"longtask.name": entry.name,
|
|
376
|
+
"monoscope.kind": "long_task",
|
|
377
|
+
"monoscope.display.label": `Long task · ${Math.round(entry.duration)}ms`,
|
|
184
378
|
};
|
|
185
379
|
const attr = entry.attribution;
|
|
186
380
|
if (attr?.[0]?.containerSrc)
|
|
@@ -214,12 +408,15 @@ export class OpenTelemetryManager {
|
|
|
214
408
|
continue;
|
|
215
409
|
try {
|
|
216
410
|
const re = entry;
|
|
411
|
+
const base = re.name.split("?")[0].split("/").pop() || re.name;
|
|
217
412
|
this.emitSpan("resource", {
|
|
218
413
|
"resource.name": re.name,
|
|
219
414
|
"resource.duration": re.duration,
|
|
220
415
|
"resource.type": re.initiatorType,
|
|
221
416
|
"resource.transferSize": re.transferSize,
|
|
222
417
|
"resource.encodedBodySize": re.encodedBodySize,
|
|
418
|
+
"monoscope.kind": "resource",
|
|
419
|
+
"monoscope.display.label": `${re.initiatorType} · ${base}`,
|
|
223
420
|
});
|
|
224
421
|
}
|
|
225
422
|
catch (e) {
|
|
@@ -236,7 +433,7 @@ export class OpenTelemetryManager {
|
|
|
236
433
|
}
|
|
237
434
|
startSpan(name, fn) {
|
|
238
435
|
const tracer = trace.getTracer(MONOSCOPE_TRACER);
|
|
239
|
-
return this.
|
|
436
|
+
return this.withActiveContext(() => tracer.startActiveSpan(name, (span) => {
|
|
240
437
|
this.applyCommonAttrs(span);
|
|
241
438
|
try {
|
|
242
439
|
const result = fn(span);
|
|
@@ -266,14 +463,15 @@ export class OpenTelemetryManager {
|
|
|
266
463
|
async shutdown() {
|
|
267
464
|
this.longTaskObserver?.disconnect();
|
|
268
465
|
this.resourceObserver?.disconnect();
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
window.removeEventListener("pagehide", this.
|
|
272
|
-
|
|
273
|
-
|
|
466
|
+
this.endRouteChange();
|
|
467
|
+
if (this.flushOnHideHandler) {
|
|
468
|
+
window.removeEventListener("pagehide", this.flushOnHideHandler);
|
|
469
|
+
this.flushOnHideHandler = null;
|
|
470
|
+
}
|
|
471
|
+
if (this.visibilityHandler) {
|
|
472
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
473
|
+
this.visibilityHandler = null;
|
|
274
474
|
}
|
|
275
|
-
this.pageSpan = null;
|
|
276
|
-
this.pageContext = null;
|
|
277
475
|
this._configured = false;
|
|
278
476
|
await this.provider.shutdown();
|
|
279
477
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type MonoscopeConfig = {
|
|
|
16
16
|
replaySampleRate?: number;
|
|
17
17
|
enabled?: boolean;
|
|
18
18
|
resourceTimingThresholdMs?: number;
|
|
19
|
+
captureResourceTiming?: boolean;
|
|
20
|
+
captureLongTasks?: boolean;
|
|
19
21
|
enableUserInteraction?: boolean;
|
|
20
22
|
};
|
|
21
23
|
export type MonoscopeUser = {
|
package/dist/web-vitals.js
CHANGED
|
@@ -13,12 +13,17 @@ export class WebVitalsCollector {
|
|
|
13
13
|
const report = (m) => {
|
|
14
14
|
if (!this._enabled)
|
|
15
15
|
return;
|
|
16
|
+
// CLS is unitless; the others are milliseconds.
|
|
17
|
+
const unit = m.name === "CLS" ? "" : "ms";
|
|
18
|
+
const value = m.name === "CLS" ? m.value.toFixed(3) : Math.round(m.value);
|
|
16
19
|
this.emit(`web-vital.${m.name}`, {
|
|
17
20
|
"vital.name": m.name,
|
|
18
21
|
"vital.value": m.value,
|
|
19
22
|
"vital.rating": m.rating,
|
|
20
23
|
"vital.id": m.id,
|
|
21
24
|
"vital.navigationType": m.navigationType,
|
|
25
|
+
"monoscope.kind": "web_vital",
|
|
26
|
+
"monoscope.display.label": `${m.name} · ${value}${unit} (${m.rating})`,
|
|
22
27
|
});
|
|
23
28
|
};
|
|
24
29
|
[onCLS, onINP, onLCP, onFCP, onTTFB].forEach(fn => fn(report));
|