diff --git a/.size-limit.js b/.size-limit.js index 10efb849a582..324c5da003bc 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -120,7 +120,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '40.5 KB', + limit: '41 KB', }, // Vue SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js index f26a4197747c..a3b99e2e1dc3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js @@ -4,7 +4,8 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration()], + // We want to ignore redirects for this test + integrations: [Sentry.browserTracingIntegration({ detectRedirects: false })], tracesSampler: ctx => { if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') { return 0; diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js index 8fb188a75278..ad357eee8cc6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js @@ -4,9 +4,12 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration()], + integrations: [Sentry.browserTracingIntegration({ idleTimeout: 2000 })], tracesSampleRate: 1, }); -// Immediately navigate to a new page to abort the pageload -window.history.pushState({}, '', '/sub-page'); +// Navigate to a new page to abort the pageload +// We have to wait >300ms to avoid the redirect handling +setTimeout(() => { + window.history.pushState({}, '', '/sub-page'); +}, 500); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/init.js new file mode 100644 index 000000000000..83abe7de1b7a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/init.js @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, + debug: true, +}); + +document.getElementById('btn1').addEventListener('click', () => { + // Trigger navigation later than click, so the last click is more than 300ms ago + setTimeout(() => { + window.history.pushState({}, '', '/sub-page'); + + // then trigger redirect inside of this navigation, which should be detected as a redirect + // because the last click was more than 300ms ago + setTimeout(() => { + window.history.pushState({}, '', '/sub-page-redirect'); + }, 100); + }, 400); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/template.html new file mode 100644 index 000000000000..d364e6680b41 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/template.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/test.ts new file mode 100644 index 000000000000..97cbc67c8af8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click-early/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'should create a navigation.redirect span if a click happened more than 300ms before navigation', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const navigationRequestPromise = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === 'navigation', + ); + + await page.goto(url); + + await pageloadRequestPromise; + + // Now trigger navigation, and then a redirect in the navigation, with + await page.click('#btn1'); + + const navigationRequest = envelopeRequestParser(await navigationRequestPromise); + + expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + expect(navigationRequest.transaction).toEqual('/sub-page'); + + const spans = navigationRequest.spans || []; + + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'navigation.redirect', + description: '/sub-page-redirect', + }), + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/init.js new file mode 100644 index 000000000000..0656ee398dcf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, + debug: true, +}); + +document.getElementById('btn1').addEventListener('click', () => { + // trigger redirect immediately + window.history.pushState({}, '', '/sub-page'); +}); + +// Now trigger click, whic should trigger navigation +document.getElementById('btn1').click(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/template.html new file mode 100644 index 000000000000..d364e6680b41 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/template.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/test.ts new file mode 100644 index 000000000000..4a5cb9acd73b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/click/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'should not create a navigation.redirect span if a click happened before navigation', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const navigationRequestPromise = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === 'navigation', + ); + + await page.goto(url); + + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + // Ensure a navigation span is sent, too + await navigationRequestPromise; + + const spans = pageloadRequest.spans || []; + + expect(spans).not.toContainEqual( + expect.objectContaining({ + op: 'navigation.redirect', + }), + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/immediately/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/immediately/init.js new file mode 100644 index 000000000000..cba0015b22c8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/immediately/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); + +// trigger redirect immediately +window.history.pushState({}, '', '/sub-page'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/immediately/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/immediately/test.ts new file mode 100644 index 000000000000..f2b3e885f6ce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/immediately/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest('should create a pageload transaction with navigation.redirect span', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + + await page.goto(url); + + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + + expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); + + expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + ['sentry.idle_span_finish_reason']: 'idleTimeout', + }); + + expect(pageloadRequest.request).toEqual({ + headers: { + 'User-Agent': expect.any(String), + }, + url: 'http://sentry-test.io/index.html', + }); + + const spans = pageloadRequest.spans || []; + + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'navigation.redirect', + }), + ); + + const navigationSpan = spans.find(span => span.op === 'navigation.redirect'); + expect(navigationSpan?.timestamp).toEqual(navigationSpan?.start_timestamp); + expect(navigationSpan).toEqual({ + data: { + 'sentry.op': 'navigation.redirect', + 'sentry.origin': 'auto.navigation.browser', + 'sentry.source': 'url', + }, + description: '/sub-page', + op: 'navigation.redirect', + origin: 'auto.navigation.browser', + parent_span_id: pageloadRequest.contexts!.trace!.span_id, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/late/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/late/init.js new file mode 100644 index 000000000000..686f72903a89 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/late/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); + +// trigger redirect later +setTimeout(() => { + window.history.pushState({}, '', '/sub-page'); +}, 400); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/late/test.ts new file mode 100644 index 000000000000..f1108cdbc1c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/late/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'should not create a navigation.redirect span if redirect happened more than 300ms after pageload', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const navigationRequestPromise = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === 'navigation', + ); + + await page.goto(url); + + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + // Ensure a navigation span is sent, too + await navigationRequestPromise; + + const spans = pageloadRequest.spans || []; + + expect(spans).not.toContainEqual( + expect.objectContaining({ + op: 'navigation.redirect', + }), + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js new file mode 100644 index 000000000000..331024032a6f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + detectRedirects: false, + }), + ], + tracesSampleRate: 1, +}); + +// trigger redirect immediately +window.history.pushState({}, '', '/sub-page'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts new file mode 100644 index 000000000000..e96e9e650122 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/opt-out/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'should not create a navigation.redirect span if `detectRedirects` is set to false', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + const navigationRequestPromise = waitForTransactionRequest( + page, + event => event.contexts?.trace?.op === 'navigation', + ); + + await page.goto(url); + + const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise); + // Ensure a navigation span is sent, too + await navigationRequestPromise; + + const spans = pageloadRequest.spans || []; + + expect(spans).not.toContainEqual( + expect.objectContaining({ + op: 'navigation.redirect', + }), + ); + }, +); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index d31fe41742f8..06b0c70eb18f 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -3,6 +3,7 @@ import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, import { addNonEnumerableProperty, browserPerformanceTimeOrigin, + dateTimestampInSeconds, generateTraceId, getClient, getCurrentScope, @@ -20,6 +21,8 @@ import { spanIsSampled, spanToJSON, startIdleSpan, + startInactiveSpan, + timestampInSeconds, TRACING_DEFAULTS, } from '@sentry/core'; import { @@ -144,6 +147,14 @@ export interface BrowserTracingOptions { */ enableHTTPTimings: boolean; + /** + * By default, the SDK will try to detect redirects and avoid creating separate spans for them. + * If you want to opt-out of this behavior, you can set this option to `false`. + * + * Default: true + */ + detectRedirects: boolean; + /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or * manually started span). When enabled, this option will allow you to navigate between traces @@ -226,6 +237,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, + detectRedirects: true, linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, @@ -270,6 +282,7 @@ export const browserTracingIntegration = ((_options: Partial void); + let lastClickTimestamp: number | undefined; /** Create routing idle transaction. */ - function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions): void { + function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions, makeActive = true): void { const isPageloadTransaction = startSpanOptions.op === 'pageload'; const finalStartSpanOptions: StartSpanOptions = beforeStartSpan @@ -297,6 +311,16 @@ export const browserTracingIntegration = ((_options: Partial (lastClickTimestamp = timestampInSeconds()), { capture: true, passive: true }); + } + function maybeEndActiveSpan(): void { const activeSpan = getActiveIdleSpan(client); @@ -382,11 +411,24 @@ export const browserTracingIntegration = ((_options: Partial { + client.on('startNavigationSpan', (startSpanOptions, navigationOptions) => { if (getClient() !== client) { return; } + if (navigationOptions?.isRedirect) { + DEBUG_BUILD && logger.warn('[Tracing] Detected redirect, navigation span will not be the root span.'); + _createRouteSpan( + client, + { + op: 'navigation.redirect', + ...startSpanOptions, + }, + false, + ); + return; + } + maybeEndActiveSpan(); getIsolationScope().setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random() }); @@ -470,23 +512,19 @@ export const browserTracingIntegration = ((_options: Partial never consider this a redirect + const startTimestamp = spanData.start_timestamp; + if (now - startTimestamp > REDIRECT_THRESHOLD) { + return false; + } + + // A click happened in the last 300ms? + // --> never consider this a redirect + if (lastClickTimestamp && now - lastClickTimestamp <= REDIRECT_THRESHOLD) { + return false; + } + + return true; +} diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 0b71fcc01383..0545c6618db8 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -53,36 +53,35 @@ Object.defineProperty(global, 'history', { value: dom.window.history, writable: const originalGlobalDocument = WINDOW.document; const originalGlobalLocation = WINDOW.location; const originalGlobalHistory = WINDOW.history; -afterAll(() => { - // Clean up JSDom - Object.defineProperty(WINDOW, 'document', { value: originalGlobalDocument }); - Object.defineProperty(WINDOW, 'location', { value: originalGlobalLocation }); - Object.defineProperty(WINDOW, 'history', { value: originalGlobalHistory }); -}); - -afterEach(() => { - vi.useRealTimers(); - performance.clearMarks(); -}); describe('browserTracingIntegration', () => { beforeEach(() => { + vi.useFakeTimers(); getCurrentScope().clear(); getIsolationScope().clear(); getCurrentScope().setClient(undefined); document.head.innerHTML = ''; + const dom = new JSDOM(undefined, { url: 'https://example.com/' }); + Object.defineProperty(global, 'location', { value: dom.window.document.location, writable: true }); + // We want to suppress the "Multiple browserTracingIntegration instances are not supported." warnings vi.spyOn(console, 'warn').mockImplementation(() => {}); }); afterEach(() => { getActiveSpan()?.end(); + vi.useRealTimers(); + performance.clearMarks(); }); afterAll(() => { global.window.TextEncoder = oldTextEncoder; global.window.TextDecoder = oldTextDecoder; + // Clean up JSDom + Object.defineProperty(WINDOW, 'document', { value: originalGlobalDocument }); + Object.defineProperty(WINDOW, 'location', { value: originalGlobalLocation }); + Object.defineProperty(WINDOW, 'history', { value: originalGlobalHistory }); }); it('works with tracing enabled', () => { @@ -155,7 +154,7 @@ describe('browserTracingIntegration', () => { expect(spanIsSampled(span!)).toBe(false); }); - it('starts navigation when URL changes', () => { + it('starts navigation when URL changes after > 300ms', () => { const client = new BrowserClient( getDefaultBrowserClientOptions({ tracesSampleRate: 1, @@ -188,6 +187,7 @@ describe('browserTracingIntegration', () => { const dom = new JSDOM(undefined, { url: 'https://example.com/test' }); Object.defineProperty(global, 'location', { value: dom.window.document.location, writable: true }); + vi.advanceTimersByTime(400); WINDOW.history.pushState({}, '', '/test'); expect(span!.isRecording()).toBe(false); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0dda6c86fd26..318823b1020b 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -607,7 +607,10 @@ export abstract class Client { * A hook for browser tracing integrations to trigger a span for a navigation. * @returns {() => void} A function that, when executed, removes the registered callback. */ - public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void; + public on( + hook: 'startNavigationSpan', + callback: (options: StartSpanOptions, navigationOptions?: { isRedirect?: boolean }) => void, + ): () => void; /** * A hook for GraphQL client integration to enhance a span with request data. @@ -782,7 +785,11 @@ export abstract class Client { /** * Emit a hook event for browser tracing integrations to trigger a span for a navigation. */ - public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; + public emit( + hook: 'startNavigationSpan', + options: StartSpanOptions, + navigationOptions?: { isRedirect?: boolean }, + ): void; /** * Emit a hook event for GraphQL client integration to enhance a span with request data. diff --git a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts index d189dc0a20c3..ba4fdd0d9313 100644 --- a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts @@ -332,6 +332,7 @@ describe('pagesRouterInstrumentNavigation', () => { 'sentry.source': expectedTransactionSource, }, }), + { isRedirect: undefined }, ); }, );