@@ -194,9 +194,9 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
194
194
const _collectWebVitals = startTrackingWebVitals ( ) ;
195
195
196
196
/** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
197
- const interactionIdtoRouteNameMapping : InteractionRouteNameMapping = { } ;
197
+ const interactionIdToRouteNameMapping : InteractionRouteNameMapping = { } ;
198
198
if ( options . enableInp ) {
199
- startTrackingINP ( interactionIdtoRouteNameMapping ) ;
199
+ startTrackingINP ( interactionIdToRouteNameMapping ) ;
200
200
}
201
201
202
202
if ( options . enableLongTask ) {
@@ -411,7 +411,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
411
411
}
412
412
413
413
if ( options . enableInp ) {
414
- registerInpInteractionListener ( interactionIdtoRouteNameMapping , latestRoute ) ;
414
+ registerInpInteractionListener ( interactionIdToRouteNameMapping , latestRoute ) ;
415
415
}
416
416
417
417
instrumentOutgoingRequests ( {
@@ -541,13 +541,13 @@ const MAX_INTERACTIONS = 10;
541
541
542
542
/** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
543
543
function registerInpInteractionListener (
544
- interactionIdtoRouteNameMapping : InteractionRouteNameMapping ,
544
+ interactionIdToRouteNameMapping : InteractionRouteNameMapping ,
545
545
latestRoute : {
546
546
name : string | undefined ;
547
547
context : TransactionContext | undefined ;
548
548
} ,
549
549
) : void {
550
- addPerformanceInstrumentationHandler ( 'event' , ( { entries } ) => {
550
+ const handleEntries = ( { entries } : { entries : PerformanceEntry [ ] } ) : void => {
551
551
const client = getClient ( ) ;
552
552
// We need to get the replay, user, and activeTransaction from the current scope
553
553
// so that we can associate replay id, profile id, and a user display to the span
@@ -562,38 +562,70 @@ function registerInpInteractionListener(
562
562
const user = currentScope !== undefined ? currentScope . getUser ( ) : undefined ;
563
563
for ( const entry of entries ) {
564
564
if ( isPerformanceEventTiming ( entry ) ) {
565
+ const interactionId = entry . interactionId ;
566
+ if ( interactionId === undefined ) {
567
+ return ;
568
+ }
569
+ const existingInteraction = interactionIdToRouteNameMapping [ interactionId ] ;
565
570
const duration = entry . duration ;
566
- const keys = Object . keys ( interactionIdtoRouteNameMapping ) ;
571
+ const startTime = entry . startTime ;
572
+ const keys = Object . keys ( interactionIdToRouteNameMapping ) ;
567
573
const minInteractionId =
568
574
keys . length > 0
569
575
? keys . reduce ( ( a , b ) => {
570
- return interactionIdtoRouteNameMapping [ a ] . duration < interactionIdtoRouteNameMapping [ b ] . duration
576
+ return interactionIdToRouteNameMapping [ a ] . duration < interactionIdToRouteNameMapping [ b ] . duration
571
577
? a
572
578
: b ;
573
579
} )
574
580
: 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.
577
606
const routeName = latestRoute . name ;
578
607
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 ) {
581
610
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
582
- delete interactionIdtoRouteNameMapping [ minInteractionId ] ;
611
+ delete interactionIdToRouteNameMapping [ minInteractionId ] ;
583
612
}
584
- interactionIdtoRouteNameMapping [ interactionId ] = {
613
+ interactionIdToRouteNameMapping [ interactionId ] = {
585
614
routeName,
586
615
duration,
587
616
parentContext,
588
617
user,
589
618
activeTransaction,
590
619
replayId,
620
+ startTime,
591
621
} ;
592
622
}
593
623
}
594
624
}
595
625
}
596
- } ) ;
626
+ } ;
627
+ addPerformanceInstrumentationHandler ( 'event' , handleEntries ) ;
628
+ addPerformanceInstrumentationHandler ( 'first-input' , handleEntries ) ;
597
629
}
598
630
599
631
function getSource ( context : TransactionContext ) : TransactionSource | undefined {
0 commit comments