@@ -12,6 +12,7 @@ import {
1212 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
1313 spanToJSON ,
1414} from '@sentry/core' ;
15+ import type { InstrumentationHandlerCallback } from './instrument' ;
1516import {
1617 addInpInstrumentationHandler ,
1718 addPerformanceInstrumentationHandler ,
@@ -22,6 +23,11 @@ import { getBrowserPerformanceAPI, msToSec, startStandaloneWebVitalSpan } from '
2223const LAST_INTERACTIONS : number [ ] = [ ] ;
2324const INTERACTIONS_SPAN_MAP = new Map < number , Span > ( ) ;
2425
26+ /**
27+ * 60 seconds is the maximum for a plausible INP value
28+ * (source: Me)
29+ */
30+ const MAX_PLAUSIBLE_INP_DURATION = 60 ;
2531/**
2632 * Start tracking INP webvital events.
2733 */
@@ -67,62 +73,77 @@ const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
6773 input : 'press' ,
6874} ;
6975
70- /** Starts tracking the Interaction to Next Paint on the current page. */
71- function _trackINP ( ) : ( ) => void {
72- return addInpInstrumentationHandler ( ( { metric } ) => {
73- if ( metric . value == undefined ) {
74- return ;
75- }
76+ /** Starts tracking the Interaction to Next Paint on the current page. #
77+ * exported only for testing
78+ */
79+ export function _trackINP ( ) : ( ) => void {
80+ return addInpInstrumentationHandler ( _onInp ) ;
81+ }
82+
83+ /**
84+ * exported only for testing
85+ */
86+ export const _onInp : InstrumentationHandlerCallback = ( { metric } ) => {
87+ if ( metric . value == undefined ) {
88+ return ;
89+ }
7690
77- const entry = metric . entries . find ( entry => entry . duration === metric . value && INP_ENTRY_MAP [ entry . name ] ) ;
91+ const duration = msToSec ( metric . value ) ;
7892
79- if ( ! entry ) {
80- return ;
81- }
93+ // We received occasional reports of hour-long INP values.
94+ // Therefore, we add a sanity check to avoid creating spans for
95+ // unrealistically long INP durations.
96+ if ( duration > MAX_PLAUSIBLE_INP_DURATION ) {
97+ return ;
98+ }
8299
83- const { interactionId } = entry ;
84- const interactionType = INP_ENTRY_MAP [ entry . name ] ;
100+ const entry = metric . entries . find ( entry => entry . duration === metric . value && INP_ENTRY_MAP [ entry . name ] ) ;
85101
86- /** Build the INP span, create an envelope from the span, and then send the envelope */
87- const startTime = msToSec ( ( browserPerformanceTimeOrigin ( ) as number ) + entry . startTime ) ;
88- const duration = msToSec ( metric . value ) ;
89- const activeSpan = getActiveSpan ( ) ;
90- const rootSpan = activeSpan ? getRootSpan ( activeSpan ) : undefined ;
102+ if ( ! entry ) {
103+ return ;
104+ }
91105
92- // We first try to lookup the span from our INTERACTIONS_SPAN_MAP,
93- // where we cache the route per interactionId
94- const cachedSpan = interactionId != null ? INTERACTIONS_SPAN_MAP . get ( interactionId ) : undefined ;
106+ const { interactionId } = entry ;
107+ const interactionType = INP_ENTRY_MAP [ entry . name ] ;
95108
96- const spanToUse = cachedSpan || rootSpan ;
109+ /** Build the INP span, create an envelope from the span, and then send the envelope */
110+ const startTime = msToSec ( ( browserPerformanceTimeOrigin ( ) as number ) + entry . startTime ) ;
111+ const activeSpan = getActiveSpan ( ) ;
112+ const rootSpan = activeSpan ? getRootSpan ( activeSpan ) : undefined ;
97113
98- // Else, we try to use the active span.
99- // Finally, we fall back to look at the transactionName on the scope
100- const routeName = spanToUse ? spanToJSON ( spanToUse ) . description : getCurrentScope ( ) . getScopeData ( ) . transactionName ;
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 ;
101117
102- const name = htmlTreeAsString ( entry . target ) ;
103- const attributes : SpanAttributes = {
104- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.browser.inp' ,
105- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `ui.interaction.${ interactionType } ` ,
106- [ SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME ] : entry . duration ,
107- } ;
118+ const spanToUse = cachedSpan || rootSpan ;
108119
109- const span = startStandaloneWebVitalSpan ( {
110- name,
111- transaction : routeName ,
112- attributes,
113- startTime,
114- } ) ;
120+ // Else, we try to use the active span.
121+ // Finally, we fall back to look at the transactionName on the scope
122+ const routeName = spanToUse ? spanToJSON ( spanToUse ) . description : getCurrentScope ( ) . getScopeData ( ) . transactionName ;
115123
116- if ( span ) {
117- span . addEvent ( 'inp' , {
118- [ SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT ] : 'millisecond' ,
119- [ SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE ] : metric . value ,
120- } ) ;
124+ const name = htmlTreeAsString ( entry . target ) ;
125+ const attributes : SpanAttributes = {
126+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.browser.inp' ,
127+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `ui.interaction.${ interactionType } ` ,
128+ [ SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME ] : entry . duration ,
129+ } ;
121130
122- span . end ( startTime + duration ) ;
123- }
131+ const span = startStandaloneWebVitalSpan ( {
132+ name,
133+ transaction : routeName ,
134+ attributes,
135+ startTime,
124136 } ) ;
125- }
137+
138+ if ( span ) {
139+ span . addEvent ( 'inp' , {
140+ [ SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT ] : 'millisecond' ,
141+ [ SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE ] : metric . value ,
142+ } ) ;
143+
144+ span . end ( startTime + duration ) ;
145+ }
146+ } ;
126147
127148/**
128149 * Register a listener to cache route information for INP interactions.
0 commit comments