diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts index 5a46a65a4392..f7dc01ca7f54 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -49,3 +49,25 @@ sentryTest('should create a navigation transaction on page navigation', async ({ expect(pageloadSpanId).not.toEqual(navigationSpanId); }); + +sentryTest('should create a new trace for for multiple navigations', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await getFirstSentryEnvelopeRequest(page, url); + const navigationEvent1 = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + const navigationEvent2 = await getFirstSentryEnvelopeRequest(page, `${url}#bar`); + + expect(navigationEvent1.contexts?.trace?.op).toBe('navigation'); + expect(navigationEvent2.contexts?.trace?.op).toBe('navigation'); + + const navigation1TraceId = navigationEvent1.contexts?.trace?.trace_id; + const navigation2TraceId = navigationEvent2.contexts?.trace?.trace_id; + + expect(navigation1TraceId).toBeDefined(); + expect(navigation2TraceId).toBeDefined(); + expect(navigation1TraceId).not.toEqual(navigation2TraceId); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte index d8175182884a..71a0ce872185 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte @@ -29,4 +29,10 @@
  • Route with nested fetch in server load
  • +
  • + Nav 1 +
  • +
  • + Nav 2 +
  • diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav1/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav1/+page.svelte new file mode 100644 index 000000000000..31abffc512a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav1/+page.svelte @@ -0,0 +1 @@ +

    Navigation 1

    diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav2/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav2/+page.svelte new file mode 100644 index 000000000000..20b44bb32da9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/nav2/+page.svelte @@ -0,0 +1 @@ +

    Navigation 2

    diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts new file mode 100644 index 000000000000..66be1892123d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.client.test.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; +import { waitForInitialPageload } from './utils'; + +test.describe('client-specific performance events', () => { + test('multiple navigations have distinct traces', async ({ page }) => { + const navigationTxn1EventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/nav1' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const navigationTxn2EventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const navigationTxn3EventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/nav2' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + await waitForInitialPageload(page); + + const [navigationTxn1Event] = await Promise.all([navigationTxn1EventPromise, page.getByText('Nav 1').click()]); + const [navigationTxn2Event] = await Promise.all([navigationTxn2EventPromise, page.goBack()]); + const [navigationTxn3Event] = await Promise.all([navigationTxn3EventPromise, page.getByText('Nav 2').click()]); + + expect(navigationTxn1Event).toMatchObject({ + transaction: '/nav1', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + expect(navigationTxn2Event).toMatchObject({ + transaction: '/', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + expect(navigationTxn3Event).toMatchObject({ + transaction: '/nav2', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + // traces should NOT be connected + expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn2Event.contexts?.trace?.trace_id); + expect(navigationTxn2Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id); + expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id); + }); +}); diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 42fc68fcaabf..58bd3e3b9790 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -8,6 +8,7 @@ import { getActiveSpan, getClient, getCurrentScope, + getIsolationScope, getRootSpan, spanToJSON, startIdleSpan, @@ -15,7 +16,13 @@ import { } from '@sentry/core'; import type { Client, IntegrationFn, StartSpanOptions, TransactionSource } from '@sentry/types'; import type { Span } from '@sentry/types'; -import { addHistoryInstrumentationHandler, browserPerformanceTimeOrigin, getDomElement, logger } from '@sentry/utils'; +import { + addHistoryInstrumentationHandler, + browserPerformanceTimeOrigin, + getDomElement, + logger, + uuid4, +} from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; import { registerBackgroundTabDetection } from './backgroundtab'; @@ -373,6 +380,15 @@ export function startBrowserTracingPageLoadSpan( * This will only do something if a browser tracing integration has been setup. */ export function startBrowserTracingNavigationSpan(client: Client, spanOptions: StartSpanOptions): Span | undefined { + getCurrentScope().setPropagationContext({ + traceId: uuid4(), + spanId: uuid4().substring(16), + }); + getIsolationScope().setPropagationContext({ + traceId: uuid4(), + spanId: uuid4().substring(16), + }); + client.emit('startNavigationSpan', spanOptions); getCurrentScope().setTransactionName(spanOptions.name); diff --git a/packages/tracing-internal/test/browser/browserTracingIntegration.test.ts b/packages/tracing-internal/test/browser/browserTracingIntegration.test.ts index 243ef14f159c..b537d684c8eb 100644 --- a/packages/tracing-internal/test/browser/browserTracingIntegration.test.ts +++ b/packages/tracing-internal/test/browser/browserTracingIntegration.test.ts @@ -603,7 +603,7 @@ describe('browserTracingIntegration', () => { expect(spanToJSON(pageloadSpan!).data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); }); - it('sets the pageload span name on `scope.transactionName`', () => { + it('sets the navigation span name on `scope.transactionName`', () => { const client = new TestClient( getDefaultClientOptions({ integrations: [browserTracingIntegration()], @@ -612,10 +612,48 @@ describe('browserTracingIntegration', () => { setCurrentClient(client); client.init(); - startBrowserTracingPageLoadSpan(client, { name: 'test navigation span' }); + startBrowserTracingNavigationSpan(client, { name: 'test navigation span' }); expect(getCurrentScope().getScopeData().transactionName).toBe('test navigation span'); }); + + it("resets the scopes' propagationContexts", () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const oldIsolationScopePropCtx = getIsolationScope().getPropagationContext(); + const oldCurrentScopePropCtx = getCurrentScope().getPropagationContext(); + + startBrowserTracingNavigationSpan(client, { name: 'test navigation span' }); + + const newIsolationScopePropCtx = getIsolationScope().getPropagationContext(); + const newCurrentScopePropCtx = getCurrentScope().getPropagationContext(); + + expect(oldCurrentScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(oldIsolationScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(newCurrentScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + expect(newIsolationScopePropCtx).toEqual({ + spanId: expect.stringMatching(/[a-f0-9]{16}/), + traceId: expect.stringMatching(/[a-f0-9]{32}/), + }); + + expect(newIsolationScopePropCtx?.traceId).not.toEqual(oldIsolationScopePropCtx?.traceId); + expect(newCurrentScopePropCtx?.traceId).not.toEqual(oldCurrentScopePropCtx?.traceId); + }); }); describe('using the tag data', () => {