@@ -12,9 +12,21 @@ import {
12
12
} from '@sentry/core' ;
13
13
import type { Integration , SpanAttributes } from '@sentry/types' ;
14
14
import { browserPerformanceTimeOrigin , dropUndefinedKeys , htmlTreeAsString } from '@sentry/utils' ;
15
- import { addInpInstrumentationHandler } from './instrument' ;
15
+ import {
16
+ addInpInstrumentationHandler ,
17
+ addPerformanceInstrumentationHandler ,
18
+ isPerformanceEventTiming ,
19
+ } from './instrument' ;
16
20
import { getBrowserPerformanceAPI , msToSec } from './utils' ;
17
21
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
+
18
30
/**
19
31
* Start tracking INP webvital events.
20
32
*/
@@ -74,6 +86,7 @@ function _trackINP(): () => void {
74
86
return ;
75
87
}
76
88
89
+ const { interactionId } = entry ;
77
90
const interactionType = INP_ENTRY_MAP [ entry . name ] ;
78
91
79
92
const options = client . getOptions ( ) ;
@@ -84,9 +97,15 @@ function _trackINP(): () => void {
84
97
const activeSpan = getActiveSpan ( ) ;
85
98
const rootSpan = activeSpan ? getRootSpan ( activeSpan ) : undefined ;
86
99
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
+
90
109
const user = scope . getUser ( ) ;
91
110
92
111
// We need to get the replay, user, and activeTransaction from the current scope
@@ -134,3 +153,39 @@ function _trackINP(): () => void {
134
153
span . end ( startTime + duration ) ;
135
154
} ) ;
136
155
}
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
+ }
0 commit comments