Skip to content

Commit e5e72b0

Browse files
Fixes various bugs with inp interaction tracking
1 parent 5a87aec commit e5e72b0

File tree

5 files changed

+106
-42
lines changed

5 files changed

+106
-42
lines changed

packages/tracing-internal/src/browser/browserTracingIntegration.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,9 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
194194
const _collectWebVitals = startTrackingWebVitals();
195195

196196
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
197-
const interactionIdtoRouteNameMapping: InteractionRouteNameMapping = {};
197+
const interactionIdToRouteNameMapping: InteractionRouteNameMapping = {};
198198
if (options.enableInp) {
199-
startTrackingINP(interactionIdtoRouteNameMapping);
199+
startTrackingINP(interactionIdToRouteNameMapping);
200200
}
201201

202202
if (options.enableLongTask) {
@@ -411,7 +411,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
411411
}
412412

413413
if (options.enableInp) {
414-
registerInpInteractionListener(interactionIdtoRouteNameMapping, latestRoute);
414+
registerInpInteractionListener(interactionIdToRouteNameMapping, latestRoute);
415415
}
416416

417417
instrumentOutgoingRequests({
@@ -541,13 +541,13 @@ const MAX_INTERACTIONS = 10;
541541

542542
/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
543543
function registerInpInteractionListener(
544-
interactionIdtoRouteNameMapping: InteractionRouteNameMapping,
544+
interactionIdToRouteNameMapping: InteractionRouteNameMapping,
545545
latestRoute: {
546546
name: string | undefined;
547547
context: TransactionContext | undefined;
548548
},
549549
): void {
550-
addPerformanceInstrumentationHandler('event', ({ entries }) => {
550+
const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => {
551551
const client = getClient();
552552
// We need to get the replay, user, and activeTransaction from the current scope
553553
// so that we can associate replay id, profile id, and a user display to the span
@@ -562,38 +562,70 @@ function registerInpInteractionListener(
562562
const user = currentScope !== undefined ? currentScope.getUser() : undefined;
563563
for (const entry of entries) {
564564
if (isPerformanceEventTiming(entry)) {
565+
const interactionId = entry.interactionId;
566+
if (interactionId === undefined) {
567+
return;
568+
}
569+
const existingInteraction = interactionIdToRouteNameMapping[interactionId];
565570
const duration = entry.duration;
566-
const keys = Object.keys(interactionIdtoRouteNameMapping);
571+
const startTime = entry.startTime;
572+
const keys = Object.keys(interactionIdToRouteNameMapping);
567573
const minInteractionId =
568574
keys.length > 0
569575
? keys.reduce((a, b) => {
570-
return interactionIdtoRouteNameMapping[a].duration < interactionIdtoRouteNameMapping[b].duration
576+
return interactionIdToRouteNameMapping[a].duration < interactionIdToRouteNameMapping[b].duration
571577
? a
572578
: b;
573579
})
574580
: undefined;
575-
if (minInteractionId === undefined || duration > interactionIdtoRouteNameMapping[minInteractionId].duration) {
576-
const interactionId = entry.interactionId;
581+
// For a first input event to be considered, we must check that an interaction event does not already exist with the same duration and start time.
582+
// This is also checked in the web-vitals library.
583+
if (entry.entryType === 'first-input') {
584+
const matchingEntry = keys
585+
.map(key => interactionIdToRouteNameMapping[key])
586+
.some(interaction => {
587+
return interaction.duration === duration && interaction.startTime === startTime;
588+
});
589+
if (matchingEntry) {
590+
return;
591+
}
592+
}
593+
// Interactions with an id of 0 and are not first-input are not valid.
594+
if (!interactionId) {
595+
return;
596+
}
597+
// If the interaction already exists, we want to use the duration of the longest entry, since that is what the INP metric uses.
598+
if (existingInteraction) {
599+
existingInteraction.duration = Math.max(existingInteraction.duration, duration);
600+
} else if (
601+
keys.length < MAX_INTERACTIONS ||
602+
minInteractionId === undefined ||
603+
duration > interactionIdToRouteNameMapping[minInteractionId].duration
604+
) {
605+
// If the interaction does not exist, we want to add it to the mapping if there is space, or if the duration is longer than the shortest entry.
577606
const routeName = latestRoute.name;
578607
const parentContext = latestRoute.context;
579-
if (interactionId && routeName && parentContext) {
580-
if (minInteractionId && Object.keys(interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) {
608+
if (routeName && parentContext) {
609+
if (minInteractionId && Object.keys(interactionIdToRouteNameMapping).length >= MAX_INTERACTIONS) {
581610
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
582-
delete interactionIdtoRouteNameMapping[minInteractionId];
611+
delete interactionIdToRouteNameMapping[minInteractionId];
583612
}
584-
interactionIdtoRouteNameMapping[interactionId] = {
613+
interactionIdToRouteNameMapping[interactionId] = {
585614
routeName,
586615
duration,
587616
parentContext,
588617
user,
589618
activeTransaction,
590619
replayId,
620+
startTime,
591621
};
592622
}
593623
}
594624
}
595625
}
596-
});
626+
};
627+
addPerformanceInstrumentationHandler('event', handleEntries);
628+
addPerformanceInstrumentationHandler('first-input', handleEntries);
597629
}
598630

599631
function getSource(context: TransactionContext): TransactionSource | undefined {

packages/tracing-internal/src/browser/browsertracing.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export class BrowserTracing implements Integration {
193193
private _collectWebVitals: () => void;
194194

195195
private _hasSetTracePropagationTargets: boolean;
196-
private _interactionIdtoRouteNameMapping: InteractionRouteNameMapping;
196+
private _interactionIdToRouteNameMapping: InteractionRouteNameMapping;
197197
private _latestRoute: {
198198
name: string | undefined;
199199
context: TransactionContext | undefined;
@@ -235,10 +235,10 @@ export class BrowserTracing implements Integration {
235235

236236
this._collectWebVitals = startTrackingWebVitals();
237237
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
238-
this._interactionIdtoRouteNameMapping = {};
238+
this._interactionIdToRouteNameMapping = {};
239239

240240
if (this.options.enableInp) {
241-
startTrackingINP(this._interactionIdtoRouteNameMapping);
241+
startTrackingINP(this._interactionIdToRouteNameMapping);
242242
}
243243
if (this.options.enableLongTask) {
244244
startTrackingLongTasks();
@@ -489,7 +489,7 @@ export class BrowserTracing implements Integration {
489489

490490
/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
491491
private _registerInpInteractionListener(): void {
492-
addPerformanceInstrumentationHandler('event', ({ entries }) => {
492+
const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => {
493493
const client = getClient();
494494
// We need to get the replay, user, and activeTransaction from the current scope
495495
// so that we can associate replay id, profile id, and a user display to the span
@@ -504,42 +504,71 @@ export class BrowserTracing implements Integration {
504504
const user = currentScope !== undefined ? currentScope.getUser() : undefined;
505505
for (const entry of entries) {
506506
if (isPerformanceEventTiming(entry)) {
507+
const interactionId = entry.interactionId;
508+
if (interactionId === undefined) {
509+
return;
510+
}
511+
const existingInteraction = this._interactionIdToRouteNameMapping[interactionId];
507512
const duration = entry.duration;
508-
const keys = Object.keys(this._interactionIdtoRouteNameMapping);
513+
const startTime = entry.startTime;
514+
const keys = Object.keys(this._interactionIdToRouteNameMapping);
509515
const minInteractionId =
510516
keys.length > 0
511517
? keys.reduce((a, b) => {
512-
return this._interactionIdtoRouteNameMapping[a].duration <
513-
this._interactionIdtoRouteNameMapping[b].duration
518+
return this._interactionIdToRouteNameMapping[a].duration <
519+
this._interactionIdToRouteNameMapping[b].duration
514520
? a
515521
: b;
516522
})
517523
: undefined;
518-
if (
524+
// For a first input event to be considered, we must check that an interaction event does not already exist with the same duration and start time.
525+
// This is also checked in the web-vitals library.
526+
if (entry.entryType === 'first-input') {
527+
const matchingEntry = keys
528+
.map(key => this._interactionIdToRouteNameMapping[key])
529+
.some(interaction => {
530+
return interaction.duration === duration && interaction.startTime === startTime;
531+
});
532+
if (matchingEntry) {
533+
return;
534+
}
535+
}
536+
// Interactions with an id of 0 and are not first-input are not valid.
537+
if (!interactionId) {
538+
return;
539+
}
540+
// If the interaction already exists, we want to use the duration of the longest entry, since that is what the INP metric uses.
541+
if (existingInteraction) {
542+
existingInteraction.duration = Math.max(existingInteraction.duration, duration);
543+
} else if (
544+
keys.length < MAX_INTERACTIONS ||
519545
minInteractionId === undefined ||
520-
duration > this._interactionIdtoRouteNameMapping[minInteractionId].duration
546+
duration > this._interactionIdToRouteNameMapping[minInteractionId].duration
521547
) {
522-
const interactionId = entry.interactionId;
548+
// If the interaction does not exist, we want to add it to the mapping if there is space, or if the duration is longer than the shortest entry.
523549
const routeName = this._latestRoute.name;
524550
const parentContext = this._latestRoute.context;
525-
if (interactionId && routeName && parentContext) {
526-
if (minInteractionId && Object.keys(this._interactionIdtoRouteNameMapping).length >= MAX_INTERACTIONS) {
551+
if (routeName && parentContext) {
552+
if (minInteractionId && Object.keys(this._interactionIdToRouteNameMapping).length >= MAX_INTERACTIONS) {
527553
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
528-
delete this._interactionIdtoRouteNameMapping[minInteractionId];
554+
delete this._interactionIdToRouteNameMapping[minInteractionId];
529555
}
530-
this._interactionIdtoRouteNameMapping[interactionId] = {
556+
this._interactionIdToRouteNameMapping[interactionId] = {
531557
routeName,
532558
duration,
533559
parentContext,
534560
user,
535561
activeTransaction,
536562
replayId,
563+
startTime,
537564
};
538565
}
539566
}
540567
}
541568
}
542-
});
569+
};
570+
addPerformanceInstrumentationHandler('event', handleEntries);
571+
addPerformanceInstrumentationHandler('first-input', handleEntries);
543572
}
544573
}
545574

packages/tracing-internal/src/browser/instrument.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import { onINP } from './web-vitals/getINP';
77
import { onLCP } from './web-vitals/getLCP';
88
import { observe } from './web-vitals/lib/observe';
99

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

1218
type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'inp';
1319

@@ -144,7 +150,7 @@ export function addInpInstrumentationHandler(
144150
}
145151

146152
export function addPerformanceInstrumentationHandler(
147-
type: 'event',
153+
type: 'event' | 'first-input',
148154
callback: (data: { entries: ((PerformanceEntry & { target?: unknown | null }) | PerformanceEventTiming)[] }) => void,
149155
): CleanupHandlerCallback;
150156
export function addPerformanceInstrumentationHandler(

packages/tracing-internal/src/browser/metrics/index.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
231231
};
232232

233233
/** Starts tracking the Interaction to Next Paint on the current page. */
234-
function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping): () => void {
234+
function _trackINP(interactionIdToRouteNameMapping: InteractionRouteNameMapping): () => void {
235235
return addInpInstrumentationHandler(({ metric }) => {
236236
if (metric.value === undefined) {
237237
return;
@@ -248,16 +248,12 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping)
248248
/** Build the INP span, create an envelope from the span, and then send the envelope */
249249
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
250250
const duration = msToSec(metric.value);
251-
const { routeName, parentContext, activeTransaction, user, replayId } =
252-
entry.interactionId !== undefined
253-
? interactionIdtoRouteNameMapping[entry.interactionId]
254-
: {
255-
routeName: undefined,
256-
parentContext: undefined,
257-
activeTransaction: undefined,
258-
user: undefined,
259-
replayId: undefined,
260-
};
251+
const interaction =
252+
entry.interactionId !== undefined ? interactionIdToRouteNameMapping[entry.interactionId] : undefined;
253+
if (interaction === undefined) {
254+
return;
255+
}
256+
const { routeName, parentContext, activeTransaction, user, replayId } = interaction;
261257
const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined;
262258
// eslint-disable-next-line deprecation/deprecation
263259
const profileId = activeTransaction !== undefined ? activeTransaction.getProfileId() : undefined;

packages/tracing-internal/src/browser/web-vitals/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,6 @@ export type InteractionRouteNameMapping = {
172172
user?: User;
173173
activeTransaction?: Transaction;
174174
replayId?: string;
175+
startTime: number;
175176
};
176177
};

0 commit comments

Comments
 (0)