4
4
addNonEnumerableProperty ,
5
5
browserPerformanceTimeOrigin ,
6
6
consoleSandbox ,
7
+ dateTimestampInSeconds ,
7
8
generateTraceId ,
8
9
getClient ,
9
10
getCurrentScope ,
@@ -21,6 +22,8 @@ import {
21
22
spanIsSampled ,
22
23
spanToJSON ,
23
24
startIdleSpan ,
25
+ startInactiveSpan ,
26
+ timestampInSeconds ,
24
27
TRACING_DEFAULTS ,
25
28
} from '@sentry/core' ;
26
29
import {
@@ -145,6 +148,14 @@ export interface BrowserTracingOptions {
145
148
*/
146
149
enableHTTPTimings : boolean ;
147
150
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
+
148
159
/**
149
160
* Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or
150
161
* manually started span). When enabled, this option will allow you to navigate between traces
@@ -227,6 +238,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
227
238
enableLongTask : true ,
228
239
enableLongAnimationFrame : true ,
229
240
enableInp : true ,
241
+ detectRedirects : true ,
230
242
linkPreviousTrace : 'in-memory' ,
231
243
consistentTraceSampling : false ,
232
244
_experiments : { } ,
@@ -279,6 +291,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
279
291
enableHTTPTimings,
280
292
instrumentPageLoad,
281
293
instrumentNavigation,
294
+ detectRedirects,
282
295
linkPreviousTrace,
283
296
consistentTraceSampling,
284
297
onRequestSpanStart,
@@ -313,8 +326,14 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
313
326
source : undefined ,
314
327
} ;
315
328
329
+ let lastClickTimestamp : number | undefined ;
330
+
331
+ if ( detectRedirects ) {
332
+ addEventListener ( 'click' , ( ) => ( lastClickTimestamp = timestampInSeconds ( ) ) , { capture : true , passive : true } ) ;
333
+ }
334
+
316
335
/** Create routing idle transaction. */
317
- function _createRouteSpan ( client : Client , startSpanOptions : StartSpanOptions ) : void {
336
+ function _createRouteSpan ( client : Client , startSpanOptions : StartSpanOptions , makeActive = true ) : void {
318
337
const isPageloadTransaction = startSpanOptions . op === 'pageload' ;
319
338
320
339
const finalStartSpanOptions : StartSpanOptions = beforeStartSpan
@@ -330,6 +349,16 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
330
349
finalStartSpanOptions . attributes = attributes ;
331
350
}
332
351
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
+
333
362
latestRoute . name = finalStartSpanOptions . name ;
334
363
latestRoute . source = attributes [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] ;
335
364
@@ -342,6 +371,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
342
371
beforeSpanEnd : span => {
343
372
_collectWebVitals ( ) ;
344
373
addPerformanceEntries ( span , { recordClsOnPageloadSpan : ! enableStandaloneClsSpans } ) ;
374
+
345
375
setActiveIdleSpan ( client , undefined ) ;
346
376
347
377
// 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
397
427
return ;
398
428
}
399
429
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
+
400
444
maybeEndActiveSpan ( ) ;
401
445
402
446
getIsolationScope ( ) . setPropagationContext ( { traceId : generateTraceId ( ) , sampleRand : Math . random ( ) } ) ;
@@ -621,7 +665,7 @@ function registerInteractionListener(
621
665
} ;
622
666
623
667
if ( optionalWindowDocument ) {
624
- addEventListener ( 'click' , registerInteractionTransaction , { once : false , capture : true } ) ;
668
+ addEventListener ( 'click' , registerInteractionTransaction , { capture : true } ) ;
625
669
}
626
670
}
627
671
@@ -634,3 +678,27 @@ function getActiveIdleSpan(client: Client): Span | undefined {
634
678
function setActiveIdleSpan ( client : Client , span : Span | undefined ) : void {
635
679
addNonEnumerableProperty ( client , ACTIVE_IDLE_SPAN_PROPERTY , span ) ;
636
680
}
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