@@ -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 */
543543function 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
599631function getSource ( context : TransactionContext ) : TransactionSource | undefined {
0 commit comments