11/* eslint-disable max-lines */
22import type { Hub , IdleTransaction } from '@sentry/core' ;
3+ import { getClient , getCurrentScope } from '@sentry/core' ;
34import {
45 SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
56 TRACING_DEFAULTS ,
@@ -12,8 +13,10 @@ import { getDomElement, logger, propagationContextFromHeaders } from '@sentry/ut
1213
1314import { DEBUG_BUILD } from '../common/debug-build' ;
1415import { registerBackgroundTabDetection } from './backgroundtab' ;
16+ import { addPerformanceInstrumentationHandler } from './instrument' ;
1517import {
1618 addPerformanceEntries ,
19+ startTrackingINP ,
1720 startTrackingInteractions ,
1821 startTrackingLongTasks ,
1922 startTrackingWebVitals ,
@@ -22,6 +25,7 @@ import type { RequestInstrumentationOptions } from './request';
2225import { defaultRequestInstrumentationOptions , instrumentOutgoingRequests } from './request' ;
2326import { instrumentRoutingWithDefaults } from './router' ;
2427import { WINDOW } from './types' ;
28+ import type { InteractionRouteNameMapping } from './web-vitals/types' ;
2529
2630export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing' ;
2731
@@ -87,6 +91,13 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
8791 */
8892 enableLongTask : boolean ;
8993
94+ /**
95+ * If true, Sentry will capture INP web vitals as standalone spans .
96+ *
97+ * Default: false
98+ */
99+ enableInp : boolean ;
100+
90101 /**
91102 * _metricOptions allows the user to send options to change how metrics are collected.
92103 *
@@ -146,10 +157,14 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
146157 startTransactionOnLocationChange : true ,
147158 startTransactionOnPageLoad : true ,
148159 enableLongTask : true ,
160+ enableInp : false ,
149161 _experiments : { } ,
150162 ...defaultRequestInstrumentationOptions ,
151163} ;
152164
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+
153168/**
154169 * The Browser Tracing integration automatically instruments browser pageload/navigation
155170 * actions as transactions, and captures requests, metrics and errors as spans.
@@ -175,12 +190,14 @@ export class BrowserTracing implements Integration {
175190
176191 private _getCurrentHub ?: ( ) => Hub ;
177192
178- private _latestRouteName ?: string ;
179- private _latestRouteSource ?: TransactionSource ;
180-
181193 private _collectWebVitals : ( ) => void ;
182194
183195 private _hasSetTracePropagationTargets : boolean ;
196+ private _interactionIdtoRouteNameMapping : InteractionRouteNameMapping ;
197+ private _latestRoute : {
198+ name : string | undefined ;
199+ context : TransactionContext | undefined ;
200+ } ;
184201
185202 public constructor ( _options ?: Partial < BrowserTracingOptions > ) {
186203 this . name = BROWSER_TRACING_INTEGRATION_ID ;
@@ -217,12 +234,23 @@ export class BrowserTracing implements Integration {
217234 }
218235
219236 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+ }
220243 if ( this . options . enableLongTask ) {
221244 startTrackingLongTasks ( ) ;
222245 }
223246 if ( this . options . _experiments . enableInteractions ) {
224247 startTrackingInteractions ( ) ;
225248 }
249+
250+ this . _latestRoute = {
251+ name : undefined ,
252+ context : undefined ,
253+ } ;
226254 }
227255
228256 /**
@@ -287,6 +315,10 @@ export class BrowserTracing implements Integration {
287315 this . _registerInteractionListener ( ) ;
288316 }
289317
318+ if ( this . options . enableInp ) {
319+ this . _registerInpInteractionListener ( ) ;
320+ }
321+
290322 instrumentOutgoingRequests ( {
291323 traceFetch,
292324 traceXHR,
@@ -349,8 +381,8 @@ export class BrowserTracing implements Integration {
349381 : // eslint-disable-next-line deprecation/deprecation
350382 finalContext . metadata ;
351383
352- this . _latestRouteName = finalContext . name ;
353- this . _latestRouteSource = getSource ( finalContext ) ;
384+ this . _latestRoute . name = finalContext . name ;
385+ this . _latestRoute . context = finalContext ;
354386
355387 // eslint-disable-next-line deprecation/deprecation
356388 if ( finalContext . sampled === false ) {
@@ -420,7 +452,7 @@ export class BrowserTracing implements Integration {
420452 return undefined ;
421453 }
422454
423- if ( ! this . _latestRouteName ) {
455+ if ( ! this . _latestRoute . name ) {
424456 DEBUG_BUILD && logger . warn ( `[Tracing] Did not create ${ op } transaction because _latestRouteName is missing.` ) ;
425457 return undefined ;
426458 }
@@ -429,11 +461,13 @@ export class BrowserTracing implements Integration {
429461 const { location } = WINDOW ;
430462
431463 const context : TransactionContext = {
432- name : this . _latestRouteName ,
464+ name : this . _latestRoute . name ,
433465 op,
434466 trimEnd : true ,
435467 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' ,
437471 } ,
438472 } ;
439473
@@ -452,6 +486,61 @@ export class BrowserTracing implements Integration {
452486 addEventListener ( type , registerInteractionTransaction , { once : false , capture : true } ) ;
453487 } ) ;
454488 }
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+ }
455544}
456545
457546/** Returns the value of a meta tag */
@@ -473,3 +562,7 @@ function getSource(context: TransactionContext): TransactionSource | undefined {
473562
474563 return sourceFromAttributes || sourceFromData || sourceFromMetadata ;
475564}
565+
566+ function isPerformanceEventTiming ( entry : PerformanceEntry ) : entry is PerformanceEventTiming {
567+ return 'duration' in entry ;
568+ }
0 commit comments