@@ -12,6 +12,7 @@ import {
12
12
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
13
13
spanToJSON ,
14
14
} from '@sentry/core' ;
15
+ import type { InstrumentationHandlerCallback } from './instrument' ;
15
16
import {
16
17
addInpInstrumentationHandler ,
17
18
addPerformanceInstrumentationHandler ,
@@ -22,6 +23,11 @@ import { getBrowserPerformanceAPI, msToSec, startStandaloneWebVitalSpan } from '
22
23
const LAST_INTERACTIONS : number [ ] = [ ] ;
23
24
const INTERACTIONS_SPAN_MAP = new Map < number , Span > ( ) ;
24
25
26
+ /**
27
+ * 60 seconds is the maximum for a plausible INP value
28
+ * (source: Me)
29
+ */
30
+ const MAX_PLAUSIBLE_INP_DURATION = 60 ;
25
31
/**
26
32
* Start tracking INP webvital events.
27
33
*/
@@ -67,62 +73,77 @@ const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
67
73
input : 'press' ,
68
74
} ;
69
75
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
+ }
76
90
77
- const entry = metric . entries . find ( entry => entry . duration === metric . value && INP_ENTRY_MAP [ entry . name ] ) ;
91
+ const duration = msToSec ( metric . value ) ;
78
92
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
+ }
82
99
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 ] ) ;
85
101
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
+ }
91
105
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 ] ;
95
108
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 ;
97
113
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 ;
101
117
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 ;
108
119
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 ;
115
123
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
+ } ;
121
130
122
- span . end ( startTime + duration ) ;
123
- }
131
+ const span = startStandaloneWebVitalSpan ( {
132
+ name,
133
+ transaction : routeName ,
134
+ attributes,
135
+ startTime,
124
136
} ) ;
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
+ } ;
126
147
127
148
/**
128
149
* Register a listener to cache route information for INP interactions.
0 commit comments