1
1
/* eslint-disable max-lines */
2
2
import type { IdleTransaction } from '@sentry/core' ;
3
- import { getActiveSpan } from '@sentry/core' ;
3
+ import { getActiveSpan , getClient , getCurrentScope } from '@sentry/core' ;
4
4
import { getCurrentHub } from '@sentry/core' ;
5
5
import {
6
6
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
@@ -12,6 +12,7 @@ import {
12
12
} from '@sentry/core' ;
13
13
import type {
14
14
Client ,
15
+ Integration ,
15
16
IntegrationFn ,
16
17
StartSpanOptions ,
17
18
Transaction ,
@@ -29,15 +30,18 @@ import {
29
30
30
31
import { DEBUG_BUILD } from '../common/debug-build' ;
31
32
import { registerBackgroundTabDetection } from './backgroundtab' ;
33
+ import { addPerformanceInstrumentationHandler } from './instrument' ;
32
34
import {
33
35
addPerformanceEntries ,
36
+ startTrackingINP ,
34
37
startTrackingInteractions ,
35
38
startTrackingLongTasks ,
36
39
startTrackingWebVitals ,
37
40
} from './metrics' ;
38
41
import type { RequestInstrumentationOptions } from './request' ;
39
42
import { defaultRequestInstrumentationOptions , instrumentOutgoingRequests } from './request' ;
40
43
import { WINDOW } from './types' ;
44
+ import type { InteractionRouteNameMapping } from './web-vitals/types' ;
41
45
42
46
export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing' ;
43
47
@@ -103,6 +107,13 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
103
107
*/
104
108
enableLongTask : boolean ;
105
109
110
+ /**
111
+ * If true, Sentry will capture INP web vitals as standalone spans .
112
+ *
113
+ * Default: false
114
+ */
115
+ enableInp : boolean ;
116
+
106
117
/**
107
118
* _metricOptions allows the user to send options to change how metrics are collected.
108
119
*
@@ -142,6 +153,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
142
153
instrumentPageLoad : true ,
143
154
markBackgroundSpan : true ,
144
155
enableLongTask : true ,
156
+ enableInp : false ,
145
157
_experiments : { } ,
146
158
...defaultRequestInstrumentationOptions ,
147
159
} ;
@@ -181,16 +193,25 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
181
193
182
194
const _collectWebVitals = startTrackingWebVitals ( ) ;
183
195
196
+ /** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
197
+ const interactionIdtoRouteNameMapping : InteractionRouteNameMapping = { } ;
198
+ if ( options . enableInp ) {
199
+ startTrackingINP ( interactionIdtoRouteNameMapping ) ;
200
+ }
201
+
184
202
if ( options . enableLongTask ) {
185
203
startTrackingLongTasks ( ) ;
186
204
}
187
205
if ( options . _experiments . enableInteractions ) {
188
206
startTrackingInteractions ( ) ;
189
207
}
190
208
191
- const latestRoute : { name : string | undefined ; source : TransactionSource | undefined } = {
209
+ const latestRoute : {
210
+ name : string | undefined ;
211
+ context : TransactionContext | undefined ;
212
+ } = {
192
213
name : undefined ,
193
- source : undefined ,
214
+ context : undefined ,
194
215
} ;
195
216
196
217
/** Create routing idle transaction. */
@@ -238,7 +259,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
238
259
finalContext . metadata ;
239
260
240
261
latestRoute . name = finalContext . name ;
241
- latestRoute . source = getSource ( finalContext ) ;
262
+ latestRoute . context = finalContext ;
242
263
243
264
if ( finalContext . sampled === false ) {
244
265
DEBUG_BUILD && logger . log ( `[Tracing] Will not send ${ finalContext . op } transaction because of beforeNavigate.` ) ;
@@ -389,6 +410,10 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
389
410
registerInteractionListener ( options , latestRoute ) ;
390
411
}
391
412
413
+ if ( options . enableInp ) {
414
+ registerInpInteractionListener ( interactionIdtoRouteNameMapping , latestRoute ) ;
415
+ }
416
+
392
417
instrumentOutgoingRequests ( {
393
418
traceFetch,
394
419
traceXHR,
@@ -448,7 +473,10 @@ export function getMetaContent(metaName: string): string | undefined {
448
473
/** Start listener for interaction transactions */
449
474
function registerInteractionListener (
450
475
options : BrowserTracingOptions ,
451
- latestRoute : { name : string | undefined ; source : TransactionSource | undefined } ,
476
+ latestRoute : {
477
+ name : string | undefined ;
478
+ context : TransactionContext | undefined ;
479
+ } ,
452
480
) : void {
453
481
let inflightInteractionTransaction : IdleTransaction | undefined ;
454
482
const registerInteractionTransaction = ( ) : void => {
@@ -483,7 +511,7 @@ function registerInteractionListener(
483
511
op,
484
512
trimEnd : true ,
485
513
data : {
486
- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : latestRoute . source || 'url' ,
514
+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : latestRoute . context ? getSource ( latestRoute . context ) : undefined || 'url' ,
487
515
} ,
488
516
} ;
489
517
@@ -504,6 +532,70 @@ function registerInteractionListener(
504
532
} ) ;
505
533
}
506
534
535
+ function isPerformanceEventTiming ( entry : PerformanceEntry ) : entry is PerformanceEventTiming {
536
+ return 'duration' in entry ;
537
+ }
538
+
539
+ /** We store up to 10 interaction candidates max to cap memory usage. This is the same cap as getINP from web-vitals */
540
+ const MAX_INTERACTIONS = 10 ;
541
+
542
+ /** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
543
+ function registerInpInteractionListener (
544
+ interactionIdtoRouteNameMapping : InteractionRouteNameMapping ,
545
+ latestRoute : {
546
+ name : string | undefined ;
547
+ context : TransactionContext | undefined ;
548
+ } ,
549
+ ) : void {
550
+ addPerformanceInstrumentationHandler ( 'event' , ( { entries } ) => {
551
+ const client = getClient ( ) ;
552
+ // We need to get the replay, user, and activeTransaction from the current scope
553
+ // so that we can associate replay id, profile id, and a user display to the span
554
+ const replay =
555
+ client !== undefined && client . getIntegrationByName !== undefined
556
+ ? ( client . getIntegrationByName ( 'Replay' ) as Integration & { getReplayId : ( ) => string } )
557
+ : undefined ;
558
+ const replayId = replay !== undefined ? replay . getReplayId ( ) : undefined ;
559
+ // eslint-disable-next-line deprecation/deprecation
560
+ const activeTransaction = getActiveTransaction ( ) ;
561
+ const currentScope = getCurrentScope ( ) ;
562
+ const user = currentScope !== undefined ? currentScope . getUser ( ) : undefined ;
563
+ for ( const entry of entries ) {
564
+ if ( isPerformanceEventTiming ( entry ) ) {
565
+ const duration = entry . duration ;
566
+ const keys = Object . keys ( interactionIdtoRouteNameMapping ) ;
567
+ const minInteractionId =
568
+ keys . length > 0
569
+ ? keys . reduce ( ( a , b ) => {
570
+ return interactionIdtoRouteNameMapping [ a ] . duration < interactionIdtoRouteNameMapping [ b ] . duration
571
+ ? a
572
+ : b ;
573
+ } )
574
+ : undefined ;
575
+ if ( minInteractionId === undefined || duration > interactionIdtoRouteNameMapping [ minInteractionId ] . duration ) {
576
+ const interactionId = entry . interactionId ;
577
+ const routeName = latestRoute . name ;
578
+ const parentContext = latestRoute . context ;
579
+ if ( interactionId && routeName && parentContext ) {
580
+ if ( minInteractionId && Object . keys ( interactionIdtoRouteNameMapping ) . length >= MAX_INTERACTIONS ) {
581
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
582
+ delete interactionIdtoRouteNameMapping [ minInteractionId ] ;
583
+ }
584
+ interactionIdtoRouteNameMapping [ interactionId ] = {
585
+ routeName,
586
+ duration,
587
+ parentContext,
588
+ user,
589
+ activeTransaction,
590
+ replayId,
591
+ } ;
592
+ }
593
+ }
594
+ }
595
+ }
596
+ } ) ;
597
+ }
598
+
507
599
function getSource ( context : TransactionContext ) : TransactionSource | undefined {
508
600
const sourceFromAttributes = context . attributes && context . attributes [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] ;
509
601
// eslint-disable-next-line deprecation/deprecation
0 commit comments