1
1
/* eslint-disable max-lines */
2
2
import type { Hub , IdleTransaction } from '@sentry/core' ;
3
+ import { getClient , getCurrentScope } from '@sentry/core' ;
3
4
import {
4
5
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
5
6
TRACING_DEFAULTS ,
@@ -12,8 +13,10 @@ import { getDomElement, logger, propagationContextFromHeaders } from '@sentry/ut
12
13
13
14
import { DEBUG_BUILD } from '../common/debug-build' ;
14
15
import { registerBackgroundTabDetection } from './backgroundtab' ;
16
+ import { addPerformanceInstrumentationHandler } from './instrument' ;
15
17
import {
16
18
addPerformanceEntries ,
19
+ startTrackingINP ,
17
20
startTrackingInteractions ,
18
21
startTrackingLongTasks ,
19
22
startTrackingWebVitals ,
@@ -22,6 +25,7 @@ import type { RequestInstrumentationOptions } from './request';
22
25
import { defaultRequestInstrumentationOptions , instrumentOutgoingRequests } from './request' ;
23
26
import { instrumentRoutingWithDefaults } from './router' ;
24
27
import { WINDOW } from './types' ;
28
+ import type { InteractionRouteNameMapping } from './web-vitals/types' ;
25
29
26
30
export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing' ;
27
31
@@ -87,6 +91,13 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
87
91
*/
88
92
enableLongTask : boolean ;
89
93
94
+ /**
95
+ * If true, Sentry will capture INP web vitals as standalone spans .
96
+ *
97
+ * Default: false
98
+ */
99
+ enableInp : boolean ;
100
+
90
101
/**
91
102
* _metricOptions allows the user to send options to change how metrics are collected.
92
103
*
@@ -146,10 +157,14 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
146
157
startTransactionOnLocationChange : true ,
147
158
startTransactionOnPageLoad : true ,
148
159
enableLongTask : true ,
160
+ enableInp : false ,
149
161
_experiments : { } ,
150
162
...defaultRequestInstrumentationOptions ,
151
163
} ;
152
164
165
+ /** We store up to 10 interaction candidates max to cap memory usage. This is the same cap as getINP from web-vitals */
166
+ const MAX_INTERACTIONS = 10 ;
167
+
153
168
/**
154
169
* The Browser Tracing integration automatically instruments browser pageload/navigation
155
170
* actions as transactions, and captures requests, metrics and errors as spans.
@@ -175,12 +190,14 @@ export class BrowserTracing implements Integration {
175
190
176
191
private _getCurrentHub ?: ( ) => Hub ;
177
192
178
- private _latestRouteName ?: string ;
179
- private _latestRouteSource ?: TransactionSource ;
180
-
181
193
private _collectWebVitals : ( ) => void ;
182
194
183
195
private _hasSetTracePropagationTargets : boolean ;
196
+ private _interactionIdtoRouteNameMapping : InteractionRouteNameMapping ;
197
+ private _latestRoute : {
198
+ name : string | undefined ;
199
+ context : TransactionContext | undefined ;
200
+ } ;
184
201
185
202
public constructor ( _options ?: Partial < BrowserTracingOptions > ) {
186
203
this . name = BROWSER_TRACING_INTEGRATION_ID ;
@@ -217,12 +234,23 @@ export class BrowserTracing implements Integration {
217
234
}
218
235
219
236
this . _collectWebVitals = startTrackingWebVitals ( ) ;
237
+ /** Stores a mapping of interactionIds from PerformanceEventTimings to the origin interaction path */
238
+ this . _interactionIdtoRouteNameMapping = { } ;
239
+
240
+ if ( this . options . enableInp ) {
241
+ startTrackingINP ( this . _interactionIdtoRouteNameMapping ) ;
242
+ }
220
243
if ( this . options . enableLongTask ) {
221
244
startTrackingLongTasks ( ) ;
222
245
}
223
246
if ( this . options . _experiments . enableInteractions ) {
224
247
startTrackingInteractions ( ) ;
225
248
}
249
+
250
+ this . _latestRoute = {
251
+ name : undefined ,
252
+ context : undefined ,
253
+ } ;
226
254
}
227
255
228
256
/**
@@ -287,6 +315,10 @@ export class BrowserTracing implements Integration {
287
315
this . _registerInteractionListener ( ) ;
288
316
}
289
317
318
+ if ( this . options . enableInp ) {
319
+ this . _registerInpInteractionListener ( ) ;
320
+ }
321
+
290
322
instrumentOutgoingRequests ( {
291
323
traceFetch,
292
324
traceXHR,
@@ -349,8 +381,8 @@ export class BrowserTracing implements Integration {
349
381
: // eslint-disable-next-line deprecation/deprecation
350
382
finalContext . metadata ;
351
383
352
- this . _latestRouteName = finalContext . name ;
353
- this . _latestRouteSource = getSource ( finalContext ) ;
384
+ this . _latestRoute . name = finalContext . name ;
385
+ this . _latestRoute . context = finalContext ;
354
386
355
387
// eslint-disable-next-line deprecation/deprecation
356
388
if ( finalContext . sampled === false ) {
@@ -420,7 +452,7 @@ export class BrowserTracing implements Integration {
420
452
return undefined ;
421
453
}
422
454
423
- if ( ! this . _latestRouteName ) {
455
+ if ( ! this . _latestRoute . name ) {
424
456
DEBUG_BUILD && logger . warn ( `[Tracing] Did not create ${ op } transaction because _latestRouteName is missing.` ) ;
425
457
return undefined ;
426
458
}
@@ -429,11 +461,13 @@ export class BrowserTracing implements Integration {
429
461
const { location } = WINDOW ;
430
462
431
463
const context : TransactionContext = {
432
- name : this . _latestRouteName ,
464
+ name : this . _latestRoute . name ,
433
465
op,
434
466
trimEnd : true ,
435
467
data : {
436
- [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : this . _latestRouteSource || 'url' ,
468
+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : this . _latestRoute . context
469
+ ? getSource ( this . _latestRoute . context )
470
+ : undefined || 'url' ,
437
471
} ,
438
472
} ;
439
473
@@ -452,6 +486,61 @@ export class BrowserTracing implements Integration {
452
486
addEventListener ( type , registerInteractionTransaction , { once : false , capture : true } ) ;
453
487
} ) ;
454
488
}
489
+
490
+ /** Creates a listener on interaction entries, and maps interactionIds to the origin path of the interaction */
491
+ private _registerInpInteractionListener ( ) : void {
492
+ addPerformanceInstrumentationHandler ( 'event' , ( { entries } ) => {
493
+ const client = getClient ( ) ;
494
+ // We need to get the replay, user, and activeTransaction from the current scope
495
+ // so that we can associate replay id, profile id, and a user display to the span
496
+ const replay =
497
+ client !== undefined && client . getIntegrationByName !== undefined
498
+ ? ( client . getIntegrationByName ( 'Replay' ) as Integration & { getReplayId : ( ) => string } )
499
+ : undefined ;
500
+ const replayId = replay !== undefined ? replay . getReplayId ( ) : undefined ;
501
+ // eslint-disable-next-line deprecation/deprecation
502
+ const activeTransaction = getActiveTransaction ( ) ;
503
+ const currentScope = getCurrentScope ( ) ;
504
+ const user = currentScope !== undefined ? currentScope . getUser ( ) : undefined ;
505
+ for ( const entry of entries ) {
506
+ if ( isPerformanceEventTiming ( entry ) ) {
507
+ const duration = entry . duration ;
508
+ const keys = Object . keys ( this . _interactionIdtoRouteNameMapping ) ;
509
+ const minInteractionId =
510
+ keys . length > 0
511
+ ? keys . reduce ( ( a , b ) => {
512
+ return this . _interactionIdtoRouteNameMapping [ a ] . duration <
513
+ this . _interactionIdtoRouteNameMapping [ b ] . duration
514
+ ? a
515
+ : b ;
516
+ } )
517
+ : undefined ;
518
+ if (
519
+ minInteractionId === undefined ||
520
+ duration > this . _interactionIdtoRouteNameMapping [ minInteractionId ] . duration
521
+ ) {
522
+ const interactionId = entry . interactionId ;
523
+ const routeName = this . _latestRoute . name ;
524
+ const parentContext = this . _latestRoute . context ;
525
+ if ( interactionId && routeName && parentContext ) {
526
+ if ( minInteractionId && Object . keys ( this . _interactionIdtoRouteNameMapping ) . length >= MAX_INTERACTIONS ) {
527
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
528
+ delete this . _interactionIdtoRouteNameMapping [ minInteractionId ] ;
529
+ }
530
+ this . _interactionIdtoRouteNameMapping [ interactionId ] = {
531
+ routeName,
532
+ duration,
533
+ parentContext,
534
+ user,
535
+ activeTransaction,
536
+ replayId,
537
+ } ;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ } ) ;
543
+ }
455
544
}
456
545
457
546
/** Returns the value of a meta tag */
@@ -473,3 +562,7 @@ function getSource(context: TransactionContext): TransactionSource | undefined {
473
562
474
563
return sourceFromAttributes || sourceFromData || sourceFromMetadata ;
475
564
}
565
+
566
+ function isPerformanceEventTiming ( entry : PerformanceEntry ) : entry is PerformanceEventTiming {
567
+ return 'duration' in entry ;
568
+ }
0 commit comments