@@ -5,13 +5,15 @@ import {
55 getCurrentScope ,
66 getRootSpan ,
77 htmlTreeAsString ,
8+ isBrowser ,
89 SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME ,
910 SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT ,
1011 SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE ,
1112 SEMANTIC_ATTRIBUTE_SENTRY_OP ,
1213 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
1314 spanToJSON ,
1415} from '@sentry/core' ;
16+ import { WINDOW } from '../types' ;
1517import type { InstrumentationHandlerCallback } from './instrument' ;
1618import {
1719 addInpInstrumentationHandler ,
@@ -20,8 +22,16 @@ import {
2022} from './instrument' ;
2123import { getBrowserPerformanceAPI , msToSec , startStandaloneWebVitalSpan } from './utils' ;
2224
25+ interface InteractionContext {
26+ span : Span | undefined ;
27+ elementName : string ;
28+ }
29+
2330const LAST_INTERACTIONS : number [ ] = [ ] ;
24- const INTERACTIONS_SPAN_MAP = new Map < number , Span > ( ) ;
31+ const INTERACTIONS_SPAN_MAP = new Map < number , InteractionContext > ( ) ;
32+
33+ // Map to store element names by timestamp, since we get the DOM event before the PerformanceObserver entry
34+ const ELEMENT_NAME_TIMESTAMP_MAP = new Map < number , string > ( ) ;
2535
2636/**
2737 * 60 seconds is the maximum for a plausible INP value
@@ -111,17 +121,17 @@ export const _onInp: InstrumentationHandlerCallback = ({ metric }) => {
111121 const activeSpan = getActiveSpan ( ) ;
112122 const rootSpan = activeSpan ? getRootSpan ( activeSpan ) : undefined ;
113123
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 ;
124+ // We first try to lookup the interaction context from our INTERACTIONS_SPAN_MAP,
125+ // where we cache the route and element name per interactionId
126+ const cachedInteractionContext = interactionId != null ? INTERACTIONS_SPAN_MAP . get ( interactionId ) : undefined ;
117127
118- const spanToUse = cachedSpan || rootSpan ;
128+ const spanToUse = cachedInteractionContext ?. span || rootSpan ;
119129
120130 // Else, we try to use the active span.
121131 // Finally, we fall back to look at the transactionName on the scope
122132 const routeName = spanToUse ? spanToJSON ( spanToUse ) . description : getCurrentScope ( ) . getScopeData ( ) . transactionName ;
123133
124- const name = htmlTreeAsString ( entry . target ) ;
134+ const name = cachedInteractionContext ?. elementName || htmlTreeAsString ( entry . target ) ;
125135 const attributes : SpanAttributes = {
126136 [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.browser.inp' ,
127137 [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `ui.interaction.${ interactionType } ` ,
@@ -149,12 +159,65 @@ export const _onInp: InstrumentationHandlerCallback = ({ metric }) => {
149159 * Register a listener to cache route information for INP interactions.
150160 */
151161export function registerInpInteractionListener ( ) : void {
162+ // Listen for all interaction events that could contribute to INP
163+ const interactionEvents = Object . keys ( INP_ENTRY_MAP ) ;
164+ if ( isBrowser ( ) ) {
165+ interactionEvents . forEach ( eventType => {
166+ WINDOW . addEventListener ( eventType , captureElementFromEvent , { capture : true , passive : true } ) ;
167+ } ) ;
168+ }
169+
170+ /**
171+ * Captures the element name from a DOM event and stores it in the ELEMENT_NAME_TIMESTAMP_MAP.
172+ */
173+ function captureElementFromEvent ( event : Event ) : void {
174+ const target = event . target as HTMLElement | null ;
175+ if ( ! target ) {
176+ return ;
177+ }
178+
179+ const elementName = htmlTreeAsString ( target ) ;
180+ const timestamp = Math . round ( event . timeStamp ) ;
181+
182+ // Store the element name by timestamp so we can match it with the PerformanceEntry
183+ ELEMENT_NAME_TIMESTAMP_MAP . set ( timestamp , elementName ) ;
184+
185+ // Clean up old
186+ if ( ELEMENT_NAME_TIMESTAMP_MAP . size > 50 ) {
187+ const firstKey = ELEMENT_NAME_TIMESTAMP_MAP . keys ( ) . next ( ) . value ;
188+ if ( firstKey !== undefined ) {
189+ ELEMENT_NAME_TIMESTAMP_MAP . delete ( firstKey ) ;
190+ }
191+ }
192+ }
193+
194+ /**
195+ * Tries to get the element name from the timestamp map.
196+ */
197+ function resolveElementNameFromEntry ( entry : PerformanceEntry ) : string {
198+ const timestamp = Math . round ( entry . startTime ) ;
199+ let elementName = ELEMENT_NAME_TIMESTAMP_MAP . get ( timestamp ) ;
200+
201+ // try nearby timestamps (±5ms)
202+ if ( ! elementName ) {
203+ for ( let offset = - 5 ; offset <= 5 ; offset ++ ) {
204+ const nearbyName = ELEMENT_NAME_TIMESTAMP_MAP . get ( timestamp + offset ) ;
205+ if ( nearbyName ) {
206+ elementName = nearbyName ;
207+ break ;
208+ }
209+ }
210+ }
211+
212+ return elementName || '<unknown>' ;
213+ }
214+
152215 const handleEntries = ( { entries } : { entries : PerformanceEntry [ ] } ) : void => {
153216 const activeSpan = getActiveSpan ( ) ;
154217 const activeRootSpan = activeSpan && getRootSpan ( activeSpan ) ;
155218
156219 entries . forEach ( entry => {
157- if ( ! isPerformanceEventTiming ( entry ) || ! activeRootSpan ) {
220+ if ( ! isPerformanceEventTiming ( entry ) ) {
158221 return ;
159222 }
160223
@@ -168,16 +231,21 @@ export function registerInpInteractionListener(): void {
168231 return ;
169232 }
170233
234+ const elementName = entry . target ? htmlTreeAsString ( entry . target ) : resolveElementNameFromEntry ( entry ) ;
235+
171236 // We keep max. 10 interactions in the list, then remove the oldest one & clean up
172237 if ( LAST_INTERACTIONS . length > 10 ) {
173238 const last = LAST_INTERACTIONS . shift ( ) as number ;
174239 INTERACTIONS_SPAN_MAP . delete ( last ) ;
175240 }
176241
177242 // We add the interaction to the list of recorded interactions
178- // and store the span for this interaction
243+ // and store both the span and element name for this interaction
179244 LAST_INTERACTIONS . push ( interactionId ) ;
180- INTERACTIONS_SPAN_MAP . set ( interactionId , activeRootSpan ) ;
245+ INTERACTIONS_SPAN_MAP . set ( interactionId , {
246+ span : activeRootSpan ,
247+ elementName,
248+ } ) ;
181249 } ) ;
182250 } ;
183251
0 commit comments