Skip to content

Commit 71c5ac0

Browse files
committed
feat(browser): Detect redirects when emitting navigation spans
1 parent 61940fc commit 71c5ac0

File tree

1 file changed

+70
-2
lines changed

1 file changed

+70
-2
lines changed

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
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';
2629
import {
@@ -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 {
634678
function 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

Comments
 (0)