diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts index 58be1f6ff9a4..581c2881dbc4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts @@ -7,6 +7,7 @@ import { } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; sentryTest( 'creates a pageload and navigation root spans each with multiple navigation.redirect childspans', @@ -57,6 +58,8 @@ sentryTest( 'sentry.op': 'navigation.redirect', 'sentry.origin': 'auto.navigation.browser', 'sentry.source': 'url', + [URL_FULL]: expect.any(String), + [URL_PATH]: expect.any(String), }, description: expect.stringContaining('/sub-page-redirect-'), op: 'navigation.redirect', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts index 1fa0a081fa85..b835b0296c14 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts @@ -7,6 +7,7 @@ import { } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; sentryTest('creates a pageload root span with navigation.redirect childspan', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -53,6 +54,8 @@ sentryTest('creates a pageload root span with navigation.redirect childspan', as 'sentry.op': 'navigation.redirect', 'sentry.origin': 'auto.navigation.browser', 'sentry.source': 'url', + [URL_FULL]: 'http://sentry-test.io/sub-page', + [URL_PATH]: '/sub-page', }, description: '/sub-page', op: 'navigation.redirect', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts index 6f08a47cb624..221e5409dd70 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts @@ -8,7 +8,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { SENTRY_TRACE_LIFECYCLE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { @@ -92,7 +92,11 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow type: 'string', value: expect.any(String), }, - 'url.full': { + [URL_FULL]: { + type: 'string', + value: expect.any(String), + }, + [URL_PATH]: { type: 'string', value: expect.any(String), }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts index 16791d22a212..0fe8482e76ef 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts @@ -12,7 +12,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { SENTRY_TRACE_LIFECYCLE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; @@ -81,7 +81,11 @@ sentryTest( type: 'string', value: expect.any(String), }, - 'url.full': { + [URL_FULL]: { + type: 'string', + value: expect.any(String), + }, + [URL_PATH]: { type: 'string', value: expect.any(String), }, diff --git a/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts index 0407d9965389..d73fd0f1dbb7 100644 --- a/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts @@ -19,6 +19,8 @@ test('captures a pageload transaction', async ({ page }) => { 'sentry.origin': 'auto.pageload.browser', 'sentry.sample_rate': 1, 'sentry.source': 'url', + 'url.full': 'http://localhost:3030/', + 'url.path': '/', }), op: 'pageload', origin: 'auto.pageload.browser', @@ -83,6 +85,10 @@ test('captures a navigation transaction', async ({ page }) => { trace: { op: 'navigation', origin: 'auto.navigation.browser', + data: expect.objectContaining({ + 'url.full': 'http://localhost:3030/#navigation-target', + 'url.path': '/', + }), }, }, transaction: '/', diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts index e1e06e9bedae..48ed51bc548b 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts @@ -26,6 +26,8 @@ test('Captures a pageload transaction', async ({ page }) => { 'performance.activationStart': expect.any(Number), 'lcp.renderTime': expect.any(Number), 'lcp.loadTime': expect.any(Number), + 'url.full': 'http://localhost:3030/', + 'url.path': '/', }, op: 'pageload', span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/packages/browser/package.json b/packages/browser/package.json index e03913af2c54..18b3bad89dca 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@sentry/browser-utils": "10.64.0", + "@sentry/conventions": "^0.15.1", "@sentry/feedback": "10.64.0", "@sentry/replay": "10.64.0", "@sentry/replay-canvas": "10.64.0", diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 1b6d58724d50..02d3f1657a26 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -24,6 +24,7 @@ import { GLOBAL_OBJ, hasSpansEnabled, hasSpanStreamingEnabled, + isURLObjectRelative, parseStringToURLObject, propagationContextFromHeaders, registerSpanErrorInstrumentation, @@ -51,6 +52,7 @@ import { WEB_VITALS_INTEGRATION_NAME, webVitalsIntegration } from '../integratio import { registerBackgroundTabDetection } from './backgroundtab'; import { linkTraces } from './linkedTraces'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; @@ -416,7 +418,7 @@ export const browserTracingIntegration = ((options: Partial { @@ -779,8 +796,8 @@ export function startBrowserTracingNavigationSpan( options?: { url?: string; isRedirect?: boolean }, ): Span | undefined { const { url, isRedirect } = options || {}; - client.emit('beforeStartNavigationSpan', spanOptions, { isRedirect }); - client.emit('startNavigationSpan', spanOptions, { isRedirect }); + client.emit('beforeStartNavigationSpan', spanOptions, { isRedirect, url }); + client.emit('startNavigationSpan', spanOptions, { isRedirect, url }); const scope = getCurrentScope(); scope.setTransactionName(spanOptions.name); diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 659a381a1957..7cfe1f726913 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -33,6 +33,7 @@ import { } from '../../src/tracing/browserTracingIntegration'; import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/linkedTraces'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; const oldTextEncoder = global.window.TextEncoder; const oldTextDecoder = global.window.TextDecoder; @@ -65,11 +66,17 @@ describe('browserTracingIntegration', () => { getCurrentScope().clear(); getIsolationScope().clear(); getCurrentScope().setClient(undefined); - document.head.innerHTML = ''; + // Reset document and location to a fresh JSDOM for every test. `getLocationHref()` reads + // `WINDOW.document.location.href`, so leaving `document` bound to a shared instance leaks URL state + // (e.g. `/test`) from `pushState` in earlier tests into later ones. `history` must stay bound to the + // shared instance because the history instrumentation patches it once globally on `client.init()`. const dom = new JSDOM(undefined, { url: 'https://example.com/' }); + Object.defineProperty(global, 'document', { value: dom.window.document, writable: true }); Object.defineProperty(global, 'location', { value: dom.window.document.location, writable: true }); + document.head.innerHTML = ''; + // We want to suppress the "Multiple browserTracingIntegration instances are not supported." warnings vi.spyOn(console, 'warn').mockImplementation(() => {}); }); @@ -174,6 +181,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -258,6 +267,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -286,6 +297,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: 'https://example.com/test', + [URL_PATH]: '/test', [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: `${span?.spanContext().traceId}-${span?.spanContext().spanId}-1`, }, links: [ @@ -325,6 +338,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: 'https://example.com/test2', + [URL_PATH]: '/test2', [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: `${span2?.spanContext().traceId}-${span2?.spanContext().spanId}-1`, }, links: [ @@ -366,6 +381,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -394,6 +411,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation.redirect', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: 'https://example.com/test', + [URL_PATH]: '/test', }, description: '/test', op: 'navigation.redirect', @@ -456,6 +475,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), @@ -492,6 +513,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', testy: 'yes', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -733,6 +756,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', }, links: [ { @@ -785,6 +810,8 @@ describe('browserTracingIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [URL_FULL]: 'https://example.com/', + [URL_PATH]: '/', testy: 'yes', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0c9586728f49..f22ccb731f03 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -815,7 +815,7 @@ export abstract class Client { */ public on( hook: 'beforeStartNavigationSpan', - callback: (options: StartSpanOptions, navigationOptions?: { isRedirect?: boolean }) => void, + callback: (options: StartSpanOptions, navigationOptions?: { isRedirect?: boolean; url?: string }) => void, ): () => void; /** @@ -824,7 +824,7 @@ export abstract class Client { */ public on( hook: 'startNavigationSpan', - callback: (options: StartSpanOptions, navigationOptions?: { isRedirect?: boolean }) => void, + callback: (options: StartSpanOptions, navigationOptions?: { isRedirect?: boolean; url?: string }) => void, ): () => void; /** @@ -1100,7 +1100,7 @@ export abstract class Client { public emit( hook: 'beforeStartNavigationSpan', options: StartSpanOptions, - navigationOptions?: { isRedirect?: boolean }, + navigationOptions?: { isRedirect?: boolean; url?: string }, ): void; /** @@ -1109,7 +1109,7 @@ export abstract class Client { public emit( hook: 'startNavigationSpan', options: StartSpanOptions, - navigationOptions?: { isRedirect?: boolean }, + navigationOptions?: { isRedirect?: boolean; url?: string }, ): void; /**