@@ -12,6 +12,7 @@ import {
1212 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
1313 spanToJSON ,
1414} from '@sentry/core' ;
15+ import { WINDOW } from '../types' ;
1516import type { InstrumentationHandlerCallback } from './instrument' ;
1617import {
1718 addInpInstrumentationHandler ,
@@ -20,8 +21,16 @@ import {
2021} from './instrument' ;
2122import { getBrowserPerformanceAPI , msToSec , startStandaloneWebVitalSpan } from './utils' ;
2223
24+ interface InteractionContext {
25+ span : Span | undefined ;
26+ elementName : string ;
27+ }
28+
2329const LAST_INTERACTIONS : number [ ] = [ ] ;
24- const INTERACTIONS_SPAN_MAP = new Map < number , Span > ( ) ;
30+ const INTERACTIONS_SPAN_MAP = new Map < number , InteractionContext > ( ) ;
31+
32+ // Map to store element names by timestamp, since we get the DOM event before the PerformanceObserver entry
33+ const ELEMENT_NAME_TIMESTAMP_MAP = new Map < number , string > ( ) ;
2534
2635/**
2736 * 60 seconds is the maximum for a plausible INP value
@@ -111,17 +120,17 @@ export const _onInp: InstrumentationHandlerCallback = ({ metric }) => {
111120 const activeSpan = getActiveSpan ( ) ;
112121 const rootSpan = activeSpan ? getRootSpan ( activeSpan ) : undefined ;
113122
114- // We first try to lookup the span from our INTERACTIONS_SPAN_MAP,
115- // where we cache the route per interactionId
116- const cachedSpan = interactionId != null ? INTERACTIONS_SPAN_MAP . get ( interactionId ) : undefined ;
123+ // We first try to lookup the interaction context from our INTERACTIONS_SPAN_MAP,
124+ // where we cache the route and element name per interactionId
125+ const cachedInteractionContext = interactionId != null ? INTERACTIONS_SPAN_MAP . get ( interactionId ) : undefined ;
117126
118- const spanToUse = cachedSpan || rootSpan ;
127+ const spanToUse = cachedInteractionContext ?. span || rootSpan ;
119128
120129 // Else, we try to use the active span.
121130 // Finally, we fall back to look at the transactionName on the scope
122131 const routeName = spanToUse ? spanToJSON ( spanToUse ) . description : getCurrentScope ( ) . getScopeData ( ) . transactionName ;
123132
124- const name = htmlTreeAsString ( entry . target ) ;
133+ const name = cachedInteractionContext ?. elementName || htmlTreeAsString ( entry . target ) ;
125134 const attributes : SpanAttributes = {
126135 [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.browser.inp' ,
127136 [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `ui.interaction.${ interactionType } ` ,
@@ -149,12 +158,63 @@ export const _onInp: InstrumentationHandlerCallback = ({ metric }) => {
149158 * Register a listener to cache route information for INP interactions.
150159 */
151160export function registerInpInteractionListener ( ) : void {
161+ // Listen for all interaction events that could contribute to INP
162+ const interactionEvents = Object . keys ( INP_ENTRY_MAP ) ;
163+ interactionEvents . forEach ( eventType => {
164+ WINDOW . addEventListener ( eventType , captureElementFromEvent , { capture : true , passive : true } ) ;
165+ } ) ;
166+
167+ /**
168+ * Captures the element name from a DOM event and stores it in the ELEMENT_NAME_TIMESTAMP_MAP.
169+ */
170+ function captureElementFromEvent ( event : Event ) : void {
171+ const target = event . target as HTMLElement | null ;
172+ if ( ! target ) {
173+ return ;
174+ }
175+
176+ const elementName = htmlTreeAsString ( target ) ;
177+ const timestamp = Math . round ( event . timeStamp ) ;
178+
179+ // Store the element name by timestamp so we can match it with the PerformanceEntry
180+ ELEMENT_NAME_TIMESTAMP_MAP . set ( timestamp , elementName ) ;
181+
182+ // Clean up old
183+ if ( ELEMENT_NAME_TIMESTAMP_MAP . size > 50 ) {
184+ const firstKey = ELEMENT_NAME_TIMESTAMP_MAP . keys ( ) . next ( ) . value ;
185+ if ( firstKey !== undefined ) {
186+ ELEMENT_NAME_TIMESTAMP_MAP . delete ( firstKey ) ;
187+ }
188+ }
189+ }
190+
191+ /**
192+ * Tries to get the element name from the timestamp map.
193+ */
194+ function resolveElementNameFromEntry ( entry : PerformanceEntry ) : string {
195+ const timestamp = Math . round ( entry . startTime ) ;
196+ let elementName = ELEMENT_NAME_TIMESTAMP_MAP . get ( timestamp ) ;
197+
198+ // try nearby timestamps (±5ms)
199+ if ( ! elementName ) {
200+ for ( let offset = - 5 ; offset <= 5 ; offset ++ ) {
201+ const nearbyName = ELEMENT_NAME_TIMESTAMP_MAP . get ( timestamp + offset ) ;
202+ if ( nearbyName ) {
203+ elementName = nearbyName ;
204+ break ;
205+ }
206+ }
207+ }
208+
209+ return elementName || '<unknown>' ;
210+ }
211+
152212 const handleEntries = ( { entries } : { entries : PerformanceEntry [ ] } ) : void => {
153213 const activeSpan = getActiveSpan ( ) ;
154214 const activeRootSpan = activeSpan && getRootSpan ( activeSpan ) ;
155215
156216 entries . forEach ( entry => {
157- if ( ! isPerformanceEventTiming ( entry ) || ! activeRootSpan ) {
217+ if ( ! isPerformanceEventTiming ( entry ) ) {
158218 return ;
159219 }
160220
@@ -168,16 +228,21 @@ export function registerInpInteractionListener(): void {
168228 return ;
169229 }
170230
231+ const elementName = entry . target ? htmlTreeAsString ( entry . target ) : resolveElementNameFromEntry ( entry ) ;
232+
171233 // We keep max. 10 interactions in the list, then remove the oldest one & clean up
172234 if ( LAST_INTERACTIONS . length > 10 ) {
173235 const last = LAST_INTERACTIONS . shift ( ) as number ;
174236 INTERACTIONS_SPAN_MAP . delete ( last ) ;
175237 }
176238
177239 // We add the interaction to the list of recorded interactions
178- // and store the span for this interaction
240+ // and store both the span and element name for this interaction
179241 LAST_INTERACTIONS . push ( interactionId ) ;
180- INTERACTIONS_SPAN_MAP . set ( interactionId , activeRootSpan ) ;
242+ INTERACTIONS_SPAN_MAP . set ( interactionId , {
243+ span : activeRootSpan ,
244+ elementName,
245+ } ) ;
181246 } ) ;
182247 } ;
183248
0 commit comments