Skip to content

Commit ab0e012

Browse files
committed
fix(browser): Handle more edge cases with INP
1 parent 1dad6cd commit ab0e012

File tree

5 files changed

+87
-8
lines changed

5 files changed

+87
-8
lines changed

packages/browser-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export {
1212
startTrackingLongTasks,
1313
startTrackingWebVitals,
1414
startTrackingINP,
15+
registerInpInteractionListener,
1516
} from './metrics/browserMetrics';
1617

1718
export { addClickKeypressInstrumentationHandler } from './instrument/dom';

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export function startTrackingInteractions(): void {
157157
});
158158
}
159159

160-
export { startTrackingINP } from './inp';
160+
export { startTrackingINP, registerInpInteractionListener } from './inp';
161161

162162
/** Starts tracking the Cumulative Layout Shift on the current page. */
163163
function _trackCLS(): () => void {

packages/browser-utils/src/metrics/inp.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,21 @@ import {
1212
} from '@sentry/core';
1313
import type { Integration, SpanAttributes } from '@sentry/types';
1414
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString } from '@sentry/utils';
15-
import { addInpInstrumentationHandler } from './instrument';
15+
import {
16+
addInpInstrumentationHandler,
17+
addPerformanceInstrumentationHandler,
18+
isPerformanceEventTiming,
19+
} from './instrument';
1620
import { getBrowserPerformanceAPI, msToSec } from './utils';
1721

22+
// We only care about name here
23+
interface PartialRouteInfo {
24+
name: string | undefined;
25+
}
26+
27+
const LAST_INTERACTIONS: number[] = [];
28+
const INTERACTIONS_ROUTE_MAP = new Map<number, string>();
29+
1830
/**
1931
* Start tracking INP webvital events.
2032
*/
@@ -74,6 +86,7 @@ function _trackINP(): () => void {
7486
return;
7587
}
7688

89+
const { interactionId } = entry;
7790
const interactionType = INP_ENTRY_MAP[entry.name];
7891

7992
const options = client.getOptions();
@@ -84,9 +97,15 @@ function _trackINP(): () => void {
8497
const activeSpan = getActiveSpan();
8598
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
8699

87-
// If there is no active span, we fall back to look at the transactionName on the scope
88-
// This is set if the pageload/navigation span is already finished,
89-
const routeName = rootSpan ? spanToJSON(rootSpan).description : scope.getScopeData().transactionName;
100+
// We first try to lookup the route name from our INTERACTIONS_ROUTE_MAP,
101+
// where we cache the route per interactionId
102+
const cachedRouteName = interactionId != null ? INTERACTIONS_ROUTE_MAP.get(interactionId) : undefined;
103+
104+
// Else, we try to use the active span.
105+
// Finally, we fall back to look at the transactionName on the scope
106+
const routeName =
107+
cachedRouteName || (rootSpan ? spanToJSON(rootSpan).description : scope.getScopeData().transactionName);
108+
90109
const user = scope.getUser();
91110

92111
// We need to get the replay, user, and activeTransaction from the current scope
@@ -134,3 +153,39 @@ function _trackINP(): () => void {
134153
span.end(startTime + duration);
135154
});
136155
}
156+
157+
/** Register a listener to cache route information for INP interactions. */
158+
export function registerInpInteractionListener(latestRoute: PartialRouteInfo): void {
159+
const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => {
160+
entries.forEach(entry => {
161+
if (!isPerformanceEventTiming(entry) || !latestRoute.name) {
162+
return;
163+
}
164+
165+
const interactionId = entry.interactionId;
166+
if (interactionId == null) {
167+
return;
168+
}
169+
170+
// If the interaction was already recorded before, nothing more to do
171+
if (LAST_INTERACTIONS.includes(interactionId)) {
172+
return;
173+
}
174+
175+
// We keep max. 10 interactions in the list, then remove the oldest one & clean up
176+
if (LAST_INTERACTIONS.length > 10) {
177+
const last = LAST_INTERACTIONS.shift() as number;
178+
INTERACTIONS_ROUTE_MAP.delete(last);
179+
}
180+
181+
// We add the interaction to the list of recorded interactions
182+
// and store the route information for this interaction
183+
// (we clone the object because it is mutated when it changes)
184+
LAST_INTERACTIONS.push(interactionId);
185+
INTERACTIONS_ROUTE_MAP.set(interactionId, latestRoute.name);
186+
});
187+
};
188+
189+
addPerformanceInstrumentationHandler('event', handleEntries);
190+
addPerformanceInstrumentationHandler('first-input', handleEntries);
191+
}

packages/browser-utils/src/metrics/instrument.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { onLCP } from './web-vitals/getLCP';
88
import { observe } from './web-vitals/lib/observe';
99
import { onTTFB } from './web-vitals/onTTFB';
1010

11-
type InstrumentHandlerTypePerformanceObserver = 'longtask' | 'event' | 'navigation' | 'paint' | 'resource';
11+
type InstrumentHandlerTypePerformanceObserver =
12+
| 'longtask'
13+
| 'event'
14+
| 'navigation'
15+
| 'paint'
16+
| 'resource'
17+
| 'first-input';
1218

1319
type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb' | 'inp';
1420

@@ -319,3 +325,10 @@ function getCleanupCallback(
319325
}
320326
};
321327
}
328+
329+
/**
330+
* Check if a PerformanceEntry is a PerformanceEventTiming by checking for the `duration` property.
331+
*/
332+
export function isPerformanceEventTiming(entry: PerformanceEntry): entry is PerformanceEventTiming {
333+
return 'duration' in entry;
334+
}

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
addHistoryInstrumentationHandler,
44
addPerformanceEntries,
5+
registerInpInteractionListener,
56
startTrackingINP,
67
startTrackingInteractions,
78
startTrackingLongTasks,
@@ -40,6 +41,11 @@ import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from
4041

4142
export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
4243

44+
interface RouteInfo {
45+
name: string | undefined;
46+
source: TransactionSource | undefined;
47+
}
48+
4349
/** Options for Browser Tracing integration */
4450
export interface BrowserTracingOptions {
4551
/**
@@ -204,7 +210,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
204210
startTrackingInteractions();
205211
}
206212

207-
const latestRoute: { name: string | undefined; source: TransactionSource | undefined } = {
213+
const latestRoute: RouteInfo = {
208214
name: undefined,
209215
source: undefined,
210216
};
@@ -375,6 +381,10 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
375381
registerInteractionListener(idleTimeout, finalTimeout, childSpanTimeout, latestRoute);
376382
}
377383

384+
if (enableInp) {
385+
registerInpInteractionListener(latestRoute);
386+
}
387+
378388
instrumentOutgoingRequests({
379389
traceFetch,
380390
traceXHR,
@@ -439,7 +449,7 @@ function registerInteractionListener(
439449
idleTimeout: BrowserTracingOptions['idleTimeout'],
440450
finalTimeout: BrowserTracingOptions['finalTimeout'],
441451
childSpanTimeout: BrowserTracingOptions['childSpanTimeout'],
442-
latestRoute: { name: string | undefined; source: TransactionSource | undefined },
452+
latestRoute: RouteInfo,
443453
): void {
444454
let inflightInteractionSpan: Span | undefined;
445455
const registerInteractionTransaction = (): void => {

0 commit comments

Comments
 (0)