@sentry/browser-utils 11.0.0-rc.0 → 11.0.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/build/cjs/instrumentation/performanceObserver.js.map +1 -1
- package/build/cjs/performance/entries.js +33 -22
- package/build/cjs/performance/entries.js.map +1 -1
- package/build/cjs/performance/interactions.js +38 -11
- package/build/cjs/performance/interactions.js.map +1 -1
- package/build/cjs/web-vitals/emitSpan.js +1 -14
- package/build/cjs/web-vitals/emitSpan.js.map +1 -1
- package/build/cjs/web-vitals/inp.js +1 -0
- package/build/cjs/web-vitals/inp.js.map +1 -1
- package/build/cjs/web-vitals/reportEvents.js +4 -4
- package/build/cjs/web-vitals/reportEvents.js.map +1 -1
- package/build/cjs/web-vitals/softNavs.js +25 -5
- package/build/cjs/web-vitals/softNavs.js.map +1 -1
- package/build/cjs/web-vitals/spans.js +61 -29
- package/build/cjs/web-vitals/spans.js.map +1 -1
- package/build/cjs/web-vitals/tracking.js +7 -0
- package/build/cjs/web-vitals/tracking.js.map +1 -1
- package/build/esm/instrumentation/performanceObserver.js.map +1 -1
- package/build/esm/package.json +1 -1
- package/build/esm/performance/entries.js +35 -25
- package/build/esm/performance/entries.js.map +1 -1
- package/build/esm/performance/interactions.js +40 -13
- package/build/esm/performance/interactions.js.map +1 -1
- package/build/esm/web-vitals/emitSpan.js +1 -14
- package/build/esm/web-vitals/emitSpan.js.map +1 -1
- package/build/esm/web-vitals/inp.js +1 -1
- package/build/esm/web-vitals/inp.js.map +1 -1
- package/build/esm/web-vitals/reportEvents.js +4 -4
- package/build/esm/web-vitals/reportEvents.js.map +1 -1
- package/build/esm/web-vitals/softNavs.js +25 -5
- package/build/esm/web-vitals/softNavs.js.map +1 -1
- package/build/esm/web-vitals/spans.js +54 -22
- package/build/esm/web-vitals/spans.js.map +1 -1
- package/build/esm/web-vitals/tracking.js +8 -1
- package/build/esm/web-vitals/tracking.js.map +1 -1
- package/build/types/instrumentation/performanceObserver.d.ts +1 -2
- package/build/types/instrumentation/performanceObserver.d.ts.map +1 -1
- package/build/types/performance/entries.d.ts +6 -1
- package/build/types/performance/entries.d.ts.map +1 -1
- package/build/types/performance/interactions.d.ts.map +1 -1
- package/build/types/web-vitals/emitSpan.d.ts +0 -2
- package/build/types/web-vitals/emitSpan.d.ts.map +1 -1
- package/build/types/web-vitals/inp.d.ts +2 -0
- package/build/types/web-vitals/inp.d.ts.map +1 -1
- package/build/types/web-vitals/reportEvents.d.ts +4 -7
- package/build/types/web-vitals/reportEvents.d.ts.map +1 -1
- package/build/types/web-vitals/softNavs.d.ts.map +1 -1
- package/build/types/web-vitals/spans.d.ts +2 -3
- package/build/types/web-vitals/spans.d.ts.map +1 -1
- package/build/types/web-vitals/tracking.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { SENTRY_OP, SENTRY_SOURCE, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN, SENTRY_IDLE_SPAN_FINISH_REASON, UI_COMPONENT_NAME } from '@sentry/conventions/attributes';
|
|
1
|
+
import { SENTRY_OP, SENTRY_SOURCE, SENTRY_SEGMENT_NAME_SOURCE, ROUTER_NAVIGATION_ROUTE_ID, URL_TEMPLATE, HTTP_ROUTE, URL_PATH, URL_FULL, SENTRY_SEGMENT_NAME, SENTRY_ORIGIN, SENTRY_IDLE_SPAN_FINISH_REASON, UI_COMPONENT_NAME } from '@sentry/conventions/attributes';
|
|
2
2
|
import { UI_ACTION_CLICK, UI_INTERACTION_CLICK } from '@sentry/conventions/op';
|
|
3
|
-
import { defineIntegration, getRootSpan, spanToJSON, debug, getActiveSpan, browserPerformanceTimeOrigin } from '@sentry/core';
|
|
3
|
+
import { defineIntegration, getRootSpan, spanToJSON, debug, hasSpanStreamingEnabled, UI_ACTION_CLICK_SPAN_NAME_FALLBACK, filterCollectedUrl, getActiveSpan, browserPerformanceTimeOrigin, UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK } from '@sentry/core';
|
|
4
4
|
import { startIdleSpan } from '@sentry/core/browser';
|
|
5
5
|
import { DEBUG_BUILD } from '../debug-build.js';
|
|
6
6
|
import { htmlTreeAsString } from '../htmlTreeAsString.js';
|
|
7
7
|
import { addPerformanceInstrumentationHandler } from '../instrumentation/performanceObserver.js';
|
|
8
|
+
import { UNKNOWN_ELEMENT_NAME } from '../web-vitals/inp.js';
|
|
8
9
|
import { isBotUserAgent } from '../isBotUserAgent.js';
|
|
9
10
|
import { WINDOW } from '../types.js';
|
|
10
11
|
import { msToSec, startAndEndSpan } from './utils.js';
|
|
@@ -18,12 +19,25 @@ const _interactionsIntegration = ((options = {}) => {
|
|
|
18
19
|
if (isBotUserAgent()) {
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
const latestRoute = {
|
|
22
|
+
const latestRoute = {
|
|
23
|
+
name: void 0,
|
|
24
|
+
source: void 0,
|
|
25
|
+
routeId: void 0,
|
|
26
|
+
urlTemplate: void 0,
|
|
27
|
+
httpRoute: void 0,
|
|
28
|
+
urlPath: void 0,
|
|
29
|
+
urlFull: void 0
|
|
30
|
+
};
|
|
22
31
|
let inflightRouteSpan;
|
|
23
32
|
function trackRoute(span) {
|
|
24
33
|
const { name, attributes } = spanToJSON(span);
|
|
25
34
|
latestRoute.name = name;
|
|
26
35
|
latestRoute.source = attributes[SENTRY_SOURCE] || attributes[SENTRY_SEGMENT_NAME_SOURCE];
|
|
36
|
+
latestRoute.routeId = attributes[ROUTER_NAVIGATION_ROUTE_ID];
|
|
37
|
+
latestRoute.urlTemplate = attributes[URL_TEMPLATE];
|
|
38
|
+
latestRoute.httpRoute = attributes[HTTP_ROUTE];
|
|
39
|
+
latestRoute.urlPath = attributes[URL_PATH];
|
|
40
|
+
latestRoute.urlFull = attributes[URL_FULL];
|
|
27
41
|
}
|
|
28
42
|
client.on("spanStart", (span) => {
|
|
29
43
|
if (isRouteSpan(span)) {
|
|
@@ -38,8 +52,8 @@ const _interactionsIntegration = ((options = {}) => {
|
|
|
38
52
|
inflightRouteSpan = void 0;
|
|
39
53
|
trackRoute(span);
|
|
40
54
|
});
|
|
41
|
-
registerInteractionListener(latestRoute, () => inflightRouteSpan, options);
|
|
42
|
-
trackInteractionsAsSpans();
|
|
55
|
+
registerInteractionListener(client, latestRoute, () => inflightRouteSpan, options);
|
|
56
|
+
trackInteractionsAsSpans(client);
|
|
43
57
|
}
|
|
44
58
|
};
|
|
45
59
|
});
|
|
@@ -51,7 +65,7 @@ function isRouteSpan(span) {
|
|
|
51
65
|
const op = spanToJSON(span).attributes[SENTRY_OP];
|
|
52
66
|
return op === "pageload" || op === "navigation";
|
|
53
67
|
}
|
|
54
|
-
function registerInteractionListener(latestRoute, getInflightRouteSpan, idleSpanOptions) {
|
|
68
|
+
function registerInteractionListener(client, latestRoute, getInflightRouteSpan, idleSpanOptions) {
|
|
55
69
|
if (!WINDOW.document) {
|
|
56
70
|
return;
|
|
57
71
|
}
|
|
@@ -74,13 +88,23 @@ function registerInteractionListener(latestRoute, getInflightRouteSpan, idleSpan
|
|
|
74
88
|
DEBUG_BUILD && debug.warn(`[Tracing] Did not create ${UI_ACTION_CLICK} span because the latest route name is missing.`);
|
|
75
89
|
return;
|
|
76
90
|
}
|
|
91
|
+
const hasSpanStreaming = hasSpanStreamingEnabled(client);
|
|
92
|
+
const routeName = latestRoute.routeId || latestRoute.urlTemplate || latestRoute.httpRoute;
|
|
93
|
+
const streamedName = routeName || UI_ACTION_CLICK_SPAN_NAME_FALLBACK;
|
|
94
|
+
const streamedNameSource = !routeName || latestRoute.routeId ? "custom" : "route";
|
|
77
95
|
inflightInteractionSpan = startIdleSpan(
|
|
78
96
|
{
|
|
79
|
-
name: latestRoute.name,
|
|
97
|
+
name: hasSpanStreaming ? streamedName : latestRoute.name,
|
|
80
98
|
attributes: {
|
|
81
99
|
[SENTRY_OP]: UI_ACTION_CLICK,
|
|
82
|
-
[SENTRY_SEGMENT_NAME_SOURCE]: latestRoute.source || "url",
|
|
83
|
-
[SENTRY_ORIGIN]: "auto.browser.interactions"
|
|
100
|
+
[SENTRY_SEGMENT_NAME_SOURCE]: hasSpanStreaming ? streamedNameSource : latestRoute.source || "url",
|
|
101
|
+
[SENTRY_ORIGIN]: "auto.browser.interactions",
|
|
102
|
+
...hasSpanStreaming && { [SENTRY_SEGMENT_NAME]: streamedName },
|
|
103
|
+
...latestRoute.routeId && { [ROUTER_NAVIGATION_ROUTE_ID]: latestRoute.routeId },
|
|
104
|
+
...latestRoute.urlTemplate && { [URL_TEMPLATE]: latestRoute.urlTemplate },
|
|
105
|
+
...latestRoute.httpRoute && { [HTTP_ROUTE]: latestRoute.httpRoute },
|
|
106
|
+
...latestRoute.urlPath && { [URL_PATH]: latestRoute.urlPath },
|
|
107
|
+
...latestRoute.urlFull && { [URL_FULL]: filterCollectedUrl(latestRoute.urlFull) }
|
|
84
108
|
}
|
|
85
109
|
},
|
|
86
110
|
idleSpanOptions
|
|
@@ -89,7 +113,7 @@ function registerInteractionListener(latestRoute, getInflightRouteSpan, idleSpan
|
|
|
89
113
|
{ capture: true }
|
|
90
114
|
);
|
|
91
115
|
}
|
|
92
|
-
function trackInteractionsAsSpans() {
|
|
116
|
+
function trackInteractionsAsSpans(client) {
|
|
93
117
|
addPerformanceInstrumentationHandler("event", ({ entries }) => {
|
|
94
118
|
const parent = getActiveSpan();
|
|
95
119
|
if (!parent) {
|
|
@@ -99,15 +123,18 @@ function trackInteractionsAsSpans() {
|
|
|
99
123
|
if (entry.name === "click") {
|
|
100
124
|
const startTime = msToSec(browserPerformanceTimeOrigin() + entry.startTime);
|
|
101
125
|
const duration = msToSec(entry.duration);
|
|
126
|
+
const selector = htmlTreeAsString(entry.target);
|
|
127
|
+
const componentName = getComponentName(entry.target);
|
|
128
|
+
const hasSpanStreaming = hasSpanStreamingEnabled(client);
|
|
102
129
|
const spanOptions = {
|
|
103
|
-
name:
|
|
130
|
+
name: hasSpanStreaming ? componentName || UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK : selector,
|
|
104
131
|
startTime,
|
|
105
132
|
attributes: {
|
|
106
133
|
[SENTRY_OP]: UI_INTERACTION_CLICK,
|
|
107
|
-
[SENTRY_ORIGIN]: "auto.browser.interactions"
|
|
134
|
+
[SENTRY_ORIGIN]: "auto.browser.interactions",
|
|
135
|
+
...selector && selector !== UNKNOWN_ELEMENT_NAME && { "browser.web_vital.inp.target": selector }
|
|
108
136
|
}
|
|
109
137
|
};
|
|
110
|
-
const componentName = getComponentName(entry.target);
|
|
111
138
|
if (componentName) {
|
|
112
139
|
spanOptions.attributes[UI_COMPONENT_NAME] = componentName;
|
|
113
140
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interactions.js","sources":["../../../src/performance/interactions.ts"],"sourcesContent":["import {\n SENTRY_OP,\n SENTRY_ORIGIN,\n SENTRY_IDLE_SPAN_FINISH_REASON,\n UI_COMPONENT_NAME,\n SENTRY_SOURCE,\n SENTRY_SEGMENT_NAME_SOURCE,\n} from '@sentry/conventions/attributes';\nimport { UI_INTERACTION_CLICK, UI_ACTION_CLICK } from '@sentry/conventions/op';\nimport type { IntegrationFn, Span, StartSpanOptions, TransactionSource } from '@sentry/core';\nimport {\n browserPerformanceTimeOrigin,\n debug,\n defineIntegration,\n getActiveSpan,\n getRootSpan,\n spanToJSON,\n} from '@sentry/core';\nimport { startIdleSpan } from '@sentry/core/browser';\nimport { DEBUG_BUILD } from '../debug-build';\nimport { htmlTreeAsString } from '../htmlTreeAsString';\nimport { addPerformanceInstrumentationHandler } from '../instrumentation/performanceObserver';\nimport { isBotUserAgent } from '../isBotUserAgent';\nimport { WINDOW } from '../types';\nimport { msToSec, startAndEndSpan } from './utils';\nimport { getComponentName } from '../component-name';\n\nconst INTEGRATION_NAME = 'Interactions';\n\ninterface InteractionsOptions {\n /**\n * The time that has to pass without any span being created.\n * If this time is exceeded, the interaction span will finish.\n *\n * Default: 1000 (ms)\n */\n idleTimeout?: number;\n\n /**\n * The max. time an interaction span may run.\n * If this time is exceeded, the interaction span will finish no matter what.\n *\n * Default: 30000 (ms)\n */\n finalTimeout?: number;\n\n /**\n * The max. time a child span of an interaction span may run.\n * If this time is exceeded, the interaction span will finish no matter what.\n *\n * Default: 15000 (ms)\n */\n childSpanTimeout?: number;\n}\n\ninterface RouteInfo {\n name: string | undefined;\n source: TransactionSource | undefined;\n}\n\nconst _interactionsIntegration = ((options: InteractionsOptions = {}) => {\n return {\n name: INTEGRATION_NAME,\n setup(client) {\n if (isBotUserAgent()) {\n return;\n }\n\n const latestRoute: RouteInfo = { name: undefined, source: undefined };\n // The pageload/navigation span that is currently in progress, if any. Clicks that happen while one\n // is open are not turned into interaction spans, as they'd compete with the route span for children.\n let inflightRouteSpan: Span | undefined;\n\n function trackRoute(span: Span): void {\n const { name, attributes } = spanToJSON(span);\n latestRoute.name = name;\n // oxlint-disable-next-line typescript/no-deprecated\n latestRoute.source = (attributes[SENTRY_SOURCE] || attributes[SENTRY_SEGMENT_NAME_SOURCE]) as\n | TransactionSource\n | undefined;\n }\n\n client.on('spanStart', span => {\n if (isRouteSpan(span)) {\n inflightRouteSpan = span;\n trackRoute(span);\n }\n });\n\n client.on('spanEnd', span => {\n if (span !== inflightRouteSpan) {\n return;\n }\n inflightRouteSpan = undefined;\n // Re-read the route on end: routing instrumentation frequently renames the pageload or navigation\n // span once the route is resolved, so this is more accurate than what we saw at span start.\n trackRoute(span);\n });\n\n registerInteractionListener(latestRoute, () => inflightRouteSpan, options);\n\n trackInteractionsAsSpans();\n },\n };\n}) satisfies IntegrationFn;\n\n/**\n * Captures user interactions as spans.\n *\n * Important: This integration is considered experimental and might lead to noisy spans. Use at your own risk.\n *\n *\n * Clicks that happen outside of an in-progress pageload or navigation start an idle `ui.action.click` span,\n * named after the last known route, which collects everything the interaction triggers (requests, child spans,\n * …). Additionally, `ui.interaction.click` spans are recorded for the browser's own `event` timing entries.\n *\n * The integration requires `browserTracingIntegration` (or another source of pageload/navigation spans)\n * to know which route an interaction belongs to.\n *\n * @example\n * ```ts\n * Sentry.init({\n * integrations: [Sentry.browserTracingIntegration(), Sentry.interactionsIntegration()],\n * });\n * ```\n */\nexport const interactionsIntegration = defineIntegration(_interactionsIntegration);\n\nfunction isRouteSpan(span: Span): boolean {\n if (getRootSpan(span) !== span) {\n return false;\n }\n const op = spanToJSON(span).attributes[SENTRY_OP];\n return op === 'pageload' || op === 'navigation';\n}\n\nfunction registerInteractionListener(\n latestRoute: RouteInfo,\n getInflightRouteSpan: () => Span | undefined,\n // `startIdleSpan` fills in `TRACING_DEFAULTS` for whatever is left out, which are the same defaults\n // `browserTracingIntegration` uses for its pageload and navigation spans.\n idleSpanOptions: InteractionsOptions,\n): void {\n // `document` is not available in all browser environments (e.g. web workers), and without it there is\n // nothing to click on.\n if (!WINDOW.document) {\n return;\n }\n\n let inflightInteractionSpan: Span | undefined;\n\n addEventListener(\n 'click',\n () => {\n if (getInflightRouteSpan()) {\n DEBUG_BUILD &&\n debug.warn(\n `[Tracing] Did not create ${UI_ACTION_CLICK} span because a pageload or navigation span is in progress.`,\n );\n return;\n }\n\n if (inflightInteractionSpan) {\n inflightInteractionSpan.setAttribute(SENTRY_IDLE_SPAN_FINISH_REASON, 'interactionInterrupted');\n inflightInteractionSpan.end();\n inflightInteractionSpan = undefined;\n }\n\n if (!latestRoute.name) {\n DEBUG_BUILD &&\n debug.warn(`[Tracing] Did not create ${UI_ACTION_CLICK} span because the latest route name is missing.`);\n return;\n }\n\n inflightInteractionSpan = startIdleSpan(\n {\n name: latestRoute.name,\n attributes: {\n [SENTRY_OP]: UI_ACTION_CLICK,\n [SENTRY_SEGMENT_NAME_SOURCE]: latestRoute.source || 'url',\n [SENTRY_ORIGIN]: 'auto.browser.interactions',\n },\n },\n idleSpanOptions,\n );\n },\n { capture: true },\n );\n}\n\n/**\n * Record the browser's `event` timing entries for clicks as spans on the currently active span.\n */\nfunction trackInteractionsAsSpans(): void {\n addPerformanceInstrumentationHandler('event', ({ entries }) => {\n const parent = getActiveSpan();\n if (!parent) {\n return;\n }\n for (const entry of entries) {\n if (entry.name === 'click') {\n const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime);\n const duration = msToSec(entry.duration);\n\n const spanOptions: StartSpanOptions & Required<Pick<StartSpanOptions, 'attributes'>> = {\n name: htmlTreeAsString(entry.target),\n startTime: startTime,\n attributes: {\n [SENTRY_OP]: UI_INTERACTION_CLICK,\n [SENTRY_ORIGIN]: 'auto.browser.interactions',\n },\n };\n\n const componentName = getComponentName(entry.target);\n if (componentName) {\n spanOptions.attributes[UI_COMPONENT_NAME] = componentName;\n }\n\n startAndEndSpan(parent, startTime, startTime + duration, spanOptions);\n }\n }\n });\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"interactions.js","sources":["../../../src/performance/interactions.ts"],"sourcesContent":["import {\n HTTP_ROUTE,\n ROUTER_NAVIGATION_ROUTE_ID,\n SENTRY_IDLE_SPAN_FINISH_REASON,\n SENTRY_OP,\n SENTRY_ORIGIN,\n SENTRY_SEGMENT_NAME,\n SENTRY_SEGMENT_NAME_SOURCE,\n SENTRY_SOURCE,\n UI_COMPONENT_NAME,\n URL_FULL,\n URL_PATH,\n URL_TEMPLATE,\n} from '@sentry/conventions/attributes';\nimport { UI_ACTION_CLICK, UI_INTERACTION_CLICK } from '@sentry/conventions/op';\nimport type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource } from '@sentry/core';\nimport {\n browserPerformanceTimeOrigin,\n debug,\n defineIntegration,\n filterCollectedUrl,\n getActiveSpan,\n getRootSpan,\n hasSpanStreamingEnabled,\n spanToJSON,\n UI_ACTION_CLICK_SPAN_NAME_FALLBACK,\n UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK,\n} from '@sentry/core';\nimport { startIdleSpan } from '@sentry/core/browser';\nimport { DEBUG_BUILD } from '../debug-build';\nimport { htmlTreeAsString } from '../htmlTreeAsString';\nimport { addPerformanceInstrumentationHandler } from '../instrumentation/performanceObserver';\nimport { UNKNOWN_ELEMENT_NAME } from '../web-vitals/inp';\nimport { isBotUserAgent } from '../isBotUserAgent';\nimport { WINDOW } from '../types';\nimport { msToSec, startAndEndSpan } from './utils';\nimport { getComponentName } from '../component-name';\n\nconst INTEGRATION_NAME = 'Interactions';\n\ninterface InteractionsOptions {\n /**\n * The time that has to pass without any span being created.\n * If this time is exceeded, the interaction span will finish.\n *\n * Default: 1000 (ms)\n */\n idleTimeout?: number;\n\n /**\n * The max. time an interaction span may run.\n * If this time is exceeded, the interaction span will finish no matter what.\n *\n * Default: 30000 (ms)\n */\n finalTimeout?: number;\n\n /**\n * The max. time a child span of an interaction span may run.\n * If this time is exceeded, the interaction span will finish no matter what.\n *\n * Default: 15000 (ms)\n */\n childSpanTimeout?: number;\n}\n\ninterface RouteInfo {\n name: string | undefined;\n source: TransactionSource | undefined;\n routeId: string | undefined;\n urlTemplate: string | undefined;\n httpRoute: string | undefined;\n urlPath: string | undefined;\n urlFull: string | undefined;\n}\n\nconst _interactionsIntegration = ((options: InteractionsOptions = {}) => {\n return {\n name: INTEGRATION_NAME,\n setup(client) {\n if (isBotUserAgent()) {\n return;\n }\n\n const latestRoute: RouteInfo = {\n name: undefined,\n source: undefined,\n routeId: undefined,\n urlTemplate: undefined,\n httpRoute: undefined,\n urlPath: undefined,\n urlFull: undefined,\n };\n // The pageload/navigation span that is currently in progress, if any. Clicks that happen while one\n // is open are not turned into interaction spans, as they'd compete with the route span for children.\n let inflightRouteSpan: Span | undefined;\n\n function trackRoute(span: Span): void {\n const { name, attributes } = spanToJSON(span);\n latestRoute.name = name;\n // oxlint-disable-next-line typescript/no-deprecated\n latestRoute.source = (attributes[SENTRY_SOURCE] || attributes[SENTRY_SEGMENT_NAME_SOURCE]) as\n | TransactionSource\n | undefined;\n latestRoute.routeId = attributes[ROUTER_NAVIGATION_ROUTE_ID] as string | undefined;\n latestRoute.urlTemplate = attributes[URL_TEMPLATE] as string | undefined;\n latestRoute.httpRoute = attributes[HTTP_ROUTE] as string | undefined;\n latestRoute.urlPath = attributes[URL_PATH] as string | undefined;\n latestRoute.urlFull = attributes[URL_FULL] as string | undefined;\n }\n\n client.on('spanStart', span => {\n if (isRouteSpan(span)) {\n inflightRouteSpan = span;\n trackRoute(span);\n }\n });\n\n client.on('spanEnd', span => {\n if (span !== inflightRouteSpan) {\n return;\n }\n inflightRouteSpan = undefined;\n // Re-read the route on end: routing instrumentation frequently renames the pageload or navigation\n // span once the route is resolved, so this is more accurate than what we saw at span start.\n trackRoute(span);\n });\n\n registerInteractionListener(client, latestRoute, () => inflightRouteSpan, options);\n\n trackInteractionsAsSpans(client);\n },\n };\n}) satisfies IntegrationFn;\n\n/**\n * Captures user interactions as spans.\n *\n * Important: This integration is considered experimental and might lead to noisy spans. Use at your own risk.\n *\n *\n * Clicks that happen outside of an in-progress pageload or navigation start an idle `ui.action.click` span,\n * named after the last known route, which collects everything the interaction triggers (requests, child spans,\n * …). Additionally, `ui.interaction.click` spans are recorded for the browser's own `event` timing entries.\n *\n * The integration requires `browserTracingIntegration` (or another source of pageload/navigation spans)\n * to know which route an interaction belongs to.\n *\n * @example\n * ```ts\n * Sentry.init({\n * integrations: [Sentry.browserTracingIntegration(), Sentry.interactionsIntegration()],\n * });\n * ```\n */\nexport const interactionsIntegration = defineIntegration(_interactionsIntegration);\n\nfunction isRouteSpan(span: Span): boolean {\n if (getRootSpan(span) !== span) {\n return false;\n }\n const op = spanToJSON(span).attributes[SENTRY_OP];\n return op === 'pageload' || op === 'navigation';\n}\n\nfunction registerInteractionListener(\n client: Client,\n latestRoute: RouteInfo,\n getInflightRouteSpan: () => Span | undefined,\n // `startIdleSpan` fills in `TRACING_DEFAULTS` for whatever is left out, which are the same defaults\n // `browserTracingIntegration` uses for its pageload and navigation spans.\n idleSpanOptions: InteractionsOptions,\n): void {\n // `document` is not available in all browser environments (e.g. web workers), and without it there is\n // nothing to click on.\n if (!WINDOW.document) {\n return;\n }\n\n let inflightInteractionSpan: Span | undefined;\n\n addEventListener(\n 'click',\n () => {\n if (getInflightRouteSpan()) {\n DEBUG_BUILD &&\n debug.warn(\n `[Tracing] Did not create ${UI_ACTION_CLICK} span because a pageload or navigation span is in progress.`,\n );\n return;\n }\n\n if (inflightInteractionSpan) {\n inflightInteractionSpan.setAttribute(SENTRY_IDLE_SPAN_FINISH_REASON, 'interactionInterrupted');\n inflightInteractionSpan.end();\n inflightInteractionSpan = undefined;\n }\n\n if (!latestRoute.name) {\n DEBUG_BUILD &&\n debug.warn(`[Tracing] Did not create ${UI_ACTION_CLICK} span because the latest route name is missing.`);\n return;\n }\n\n const hasSpanStreaming = hasSpanStreamingEnabled(client);\n const routeName = latestRoute.routeId || latestRoute.urlTemplate || latestRoute.httpRoute;\n const streamedName = routeName || UI_ACTION_CLICK_SPAN_NAME_FALLBACK;\n // A route id is a name the app gave the route, so it is `custom` rather than a `route` pattern.\n const streamedNameSource = !routeName || latestRoute.routeId ? 'custom' : 'route';\n\n inflightInteractionSpan = startIdleSpan(\n {\n name: hasSpanStreaming ? streamedName : latestRoute.name,\n attributes: {\n [SENTRY_OP]: UI_ACTION_CLICK,\n [SENTRY_SEGMENT_NAME_SOURCE]: hasSpanStreaming ? streamedNameSource : latestRoute.source || 'url',\n [SENTRY_ORIGIN]: 'auto.browser.interactions',\n ...(hasSpanStreaming && { [SENTRY_SEGMENT_NAME]: streamedName }),\n ...(latestRoute.routeId && { [ROUTER_NAVIGATION_ROUTE_ID]: latestRoute.routeId }),\n ...(latestRoute.urlTemplate && { [URL_TEMPLATE]: latestRoute.urlTemplate }),\n ...(latestRoute.httpRoute && { [HTTP_ROUTE]: latestRoute.httpRoute }),\n ...(latestRoute.urlPath && { [URL_PATH]: latestRoute.urlPath }),\n ...(latestRoute.urlFull && { [URL_FULL]: filterCollectedUrl(latestRoute.urlFull) }),\n },\n },\n idleSpanOptions,\n );\n },\n { capture: true },\n );\n}\n\n/**\n * Record the browser's `event` timing entries for clicks as spans on the currently active span.\n */\nfunction trackInteractionsAsSpans(client: Client): void {\n addPerformanceInstrumentationHandler('event', ({ entries }) => {\n const parent = getActiveSpan();\n if (!parent) {\n return;\n }\n for (const entry of entries) {\n if (entry.name === 'click') {\n const startTime = msToSec((browserPerformanceTimeOrigin() as number) + entry.startTime);\n const duration = msToSec(entry.duration);\n\n const selector = htmlTreeAsString(entry.target);\n const componentName = getComponentName(entry.target);\n const hasSpanStreaming = hasSpanStreamingEnabled(client);\n\n const spanOptions: StartSpanOptions & Required<Pick<StartSpanOptions, 'attributes'>> = {\n name: hasSpanStreaming ? componentName || UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK : selector,\n startTime: startTime,\n attributes: {\n [SENTRY_OP]: UI_INTERACTION_CLICK,\n [SENTRY_ORIGIN]: 'auto.browser.interactions',\n ...(selector && selector !== UNKNOWN_ELEMENT_NAME && { 'browser.web_vital.inp.target': selector }),\n },\n };\n\n if (componentName) {\n spanOptions.attributes[UI_COMPONENT_NAME] = componentName;\n }\n\n startAndEndSpan(parent, startTime, startTime + duration, spanOptions);\n }\n }\n });\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,15 +3,6 @@ import { startInactiveSpan } from '@sentry/core/browser';
|
|
|
3
3
|
import { USER_AGENT_ORIGINAL, BROWSER_NAVIGATION_ID, BROWSER_NAVIGATION_TYPE, SENTRY_SEGMENT_NAME, SENTRY_TRANSACTION, SENTRY_REPLAY_ID } from '@sentry/conventions/attributes';
|
|
4
4
|
import { WINDOW } from '../types.js';
|
|
5
5
|
|
|
6
|
-
const BROWSER_NAVIGATION_TYPES = {
|
|
7
|
-
reload: "reload",
|
|
8
|
-
prerender: "prerender",
|
|
9
|
-
"back-forward-cache": "bfcache",
|
|
10
|
-
"soft-navigation": "soft-navigation"
|
|
11
|
-
};
|
|
12
|
-
function toBrowserNavigationType(navigationType) {
|
|
13
|
-
return BROWSER_NAVIGATION_TYPES[navigationType] ?? "navigate";
|
|
14
|
-
}
|
|
15
6
|
function _emitWebVitalSpan(options) {
|
|
16
7
|
const {
|
|
17
8
|
name,
|
|
@@ -21,7 +12,6 @@ function _emitWebVitalSpan(options) {
|
|
|
21
12
|
value,
|
|
22
13
|
attributes: passedAttributes,
|
|
23
14
|
parentSpan,
|
|
24
|
-
reportEvent,
|
|
25
15
|
startTime,
|
|
26
16
|
endTime,
|
|
27
17
|
standalone,
|
|
@@ -45,14 +35,11 @@ function _emitWebVitalSpan(options) {
|
|
|
45
35
|
if (parentSpan && spanToJSON(parentSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === "pageload") {
|
|
46
36
|
attributes["sentry.pageload.span_id"] = parentSpan.spanContext().spanId;
|
|
47
37
|
}
|
|
48
|
-
if (reportEvent) {
|
|
49
|
-
attributes[`browser.web_vital.${metricName}.report_event`] = reportEvent;
|
|
50
|
-
}
|
|
51
38
|
if (softNavigationId != null) {
|
|
52
39
|
attributes[BROWSER_NAVIGATION_ID] = softNavigationId;
|
|
53
40
|
}
|
|
54
41
|
if (navigationType) {
|
|
55
|
-
attributes[BROWSER_NAVIGATION_TYPE] =
|
|
42
|
+
attributes[BROWSER_NAVIGATION_TYPE] = navigationType;
|
|
56
43
|
}
|
|
57
44
|
if (standalone) {
|
|
58
45
|
Object.assign(attributes, getReplayAttributes());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emitSpan.js","sources":["../../../src/web-vitals/emitSpan.ts"],"sourcesContent":["import type { Integration, Span, SpanAttributes } from '@sentry/core';\nimport {\n getClient,\n getCurrentScope,\n getRootSpan,\n SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,\n SEMANTIC_ATTRIBUTE_SENTRY_OP,\n SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,\n spanToJSON,\n} from '@sentry/core';\nimport { startInactiveSpan } from '@sentry/core/browser';\nimport {\n BROWSER_NAVIGATION_ID,\n BROWSER_NAVIGATION_TYPE,\n SENTRY_REPLAY_ID,\n SENTRY_SEGMENT_NAME,\n SENTRY_TRANSACTION,\n USER_AGENT_ORIGINAL,\n} from '@sentry/conventions/attributes';\nimport { WINDOW } from '../types';\nimport type { MetricNavigationType } from '../instrumentation/performanceObserver';\
|
|
1
|
+
{"version":3,"file":"emitSpan.js","sources":["../../../src/web-vitals/emitSpan.ts"],"sourcesContent":["import type { Integration, Span, SpanAttributes } from '@sentry/core';\nimport {\n getClient,\n getCurrentScope,\n getRootSpan,\n SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,\n SEMANTIC_ATTRIBUTE_SENTRY_OP,\n SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,\n spanToJSON,\n} from '@sentry/core';\nimport { startInactiveSpan } from '@sentry/core/browser';\nimport {\n BROWSER_NAVIGATION_ID,\n BROWSER_NAVIGATION_TYPE,\n SENTRY_REPLAY_ID,\n SENTRY_SEGMENT_NAME,\n SENTRY_TRANSACTION,\n USER_AGENT_ORIGINAL,\n} from '@sentry/conventions/attributes';\nimport { WINDOW } from '../types';\nimport type { MetricNavigationType } from '../instrumentation/performanceObserver';\n\n// Locally-defined interfaces to avoid leaking bare global type references into the\n// generated .d.ts. The `declare global` augmentations in web-vitals/types.ts make these\n// available during this package's compilation but are NOT carried to consumers.\n// This mirrors the pattern used for PerformanceEventTiming in instrument.ts.\nexport interface LayoutShift extends PerformanceEntry {\n value: number;\n sources: Array<{ node: Node | null }>;\n hadRecentInput: boolean;\n}\n\nexport interface LargestContentfulPaint extends PerformanceEntry {\n readonly renderTime: DOMHighResTimeStamp;\n readonly loadTime: DOMHighResTimeStamp;\n readonly size: number;\n readonly id: string;\n readonly url: string;\n readonly element: Element | null;\n}\n\ninterface WebVitalSpanOptions {\n name: string;\n op: string;\n origin: string;\n metricName: 'lcp' | 'cls' | 'inp';\n value: number;\n attributes?: SpanAttributes;\n parentSpan?: Span;\n startTime: number;\n endTime?: number;\n /** Set when the vital was reported for a soft navigation rather than the initial page load. */\n softNavigationId?: number;\n /** The navigation the vital was measured on, as reported by web-vitals. */\n navigationType?: MetricNavigationType;\n /**\n * When `true`, the span is sent on its own as a v2 streamed span instead of being folded into a\n * transaction. Used for INP when span streaming is disabled (it reports late, so it can't ride\n * the pageload transaction).\n *\n * TODO(standalone): remove once the static (transaction) trace lifecycle is dropped and INP always streams.\n */\n standalone?: boolean;\n}\n\n/**\n * Emits a web vital span. When `standalone` is set it is sent on its own as a v2 streamed span;\n * otherwise it flows through the span streaming pipeline as a child of `parentSpan`.\n */\nexport function _emitWebVitalSpan(options: WebVitalSpanOptions): void {\n const {\n name,\n op,\n origin,\n metricName,\n value,\n attributes: passedAttributes,\n parentSpan,\n startTime,\n endTime,\n standalone,\n softNavigationId,\n navigationType,\n } = options;\n\n // Taken off the segment span itself, so it can't diverge from it: a routing instrumentation may\n // rename that span (a pageload span is named `Pageload` until its route resolves), and the scope's\n // transaction name is deliberately not kept in sync with it. Only a standalone span, which is sent\n // without its segment span, has to fall back to the scope.\n const segmentSpan = parentSpan && getRootSpan(parentSpan);\n const segmentName = segmentSpan ? spanToJSON(segmentSpan).name : getCurrentScope().getScopeData().transactionName;\n\n const attributes: SpanAttributes = {\n [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin,\n [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op,\n [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 0,\n [`browser.web_vital.${metricName}.value`]: value,\n // oxlint-disable-next-line typescript-eslint/no-deprecated\n [SENTRY_TRANSACTION]: segmentName,\n [SENTRY_SEGMENT_NAME]: segmentName,\n // Web vital score calculation relies on the user agent\n [USER_AGENT_ORIGINAL]: WINDOW.navigator?.userAgent,\n ...passedAttributes,\n };\n\n if (parentSpan && spanToJSON(parentSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'pageload') {\n // for LCP and CLS, we collect the pageload span id as an attribute\n attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId;\n }\n\n if (softNavigationId != null) {\n attributes[BROWSER_NAVIGATION_ID] = softNavigationId;\n }\n\n if (navigationType) {\n attributes[BROWSER_NAVIGATION_TYPE] = navigationType;\n }\n\n // A standalone span is sent as a plain v2 span without running the `processSpan` hooks (see\n // `captureStandaloneSpanWithStaticCallback`), so Replay can't attach the replay id itself. Set it\n // here, mirroring Replay's `processSpan`, so INP keeps its replay association like it did on v1.\n // TODO(standalone): remove once the static (transaction) trace lifecycle is dropped and INP always\n // streams, at which point Replay's `processSpan` runs and attaches the replay id.\n if (standalone) {\n Object.assign(attributes, getReplayAttributes());\n }\n\n const span = startInactiveSpan({\n name,\n attributes,\n startTime,\n parentSpan,\n // oxlint-disable-next-line typescript/no-deprecated -- intentional during the v1/v2 transition; see the TODO(standalone) above\n experimental: standalone ? { standalone: true } : undefined,\n });\n\n if (span) {\n span.end(endTime ?? startTime);\n }\n}\n\ninterface ReplayIntegration extends Integration {\n getReplayId: (onlyIfSampled?: boolean) => string | undefined;\n getRecordingMode: () => 'session' | 'buffer' | undefined;\n}\n\n// TODO(standalone): remove once the static (transaction) trace lifecycle is dropped; Replay's\n// `processSpan` then attaches the replay id to the streamed INP span instead.\nfunction getReplayAttributes(): SpanAttributes {\n const replay = getClient()?.getIntegrationByName<ReplayIntegration>('Replay');\n const replayId = replay?.getReplayId(true);\n if (!replayId) {\n return {};\n }\n\n return {\n [SENTRY_REPLAY_ID]: replayId,\n // Not the `SENTRY_REPLAY_IS_BUFFERING` convention: that one has no `sentry._internal.` prefix, and\n // the rest of the SDK sets the prefixed key (see `logs/internal.ts`, `metrics/internal.ts`).\n 'sentry._internal.replay_is_buffering': replay!.getRecordingMode() === 'buffer' ? true : undefined,\n };\n}\n"],"names":[],"mappings":";;;;;AAqEO,SAAS,kBAAkB,OAAA,EAAoC;AACpE,EAAA,MAAM;AAAA,IACJ,IAAA;AAAA,IACA,EAAA;AAAA,IACA,MAAA;AAAA,IACA,UAAA;AAAA,IACA,KAAA;AAAA,IACA,UAAA,EAAY,gBAAA;AAAA,IACZ,UAAA;AAAA,IACA,SAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF,GAAI,OAAA;AAMJ,EAAA,MAAM,WAAA,GAAc,UAAA,IAAc,WAAA,CAAY,UAAU,CAAA;AACxD,EAAA,MAAM,WAAA,GAAc,cAAc,UAAA,CAAW,WAAW,EAAE,IAAA,GAAO,eAAA,EAAgB,CAAE,YAAA,EAAa,CAAE,eAAA;AAElG,EAAA,MAAM,UAAA,GAA6B;AAAA,IACjC,CAAC,gCAAgC,GAAG,MAAA;AAAA,IACpC,CAAC,4BAA4B,GAAG,EAAA;AAAA,IAChC,CAAC,iCAAiC,GAAG,CAAA;AAAA,IACrC,CAAC,CAAA,kBAAA,EAAqB,UAAU,CAAA,MAAA,CAAQ,GAAG,KAAA;AAAA;AAAA,IAE3C,CAAC,kBAAkB,GAAG,WAAA;AAAA,IACtB,CAAC,mBAAmB,GAAG,WAAA;AAAA;AAAA,IAEvB,CAAC,mBAAmB,GAAG,MAAA,CAAO,SAAA,EAAW,SAAA;AAAA,IACzC,GAAG;AAAA,GACL;AAEA,EAAA,IAAI,cAAc,UAAA,CAAW,UAAU,EAAE,UAAA,CAAW,4BAA4B,MAAM,UAAA,EAAY;AAEhG,IAAA,UAAA,CAAW,yBAAyB,CAAA,GAAI,UAAA,CAAW,WAAA,EAAY,CAAE,MAAA;AAAA,EACnE;AAEA,EAAA,IAAI,oBAAoB,IAAA,EAAM;AAC5B,IAAA,UAAA,CAAW,qBAAqB,CAAA,GAAI,gBAAA;AAAA,EACtC;AAEA,EAAA,IAAI,cAAA,EAAgB;AAClB,IAAA,UAAA,CAAW,uBAAuB,CAAA,GAAI,cAAA;AAAA,EACxC;AAOA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,mBAAA,EAAqB,CAAA;AAAA,EACjD;AAEA,EAAA,MAAM,OAAO,iBAAA,CAAkB;AAAA,IAC7B,IAAA;AAAA,IACA,UAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA;AAAA,IAEA,YAAA,EAAc,UAAA,GAAa,EAAE,UAAA,EAAY,MAAK,GAAI;AAAA,GACnD,CAAA;AAED,EAAA,IAAI,IAAA,EAAM;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,WAAW,SAAS,CAAA;AAAA,EAC/B;AACF;AASA,SAAS,mBAAA,GAAsC;AAC7C,EAAA,MAAM,MAAA,GAAS,SAAA,EAAU,EAAG,oBAAA,CAAwC,QAAQ,CAAA;AAC5E,EAAA,MAAM,QAAA,GAAW,MAAA,EAAQ,WAAA,CAAY,IAAI,CAAA;AACzC,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,OAAO;AAAA,IACL,CAAC,gBAAgB,GAAG,QAAA;AAAA;AAAA;AAAA,IAGpB,sCAAA,EAAwC,MAAA,CAAQ,gBAAA,EAAiB,KAAM,WAAW,IAAA,GAAO;AAAA,GAC3F;AACF;;;;"}
|
|
@@ -111,5 +111,5 @@ function registerInpInteractionListener() {
|
|
|
111
111
|
addPerformanceInstrumentationHandler("first-input", handleEntries);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export { INP_ENTRY_MAP, MAX_PLAUSIBLE_INP_DURATION, getCachedInteractionContext, registerInpInteractionListener };
|
|
114
|
+
export { INP_ENTRY_MAP, MAX_PLAUSIBLE_INP_DURATION, UNKNOWN_ELEMENT_NAME, getCachedInteractionContext, registerInpInteractionListener };
|
|
115
115
|
//# sourceMappingURL=inp.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inp.js","sources":["../../../src/web-vitals/inp.ts"],"sourcesContent":["import type { Span } from '@sentry/core';\nimport { getActiveSpan, getRootSpan, isBrowser } from '@sentry/core';\nimport { htmlTreeAsString } from '../htmlTreeAsString';\nimport { WINDOW } from '../types';\nimport { addPerformanceInstrumentationHandler, isPerformanceEventTiming } from '../instrumentation/performanceObserver';\n\ninterface InteractionContext {\n span: Span | undefined;\n elementName: string;\n}\n\nconst LAST_INTERACTIONS: number[] = [];\nconst INTERACTIONS_SPAN_MAP = new Map<number, InteractionContext>();\n\n// Map to store element names by timestamp, since we get the DOM event before the PerformanceObserver entry\n/** What `htmlTreeAsString` returns when it cannot describe the target. */\
|
|
1
|
+
{"version":3,"file":"inp.js","sources":["../../../src/web-vitals/inp.ts"],"sourcesContent":["import type { Span } from '@sentry/core';\nimport { getActiveSpan, getRootSpan, isBrowser } from '@sentry/core';\nimport { htmlTreeAsString } from '../htmlTreeAsString';\nimport { WINDOW } from '../types';\nimport { addPerformanceInstrumentationHandler, isPerformanceEventTiming } from '../instrumentation/performanceObserver';\n\ninterface InteractionContext {\n span: Span | undefined;\n elementName: string;\n}\n\nconst LAST_INTERACTIONS: number[] = [];\nconst INTERACTIONS_SPAN_MAP = new Map<number, InteractionContext>();\n\n// Map to store element names by timestamp, since we get the DOM event before the PerformanceObserver entry\n/** What `htmlTreeAsString` returns when it cannot describe the target. */\nexport const UNKNOWN_ELEMENT_NAME = '<unknown>';\n\nconst ELEMENT_NAME_TIMESTAMP_MAP = new Map<number, string>();\n\n/**\n * 60 seconds is the maximum for a plausible INP value\n * (source: Me)\n */\nexport const MAX_PLAUSIBLE_INP_DURATION = 60;\n\nexport type InteractionType = 'click' | 'hover' | 'drag' | 'press';\n\nexport const INP_ENTRY_MAP: Record<string, InteractionType> = {\n click: 'click',\n pointerdown: 'click',\n pointerup: 'click',\n mousedown: 'click',\n mouseup: 'click',\n touchstart: 'click',\n touchend: 'click',\n mouseover: 'hover',\n mouseout: 'hover',\n mouseenter: 'hover',\n mouseleave: 'hover',\n pointerover: 'hover',\n pointerout: 'hover',\n pointerenter: 'hover',\n pointerleave: 'hover',\n dragstart: 'drag',\n dragend: 'drag',\n drag: 'drag',\n dragenter: 'drag',\n dragleave: 'drag',\n dragover: 'drag',\n drop: 'drag',\n keydown: 'press',\n keyup: 'press',\n keypress: 'press',\n input: 'press',\n};\n\n/**\n * Look up a cached interaction context (element name + root span) by interactionId.\n * Returns undefined if no context was cached for this interaction.\n */\nexport function getCachedInteractionContext(interactionId: number | undefined): InteractionContext | undefined {\n return interactionId != null ? INTERACTIONS_SPAN_MAP.get(interactionId) : undefined;\n}\n\n/**\n * Register a listener to cache route information for INP interactions.\n */\nexport function registerInpInteractionListener(): void {\n // Listen for all interaction events that could contribute to INP\n const interactionEvents = Object.keys(INP_ENTRY_MAP);\n if (isBrowser()) {\n interactionEvents.forEach(eventType => {\n WINDOW.addEventListener(eventType, captureElementFromEvent, { capture: true, passive: true });\n });\n }\n\n /**\n * Captures the element name from a DOM event and stores it in the ELEMENT_NAME_TIMESTAMP_MAP.\n */\n function captureElementFromEvent(event: Event): void {\n const target = event.target as HTMLElement | null;\n if (!target) {\n return;\n }\n\n const elementName = htmlTreeAsString(target);\n const timestamp = Math.round(event.timeStamp);\n\n // Not every event of an interaction has a describable target, and one that doesn't would\n // otherwise claim the timestamp and leave the span unnamed.\n if (!elementName || elementName === UNKNOWN_ELEMENT_NAME) {\n return;\n }\n\n // Every event of one interaction shares a timestamp, and so do the `pointerover`/`mouseover`\n // the browser fires afterwards when a handler swaps out the element under the cursor. Those\n // arrive last and describe the new DOM, so keeping the first usable name is what pins the entry\n // to the element that was actually interacted with.\n if (ELEMENT_NAME_TIMESTAMP_MAP.has(timestamp)) {\n return;\n }\n\n // Store the element name by timestamp so we can match it with the PerformanceEntry\n ELEMENT_NAME_TIMESTAMP_MAP.set(timestamp, elementName);\n\n // Clean up old\n if (ELEMENT_NAME_TIMESTAMP_MAP.size > 50) {\n const firstKey = ELEMENT_NAME_TIMESTAMP_MAP.keys().next().value;\n if (firstKey !== undefined) {\n ELEMENT_NAME_TIMESTAMP_MAP.delete(firstKey);\n }\n }\n }\n\n /**\n * Tries to get the element name from the timestamp map.\n */\n function resolveElementNameFromEntry(entry: PerformanceEntry): string {\n const timestamp = Math.round(entry.startTime);\n let elementName = ELEMENT_NAME_TIMESTAMP_MAP.get(timestamp);\n\n // try nearby timestamps (±5ms)\n if (!elementName) {\n for (let offset = -5; offset <= 5; offset++) {\n const nearbyName = ELEMENT_NAME_TIMESTAMP_MAP.get(timestamp + offset);\n if (nearbyName) {\n elementName = nearbyName;\n break;\n }\n }\n }\n\n return elementName || UNKNOWN_ELEMENT_NAME;\n }\n\n const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => {\n const activeSpan = getActiveSpan();\n const activeRootSpan = activeSpan && getRootSpan(activeSpan);\n\n entries.forEach(entry => {\n if (!isPerformanceEventTiming(entry)) {\n return;\n }\n\n const interactionId = entry.interactionId;\n if (interactionId == null) {\n return;\n }\n\n // If the interaction was already recorded before, nothing more to do\n if (INTERACTIONS_SPAN_MAP.has(interactionId)) {\n return;\n }\n\n const elementName = entry.target ? htmlTreeAsString(entry.target) : resolveElementNameFromEntry(entry);\n\n // We keep max. 10 interactions in the list, then remove the oldest one & clean up\n if (LAST_INTERACTIONS.length > 10) {\n const last = LAST_INTERACTIONS.shift() as number;\n INTERACTIONS_SPAN_MAP.delete(last);\n }\n\n // We add the interaction to the list of recorded interactions\n // and store both the span and element name for this interaction\n LAST_INTERACTIONS.push(interactionId);\n INTERACTIONS_SPAN_MAP.set(interactionId, {\n span: activeRootSpan,\n elementName,\n });\n });\n };\n\n addPerformanceInstrumentationHandler('event', handleEntries);\n addPerformanceInstrumentationHandler('first-input', handleEntries);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,18 +3,18 @@ import { onHidden } from './utils.js';
|
|
|
3
3
|
function listenForWebVitalReportEvents(client, collectorCallback) {
|
|
4
4
|
let pageloadSpan;
|
|
5
5
|
let collected = false;
|
|
6
|
-
function _runCollectorCallbackOnce(
|
|
6
|
+
function _runCollectorCallbackOnce() {
|
|
7
7
|
if (!collected && pageloadSpan) {
|
|
8
|
-
collectorCallback(
|
|
8
|
+
collectorCallback(pageloadSpan);
|
|
9
9
|
}
|
|
10
10
|
collected = true;
|
|
11
11
|
}
|
|
12
12
|
onHidden(() => {
|
|
13
|
-
_runCollectorCallbackOnce(
|
|
13
|
+
_runCollectorCallbackOnce();
|
|
14
14
|
});
|
|
15
15
|
const unsubscribeStartNavigation = client.on("beforeStartNavigationSpan", (_, options) => {
|
|
16
16
|
if (!options?.isRedirect) {
|
|
17
|
-
_runCollectorCallbackOnce(
|
|
17
|
+
_runCollectorCallbackOnce();
|
|
18
18
|
unsubscribeStartNavigation();
|
|
19
19
|
unsubscribeAfterStartPageLoadSpan();
|
|
20
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reportEvents.js","sources":["../../../src/web-vitals/reportEvents.ts"],"sourcesContent":["import type { Client, Span } from '@sentry/core';\nimport { onHidden } from './utils';\n\
|
|
1
|
+
{"version":3,"file":"reportEvents.js","sources":["../../../src/web-vitals/reportEvents.ts"],"sourcesContent":["import type { Client, Span } from '@sentry/core';\nimport { onHidden } from './utils';\n\n/**\n * Listens for events on which we want to collect a previously accumulated web vital value.\n * Currently, this includes:\n *\n * - pagehide (i.e. user minimizes browser window, hides tab, etc)\n * - soft navigation (we only care about the vital of the initially loaded route)\n *\n * As a \"side-effect\", this function will also collect the pageload span.\n *\n * @param collectorCallback the callback to be called when the first of these events is triggered. It is passed the\n * pageload span, which the web vital span is parented to.\n */\nexport function listenForWebVitalReportEvents(client: Client, collectorCallback: (pageloadSpan: Span) => void) {\n let pageloadSpan: Span | undefined;\n\n let collected = false;\n function _runCollectorCallbackOnce() {\n if (!collected && pageloadSpan) {\n collectorCallback(pageloadSpan);\n }\n collected = true;\n }\n\n onHidden(() => {\n _runCollectorCallbackOnce();\n });\n\n const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {\n // we only want to collect LCP if we actually navigate. Redirects should be ignored.\n if (!options?.isRedirect) {\n _runCollectorCallbackOnce();\n unsubscribeStartNavigation();\n unsubscribeAfterStartPageLoadSpan();\n }\n });\n\n const unsubscribeAfterStartPageLoadSpan = client.on('afterStartPageLoadSpan', span => {\n pageloadSpan = span;\n unsubscribeAfterStartPageLoadSpan();\n });\n}\n"],"names":[],"mappings":";;AAeO,SAAS,6BAAA,CAA8B,QAAgB,iBAAA,EAAiD;AAC7G,EAAA,IAAI,YAAA;AAEJ,EAAA,IAAI,SAAA,GAAY,KAAA;AAChB,EAAA,SAAS,yBAAA,GAA4B;AACnC,IAAA,IAAI,CAAC,aAAa,YAAA,EAAc;AAC9B,MAAA,iBAAA,CAAkB,YAAY,CAAA;AAAA,IAChC;AACA,IAAA,SAAA,GAAY,IAAA;AAAA,EACd;AAEA,EAAA,QAAA,CAAS,MAAM;AACb,IAAA,yBAAA,EAA0B;AAAA,EAC5B,CAAC,CAAA;AAED,EAAA,MAAM,6BAA6B,MAAA,CAAO,EAAA,CAAG,2BAAA,EAA6B,CAAC,GAAG,OAAA,KAAY;AAExF,IAAA,IAAI,CAAC,SAAS,UAAA,EAAY;AACxB,MAAA,yBAAA,EAA0B;AAC1B,MAAA,0BAAA,EAA2B;AAC3B,MAAA,iCAAA,EAAkC;AAAA,IACpC;AAAA,EACF,CAAC,CAAA;AAED,EAAA,MAAM,iCAAA,GAAoC,MAAA,CAAO,EAAA,CAAG,wBAAA,EAA0B,CAAA,IAAA,KAAQ;AACpF,IAAA,YAAA,GAAe,IAAA;AACf,IAAA,iCAAA,EAAkC;AAAA,EACpC,CAAC,CAAA;AACH;;;;"}
|
|
@@ -7,10 +7,14 @@ import { WINDOW } from '../types.js';
|
|
|
7
7
|
const MAX_TRACKED_NAVIGATIONS = 5;
|
|
8
8
|
const INTERACTION_MATCH_TOLERANCE_MS = 5;
|
|
9
9
|
let _pendingNavigation;
|
|
10
|
+
let _pendingInteraction;
|
|
10
11
|
let _lastInteractionTimestamp;
|
|
11
12
|
const _interactionIdToNavigationSpan = new LRUMap(MAX_TRACKED_NAVIGATIONS);
|
|
12
13
|
const _navigationIdToNavigationSpan = new LRUMap(MAX_TRACKED_NAVIGATIONS);
|
|
13
14
|
let _correlationStarted = false;
|
|
15
|
+
function interactionMatches(entryStartTime, interactionTimestamp) {
|
|
16
|
+
return Math.abs(entryStartTime - interactionTimestamp) <= INTERACTION_MATCH_TOLERANCE_MS;
|
|
17
|
+
}
|
|
14
18
|
function supportsSoftNavigations() {
|
|
15
19
|
try {
|
|
16
20
|
return PerformanceObserver.supportedEntryTypes.includes("soft-navigation") && // Older implementations exposed this as an attribute rather than a method. Only the method
|
|
@@ -36,19 +40,35 @@ function startSoftNavigationCorrelation(client) {
|
|
|
36
40
|
if (spanToJSON(span).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== "navigation") {
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
39
|
-
_pendingNavigation =
|
|
43
|
+
_pendingNavigation = void 0;
|
|
44
|
+
const interactionTimestamp = _lastInteractionTimestamp;
|
|
45
|
+
if (interactionTimestamp == null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (_pendingInteraction?.interactionTimestamp === interactionTimestamp) {
|
|
49
|
+
_interactionIdToNavigationSpan.set(_pendingInteraction.interactionId, span);
|
|
50
|
+
_pendingInteraction = void 0;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
_pendingNavigation = { span, interactionTimestamp };
|
|
40
54
|
});
|
|
41
55
|
const bindInteractionToNavigationSpan = ({ entries }) => {
|
|
42
56
|
for (const entry of entries) {
|
|
57
|
+
if (!isPerformanceEventTiming(entry) || !entry.interactionId) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
43
60
|
const pending = _pendingNavigation;
|
|
44
|
-
if (
|
|
61
|
+
if (pending && interactionMatches(entry.startTime, pending.interactionTimestamp)) {
|
|
62
|
+
_interactionIdToNavigationSpan.set(entry.interactionId, pending.span);
|
|
63
|
+
_pendingNavigation = void 0;
|
|
45
64
|
continue;
|
|
46
65
|
}
|
|
47
|
-
if (
|
|
66
|
+
if (_interactionIdToNavigationSpan.get(entry.interactionId)) {
|
|
48
67
|
continue;
|
|
49
68
|
}
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
if (_lastInteractionTimestamp != null && interactionMatches(entry.startTime, _lastInteractionTimestamp)) {
|
|
70
|
+
_pendingInteraction = { interactionId: entry.interactionId, interactionTimestamp: _lastInteractionTimestamp };
|
|
71
|
+
}
|
|
52
72
|
}
|
|
53
73
|
};
|
|
54
74
|
addPerformanceInstrumentationHandler("event", bindInteractionToNavigationSpan);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"softNavs.js","sources":["../../../src/web-vitals/softNavs.ts"],"sourcesContent":["import type { Client, Span } from '@sentry/core';\nimport { debug, LRUMap, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core';\nimport { BROWSER_NAVIGATION_ID } from '@sentry/conventions/attributes';\nimport { DEBUG_BUILD } from '../debug-build';\nimport type { PerformanceSoftNavigation } from '../instrumentation/performanceObserver';\nimport { addPerformanceInstrumentationHandler, isPerformanceEventTiming } from '../instrumentation/performanceObserver';\nimport { WINDOW } from '../types';\n\n/**\n * A page only ever needs its most recent navigations to still be joinable: web vitals for a soft\n * navigation are finalized at the next soft navigation or on pagehide, never later than that.\n */\nconst MAX_TRACKED_NAVIGATIONS = 5;\n\n/**\n * Tolerance when matching a DOM event's `timeStamp` against the `startTime` of its Event Timing\n * entry. Both are `DOMHighResTimeStamp`s from the same clock, so this only absorbs rounding.\n */\nconst INTERACTION_MATCH_TOLERANCE_MS = 5;\n\ninterface SoftNavMetric {\n navigationType: string;\n navigationId: number;\n navigationInteractionId?: number;\n}\n\ninterface PendingNavigation {\n span: Span;\n interactionTimestamp: number;\n}\n\n// The navigation span whose triggering interaction we haven't identified yet.\nlet _pendingNavigation: PendingNavigation | undefined;\n// The timestamp of the most recent trusted click/keydown, i.e. our best guess at the interaction\n// that a history change happening right now was driven by.\nlet _lastInteractionTimestamp: number | undefined;\n\nconst _interactionIdToNavigationSpan = new LRUMap<number, Span>(MAX_TRACKED_NAVIGATIONS);\nconst _navigationIdToNavigationSpan = new LRUMap<number, Span>(MAX_TRACKED_NAVIGATIONS);\n\nlet _correlationStarted = false;\n\n/**\n * Whether the browser can report web vitals for soft navigations.\n *\n * This mirrors web-vitals' own feature detection: passing `reportSoftNavs` on a browser that fails\n * this check is a no-op there, so it has to be a no-op here too.\n */\nexport function supportsSoftNavigations(): boolean {\n try {\n return (\n PerformanceObserver.supportedEntryTypes.includes('soft-navigation') &&\n // Older implementations exposed this as an attribute rather than a method. Only the method\n // form shipped unflagged, so it's what web-vitals gates on.\n typeof (\n WINDOW as {\n PerformanceSoftNavigation?: { prototype?: { getLargestInteractionContentfulPaint?: unknown } };\n }\n ).PerformanceSoftNavigation?.prototype?.getLargestInteractionContentfulPaint === 'function'\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Start correlating the browser's soft navigations with the SDK's navigation spans.\n *\n * A navigation span is created synchronously on the history change, but the browser only mints the\n * `soft-navigation` entry (and with it the `navigationId` that web vitals are reported against)\n * once the navigation has been confirmed by a paint. So the `navigationId` cannot be known at span\n * creation time and the two have to be joined after the fact.\n *\n * The join key is the `interactionId` of the interaction that drove the navigation: per the Soft\n * Navigations spec the `soft-navigation` entry carries the `interactionId` of the interaction that\n * triggered it, which is the same id the interaction's own `PerformanceEventTiming` entry carries.\n * So we bind a navigation span to the interaction it happened during, and the soft navigation\n * joins back to that span through the shared id.\n *\n * This is inherently partial. Navigations that don't meet the browser's soft navigation heuristic\n * (programmatic navigations, navigations that never paint, back/forward from the browser chrome)\n * produce no entry at all, so those navigation spans simply have no web vitals.\n */\nexport function startSoftNavigationCorrelation(client: Client): void {\n if (_correlationStarted || !supportsSoftNavigations()) {\n return;\n }\n _correlationStarted = true;\n\n const onInteraction = (event: Event): void => {\n if (event.isTrusted) {\n _lastInteractionTimestamp = event.timeStamp;\n }\n };\n // Only click and keydown can start a soft navigation, which is also what the SDK's redirect\n // detection listens for.\n WINDOW.addEventListener('click', onInteraction, { capture: true, passive: true });\n WINDOW.addEventListener('keydown', onInteraction, { capture: true, passive: true });\n\n client.on('spanStart', span => {\n if (spanToJSON(span).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'navigation') {\n return;\n }\n\n // A navigation with no preceding interaction can't produce a soft navigation, so there is\n // nothing to wait for. Dropping the pending span here also keeps us from binding a stale one.\n _pendingNavigation =\n _lastInteractionTimestamp != null ? { span, interactionTimestamp: _lastInteractionTimestamp } : undefined;\n });\n\n const bindInteractionToNavigationSpan = ({ entries }: { entries: PerformanceEntry[] }): void => {\n for (const entry of entries) {\n const pending = _pendingNavigation;\n if (!pending || !isPerformanceEventTiming(entry) || !entry.interactionId) {\n continue;\n }\n\n if (Math.abs(entry.startTime - pending.interactionTimestamp) > INTERACTION_MATCH_TOLERANCE_MS) {\n continue;\n }\n\n _interactionIdToNavigationSpan.set(entry.interactionId, pending.span);\n _pendingNavigation = undefined;\n }\n };\n\n // `durationThreshold: 0` is applied for `event` by the shared observer, which matters here:\n // interactions below the 104ms default would otherwise never surface an `interactionId`.\n addPerformanceInstrumentationHandler('event', bindInteractionToNavigationSpan);\n addPerformanceInstrumentationHandler('first-input', bindInteractionToNavigationSpan);\n\n addPerformanceInstrumentationHandler('soft-navigation', ({ entries }) => {\n for (const entry of entries as PerformanceSoftNavigation[]) {\n const span = _interactionIdToNavigationSpan.get(entry.interactionId);\n if (!span) {\n DEBUG_BUILD && debug.log(`[SoftNav] No navigation span found for soft navigation ${entry.navigationId}`, entry);\n continue;\n }\n\n _navigationIdToNavigationSpan.set(entry.navigationId, span);\n // Best effort: the soft navigation entry usually lands well within the navigation span's idle\n // window, but if the span has already been sent this attribute is dropped.\n span.setAttribute(BROWSER_NAVIGATION_ID, entry.navigationId);\n }\n });\n}\n\n/**\n * The navigation span a soft navigation web vital belongs to, or `undefined` if the metric isn't\n * for a soft navigation or we failed to correlate it.\n */\nexport function getNavigationSpanForMetric(metric: SoftNavMetric): Span | undefined {\n if (metric.navigationType !== 'soft-navigation') {\n return undefined;\n }\n\n const span = _navigationIdToNavigationSpan.get(metric.navigationId);\n if (span) {\n return span;\n }\n\n // The `soft-navigation` observer may not have run for this navigation yet - entries from\n // different observers aren't delivered in a guaranteed order - so fall back to the join key the\n // metric carries itself.\n return metric.navigationInteractionId != null\n ? _interactionIdToNavigationSpan.get(metric.navigationInteractionId)\n : undefined;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"softNavs.js","sources":["../../../src/web-vitals/softNavs.ts"],"sourcesContent":["import type { Client, Span } from '@sentry/core';\nimport { debug, LRUMap, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core';\nimport { BROWSER_NAVIGATION_ID } from '@sentry/conventions/attributes';\nimport { DEBUG_BUILD } from '../debug-build';\nimport type { PerformanceSoftNavigation } from '../instrumentation/performanceObserver';\nimport { addPerformanceInstrumentationHandler, isPerformanceEventTiming } from '../instrumentation/performanceObserver';\nimport { WINDOW } from '../types';\n\n/**\n * A page only ever needs its most recent navigations to still be joinable: web vitals for a soft\n * navigation are finalized at the next soft navigation or on pagehide, never later than that.\n */\nconst MAX_TRACKED_NAVIGATIONS = 5;\n\n/**\n * Tolerance when matching a DOM event's `timeStamp` against the `startTime` of its Event Timing\n * entry. Both are `DOMHighResTimeStamp`s from the same clock, so this only absorbs rounding.\n */\nconst INTERACTION_MATCH_TOLERANCE_MS = 5;\n\ninterface SoftNavMetric {\n navigationType: string;\n navigationId: number;\n navigationInteractionId?: number;\n}\n\ninterface PendingNavigation {\n span: Span;\n interactionTimestamp: number;\n}\n\ninterface PendingInteraction {\n interactionId: number;\n interactionTimestamp: number;\n}\n\n// The navigation span whose triggering interaction we haven't identified yet.\nlet _pendingNavigation: PendingNavigation | undefined;\n// The interaction whose Event Timing entry arrived before any navigation span claimed it.\nlet _pendingInteraction: PendingInteraction | undefined;\n// The timestamp of the most recent trusted click/keydown, i.e. our best guess at the interaction\n// that a history change happening right now was driven by.\nlet _lastInteractionTimestamp: number | undefined;\n\nconst _interactionIdToNavigationSpan = new LRUMap<number, Span>(MAX_TRACKED_NAVIGATIONS);\nconst _navigationIdToNavigationSpan = new LRUMap<number, Span>(MAX_TRACKED_NAVIGATIONS);\n\nlet _correlationStarted = false;\n\n/**\n * Whether an Event Timing entry's `startTime` and a DOM event's `timeStamp` name the same\n * interaction.\n */\nfunction interactionMatches(entryStartTime: number, interactionTimestamp: number): boolean {\n return Math.abs(entryStartTime - interactionTimestamp) <= INTERACTION_MATCH_TOLERANCE_MS;\n}\n\n/**\n * Whether the browser can report web vitals for soft navigations.\n *\n * This mirrors web-vitals' own feature detection: passing `reportSoftNavs` on a browser that fails\n * this check is a no-op there, so it has to be a no-op here too.\n */\nexport function supportsSoftNavigations(): boolean {\n try {\n return (\n PerformanceObserver.supportedEntryTypes.includes('soft-navigation') &&\n // Older implementations exposed this as an attribute rather than a method. Only the method\n // form shipped unflagged, so it's what web-vitals gates on.\n typeof (\n WINDOW as {\n PerformanceSoftNavigation?: { prototype?: { getLargestInteractionContentfulPaint?: unknown } };\n }\n ).PerformanceSoftNavigation?.prototype?.getLargestInteractionContentfulPaint === 'function'\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Start correlating the browser's soft navigations with the SDK's navigation spans.\n *\n * A navigation span is created synchronously on the history change, but the browser only mints the\n * `soft-navigation` entry (and with it the `navigationId` that web vitals are reported against)\n * once the navigation has been confirmed by a paint. So the `navigationId` cannot be known at span\n * creation time and the two have to be joined after the fact.\n *\n * The join key is the `interactionId` of the interaction that drove the navigation: per the Soft\n * Navigations spec the `soft-navigation` entry carries the `interactionId` of the interaction that\n * triggered it, which is the same id the interaction's own `PerformanceEventTiming` entry carries.\n * So we bind a navigation span to the interaction it happened during, and the soft navigation\n * joins back to that span through the shared id.\n *\n * This is inherently partial. Navigations that don't meet the browser's soft navigation heuristic\n * (programmatic navigations, navigations that never paint, back/forward from the browser chrome)\n * produce no entry at all, so those navigation spans simply have no web vitals.\n */\nexport function startSoftNavigationCorrelation(client: Client): void {\n if (_correlationStarted || !supportsSoftNavigations()) {\n return;\n }\n _correlationStarted = true;\n\n const onInteraction = (event: Event): void => {\n if (event.isTrusted) {\n _lastInteractionTimestamp = event.timeStamp;\n }\n };\n // Only click and keydown can start a soft navigation, which is also what the SDK's redirect\n // detection listens for.\n WINDOW.addEventListener('click', onInteraction, { capture: true, passive: true });\n WINDOW.addEventListener('keydown', onInteraction, { capture: true, passive: true });\n\n client.on('spanStart', span => {\n if (spanToJSON(span).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'navigation') {\n return;\n }\n\n // A navigation with no preceding interaction can't produce a soft navigation, so there is\n // nothing to wait for. Dropping the pending span here also keeps us from binding a stale one.\n _pendingNavigation = undefined;\n const interactionTimestamp = _lastInteractionTimestamp;\n if (interactionTimestamp == null) {\n return;\n }\n\n // The interaction's entry may already be here: the router code that starts this span races the\n // paint that flushes the entry, so either one can win.\n if (_pendingInteraction?.interactionTimestamp === interactionTimestamp) {\n _interactionIdToNavigationSpan.set(_pendingInteraction.interactionId, span);\n _pendingInteraction = undefined;\n return;\n }\n\n _pendingNavigation = { span, interactionTimestamp };\n });\n\n const bindInteractionToNavigationSpan = ({ entries }: { entries: PerformanceEntry[] }): void => {\n for (const entry of entries) {\n if (!isPerformanceEventTiming(entry) || !entry.interactionId) {\n continue;\n }\n\n const pending = _pendingNavigation;\n if (pending && interactionMatches(entry.startTime, pending.interactionTimestamp)) {\n _interactionIdToNavigationSpan.set(entry.interactionId, pending.span);\n _pendingNavigation = undefined;\n continue;\n }\n\n // Once a navigation span has claimed this interaction, only a span that is still waiting can\n // rebind it, which the check above already allows. Holding on to the interaction's remaining\n // entries would instead let an unrelated later navigation claim it.\n if (_interactionIdToNavigationSpan.get(entry.interactionId)) {\n continue;\n }\n\n // The navigation span this interaction drove may still be on its way, so hold on to the\n // interaction instead of dropping it. Only the most recent one is worth keeping:\n // `_lastInteractionTimestamp` is what `spanStart` matches against and it only moves forward,\n // so an entry that doesn't match it now can never match it later.\n if (_lastInteractionTimestamp != null && interactionMatches(entry.startTime, _lastInteractionTimestamp)) {\n _pendingInteraction = { interactionId: entry.interactionId, interactionTimestamp: _lastInteractionTimestamp };\n }\n }\n };\n\n // `durationThreshold: 0` is applied for `event` by the shared observer, which matters here:\n // interactions below the 104ms default would otherwise never surface an `interactionId`.\n addPerformanceInstrumentationHandler('event', bindInteractionToNavigationSpan);\n addPerformanceInstrumentationHandler('first-input', bindInteractionToNavigationSpan);\n\n addPerformanceInstrumentationHandler('soft-navigation', ({ entries }) => {\n for (const entry of entries as PerformanceSoftNavigation[]) {\n const span = _interactionIdToNavigationSpan.get(entry.interactionId);\n if (!span) {\n DEBUG_BUILD && debug.log(`[SoftNav] No navigation span found for soft navigation ${entry.navigationId}`, entry);\n continue;\n }\n\n _navigationIdToNavigationSpan.set(entry.navigationId, span);\n // Best effort: the soft navigation entry usually lands well within the navigation span's idle\n // window, but if the span has already been sent this attribute is dropped.\n span.setAttribute(BROWSER_NAVIGATION_ID, entry.navigationId);\n }\n });\n}\n\n/**\n * The navigation span a soft navigation web vital belongs to, or `undefined` if the metric isn't\n * for a soft navigation or we failed to correlate it.\n */\nexport function getNavigationSpanForMetric(metric: SoftNavMetric): Span | undefined {\n if (metric.navigationType !== 'soft-navigation') {\n return undefined;\n }\n\n const span = _navigationIdToNavigationSpan.get(metric.navigationId);\n if (span) {\n return span;\n }\n\n // The `soft-navigation` observer may not have run for this navigation yet - entries from\n // different observers aren't delivered in a guaranteed order - so fall back to the join key the\n // metric carries itself.\n return metric.navigationInteractionId != null\n ? _interactionIdToNavigationSpan.get(metric.navigationInteractionId)\n : undefined;\n}\n"],"names":[],"mappings":"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"}
|