44 addNonEnumerableProperty ,
55 browserPerformanceTimeOrigin ,
66 consoleSandbox ,
7+ dateTimestampInSeconds ,
78 generateTraceId ,
89 getClient ,
910 getCurrentScope ,
@@ -21,6 +22,8 @@ import {
2122 spanIsSampled ,
2223 spanToJSON ,
2324 startIdleSpan ,
25+ startInactiveSpan ,
26+ timestampInSeconds ,
2427 TRACING_DEFAULTS ,
2528} from '@sentry/core' ;
2629import {
@@ -145,6 +148,14 @@ export interface BrowserTracingOptions {
145148 */
146149 enableHTTPTimings : boolean ;
147150
151+ /**
152+ * By default, the SDK will try to detect redirects and avoid creating separate spans for them.
153+ * If you want to opt-out of this behavior, you can set this option to `false`.
154+ *
155+ * Default: true
156+ */
157+ detectRedirects : boolean ;
158+
148159 /**
149160 * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or
150161 * manually started span). When enabled, this option will allow you to navigate between traces
@@ -227,6 +238,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
227238 enableLongTask : true ,
228239 enableLongAnimationFrame : true ,
229240 enableInp : true ,
241+ detectRedirects : true ,
230242 linkPreviousTrace : 'in-memory' ,
231243 consistentTraceSampling : false ,
232244 _experiments : { } ,
@@ -279,6 +291,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
279291 enableHTTPTimings,
280292 instrumentPageLoad,
281293 instrumentNavigation,
294+ detectRedirects,
282295 linkPreviousTrace,
283296 consistentTraceSampling,
284297 onRequestSpanStart,
@@ -313,8 +326,14 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
313326 source : undefined ,
314327 } ;
315328
329+ let lastClickTimestamp : number | undefined ;
330+
331+ if ( detectRedirects ) {
332+ addEventListener ( 'click' , ( ) => ( lastClickTimestamp = timestampInSeconds ( ) ) , { capture : true , passive : true } ) ;
333+ }
334+
316335 /** Create routing idle transaction. */
317- function _createRouteSpan ( client : Client , startSpanOptions : StartSpanOptions ) : void {
336+ function _createRouteSpan ( client : Client , startSpanOptions : StartSpanOptions , makeActive = true ) : void {
318337 const isPageloadTransaction = startSpanOptions . op === 'pageload' ;
319338
320339 const finalStartSpanOptions : StartSpanOptions = beforeStartSpan
@@ -330,6 +349,16 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
330349 finalStartSpanOptions . attributes = attributes ;
331350 }
332351
352+ if ( ! makeActive ) {
353+ // We want to ensure this has 0s duration
354+ const now = dateTimestampInSeconds ( ) ;
355+ startInactiveSpan ( {
356+ ...finalStartSpanOptions ,
357+ startTime : now ,
358+ } ) . end ( now ) ;
359+ return ;
360+ }
361+
333362 latestRoute . name = finalStartSpanOptions . name ;
334363 latestRoute . source = attributes [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] ;
335364
@@ -342,6 +371,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
342371 beforeSpanEnd : span => {
343372 _collectWebVitals ( ) ;
344373 addPerformanceEntries ( span , { recordClsOnPageloadSpan : ! enableStandaloneClsSpans } ) ;
374+
345375 setActiveIdleSpan ( client , undefined ) ;
346376
347377 // A trace should stay consistent over the entire timespan of one route - even after the pageload/navigation ended.
@@ -397,6 +427,20 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
397427 return ;
398428 }
399429
430+ const activeSpan = getActiveIdleSpan ( client ) ;
431+ if ( detectRedirects && activeSpan && isRedirect ( activeSpan , lastClickTimestamp ) ) {
432+ DEBUG_BUILD && logger . warn ( '[Tracing] Detected redirect, navigation span will not be the root span.' ) ;
433+ _createRouteSpan (
434+ client ,
435+ {
436+ op : 'navigation.redirect' ,
437+ ...startSpanOptions ,
438+ } ,
439+ false ,
440+ ) ;
441+ return ;
442+ }
443+
400444 maybeEndActiveSpan ( ) ;
401445
402446 getIsolationScope ( ) . setPropagationContext ( { traceId : generateTraceId ( ) , sampleRand : Math . random ( ) } ) ;
@@ -621,7 +665,7 @@ function registerInteractionListener(
621665 } ;
622666
623667 if ( optionalWindowDocument ) {
624- addEventListener ( 'click' , registerInteractionTransaction , { once : false , capture : true } ) ;
668+ addEventListener ( 'click' , registerInteractionTransaction , { capture : true } ) ;
625669 }
626670}
627671
@@ -634,3 +678,27 @@ function getActiveIdleSpan(client: Client): Span | undefined {
634678function setActiveIdleSpan ( client : Client , span : Span | undefined ) : void {
635679 addNonEnumerableProperty ( client , ACTIVE_IDLE_SPAN_PROPERTY , span ) ;
636680}
681+
682+ // The max. time in ms between two pageload/navigation spans that makes us consider the second one a redirect
683+ const REDIRECT_THRESHOLD = 300 ;
684+
685+ function isRedirect ( activeSpan : Span , lastClickTimestamp : number | undefined ) : boolean {
686+ const spanData = spanToJSON ( activeSpan ) ;
687+
688+ const now = dateTimestampInSeconds ( ) ;
689+
690+ // More than 500ms since last navigation/pageload span?
691+ // --> never consider this a redirect
692+ const startTimestamp = spanData . start_timestamp ;
693+ if ( now - startTimestamp > REDIRECT_THRESHOLD ) {
694+ return false ;
695+ }
696+
697+ // More than 500ms since last click?
698+ // --> never consider this a redirect
699+ if ( lastClickTimestamp && now - lastClickTimestamp > REDIRECT_THRESHOLD ) {
700+ return false ;
701+ }
702+
703+ return true ;
704+ }
0 commit comments