diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts index e14d25bfc0c1..9a9566051452 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.client.config.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/nuxt'; import { useRuntimeConfig } from '#imports'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: useRuntimeConfig().public.sentry.dsn, tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts index 60138a64a4c8..729b2296c683 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/sentry.server.config.ts @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/nuxt'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts index 4c31667feed0..fe2699f082d5 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts @@ -1,5 +1,5 @@ import { expect, test, type Page } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('Rendering Modes with Cached HTML', () => { test('changes tracing meta tags with multiple requests on Client-Side only page', async ({ page }) => { @@ -27,6 +27,20 @@ test.describe('Rendering Modes with Cached HTML', () => { }); }); +// Cached pages have exact-match routes, so the server segment is selected by `url.path` instead of +// its name (route matching may or may not rename exact-match segments under span streaming). +function waitForServerSegment(routePath: string) { + return waitForStreamedSpan('nuxt-3-min', span => { + return span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === routePath; + }); +} + +function waitForPageloadSegment(routePath: string) { + return waitForStreamedSpan('nuxt-3-min', span => { + return span.is_segment && getSpanOp(span) === 'pageload' && span.attributes['url.path']?.value === routePath; + }); +} + /** * Tests that tracing meta-tags change with multiple requests on ISR-cached pages * This utility handles the common pattern of: @@ -45,18 +59,13 @@ export async function testChangingTracingMetaTagsOnISRPage( expectedPageText: string, ): Promise { // === 1. Request === - const clientTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction === routePath; - }); + const clientSpanPromise1 = waitForPageloadSegment(routePath); + const serverSpanPromise1 = waitForServerSegment(routePath); - const serverTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; - }); - - const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + const [_1, clientSpan1, serverSpan1] = await Promise.all([ page.goto(routePath), - clientTxnEventPromise1, - serverTxnEventPromise1, + clientSpanPromise1, + serverSpanPromise1, expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(), ]); @@ -66,18 +75,13 @@ export async function testChangingTracingMetaTagsOnISRPage( // === 2. Request === - const clientTxnEventPromise2 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction === routePath; - }); - - const serverTxnEventPromise2 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; - }); + const clientSpanPromise2 = waitForPageloadSegment(routePath); + const serverSpanPromise2 = waitForServerSegment(routePath); - const [_2, clientTxnEvent2, serverTxnEvent2] = await Promise.all([ + const [_2, clientSpan2, serverSpan2] = await Promise.all([ page.goto(routePath), - clientTxnEventPromise2, - serverTxnEventPromise2, + clientSpanPromise2, + serverSpanPromise2, expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(), ]); @@ -85,30 +89,30 @@ export async function testChangingTracingMetaTagsOnISRPage( const sentryTraceMetaTagContent2 = await page.locator('meta[name="sentry-trace"]').getAttribute('content'); const [htmlMetaTraceId2] = sentryTraceMetaTagContent2?.split('-') || []; - const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id; - const serverTxnEvent2TraceId = serverTxnEvent2.contexts?.trace?.trace_id; + const serverSpan1TraceId = serverSpan1.trace_id; + const serverSpan2TraceId = serverSpan2.trace_id; await test.step('Test distributed trace from 1. request', () => { - expect(baggageMetaTagContent1).toContain(`sentry-trace_id=${serverTxnEvent1TraceId}`); + expect(baggageMetaTagContent1).toContain(`sentry-trace_id=${serverSpan1TraceId}`); - expect(clientTxnEvent1.contexts?.trace?.trace_id).toBe(serverTxnEvent1TraceId); - expect(clientTxnEvent1.contexts?.trace?.parent_span_id).toBe(serverTxnEvent1.contexts?.trace?.span_id); - expect(serverTxnEvent1.contexts?.trace?.trace_id).toBe(htmlMetaTraceId1); + expect(clientSpan1.trace_id).toBe(serverSpan1TraceId); + expect(clientSpan1.parent_span_id).toBe(serverSpan1.span_id); + expect(serverSpan1TraceId).toBe(htmlMetaTraceId1); }); await test.step('Test distributed trace from 2. request', () => { - expect(baggageMetaTagContent2).toContain(`sentry-trace_id=${serverTxnEvent2TraceId}`); + expect(baggageMetaTagContent2).toContain(`sentry-trace_id=${serverSpan2TraceId}`); - expect(clientTxnEvent2.contexts?.trace?.trace_id).toBe(serverTxnEvent2TraceId); - expect(clientTxnEvent2.contexts?.trace?.parent_span_id).toBe(serverTxnEvent2.contexts?.trace?.span_id); - expect(serverTxnEvent2.contexts?.trace?.trace_id).toBe(htmlMetaTraceId2); + expect(clientSpan2.trace_id).toBe(serverSpan2TraceId); + expect(clientSpan2.parent_span_id).toBe(serverSpan2.span_id); + expect(serverSpan2TraceId).toBe(htmlMetaTraceId2); }); await test.step('Test that trace IDs from subsequent requests are different', () => { - // Different trace IDs for the server transactions - expect(serverTxnEvent1TraceId).toBeDefined(); - expect(serverTxnEvent1TraceId).not.toBe(serverTxnEvent2TraceId); - expect(serverTxnEvent1TraceId).not.toBe(htmlMetaTraceId2); + // Different trace IDs for the server root spans + expect(serverSpan1TraceId).toBeDefined(); + expect(serverSpan1TraceId).not.toBe(serverSpan2TraceId); + expect(serverSpan1TraceId).not.toBe(htmlMetaTraceId2); }); } @@ -117,13 +121,11 @@ export async function testChangingTracingMetaTagsOnISRPage( * This utility handles the common pattern of: * 1. Making two requests to a cached page * 2. Verifying no tracing meta-tags are present - * 3. Verifying only the first request creates a server transaction - * 4. Verifying traces are not distributed + * 3. Verifying traces are not distributed (each pageload starts its own trace) * * @param page - Playwright page object * @param routePath - The route path to test (e.g., '/rendering-modes/swr-cached-page') * @param expectedPageText - The text to verify is visible on the page (e.g., 'SWR Cached Page') - * @returns Object containing transaction events for additional custom assertions */ export async function testExcludeTracingMetaTagsOnCachedPage( page: Page, @@ -131,19 +133,15 @@ export async function testExcludeTracingMetaTagsOnCachedPage( expectedPageText: string, ): Promise { // === 1. Request === - const clientTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction === routePath; - }); + const clientSpanPromise1 = waitForPageloadSegment(routePath); - // Only the 1. request creates a server transaction - const serverTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; - }); + // Only the 1. request creates a server root span + const serverSpanPromise1 = waitForServerSegment(routePath); - const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + const [_1, clientSpan1, serverSpan1] = await Promise.all([ page.goto(routePath), - clientTxnEventPromise1, - serverTxnEventPromise1, + clientSpanPromise1, + serverSpanPromise1, expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(), ]); @@ -153,54 +151,37 @@ export async function testExcludeTracingMetaTagsOnCachedPage( // === 2. Request === - await page.goto(routePath); - - const clientTxnEventPromise2 = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction === routePath; - }); - - let serverTxnEvent2 = undefined; - const serverTxnEventPromise2 = Promise.race([ - waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; - }), - new Promise((_, reject) => setTimeout(() => reject(new Error('No second server transaction expected')), 2000)), - ]); + const clientSpanPromise2 = waitForPageloadSegment(routePath); - try { - serverTxnEvent2 = await serverTxnEventPromise2; - throw new Error('Second server transaction should not have been sent'); - } catch (error) { - expect(error.message).toBe('No second server transaction expected'); - } - - const [clientTxnEvent2] = await Promise.all([ - clientTxnEventPromise2, + const [_2, clientSpan2] = await Promise.all([ + page.goto(routePath), + clientSpanPromise2, expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(), ]); - const clientTxnEvent1TraceId = clientTxnEvent1.contexts?.trace?.trace_id; - const clientTxnEvent2TraceId = clientTxnEvent2.contexts?.trace?.trace_id; + const clientSpan1TraceId = clientSpan1.trace_id; + const clientSpan2TraceId = clientSpan2.trace_id; - const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id; - const serverTxnEvent2TraceId = serverTxnEvent2?.contexts?.trace?.trace_id; + const serverSpan1TraceId = serverSpan1.trace_id; await test.step('No baggage and sentry-trace meta-tags are present on second request', async () => { expect(await page.locator('meta[name="baggage"]').count()).toBe(0); expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0); }); - await test.step('1. Server Transaction and all Client Transactions are defined', () => { - expect(serverTxnEvent1TraceId).toBeDefined(); - expect(clientTxnEvent1TraceId).toBeDefined(); - expect(clientTxnEvent2TraceId).toBeDefined(); - expect(serverTxnEvent2).toBeUndefined(); - expect(serverTxnEvent2TraceId).toBeUndefined(); + await test.step('1. server root span and all client root spans are defined', () => { + expect(serverSpan1TraceId).toBeDefined(); + expect(clientSpan1TraceId).toBeDefined(); + expect(clientSpan2TraceId).toBeDefined(); }); await test.step('Trace is not distributed', () => { // Cannot create distributed trace as HTML Meta Tags are not added (caching leads to multiple usages of the same server trace id) - expect(clientTxnEvent1TraceId).not.toBe(clientTxnEvent2TraceId); - expect(clientTxnEvent1TraceId).not.toBe(serverTxnEvent1TraceId); + expect(clientSpan1TraceId).not.toBe(clientSpan2TraceId); + expect(clientSpan1TraceId).not.toBe(serverSpan1TraceId); + expect(clientSpan2TraceId).not.toBe(serverSpan1TraceId); + // Without meta tags the pageloads have no parent and start their own traces + expect(clientSpan1.parent_span_id).toBeUndefined(); + expect(clientSpan2.parent_span_id).toBeUndefined(); }); } diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts index 3ec93344c3e1..00c10eed15a0 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts @@ -1,61 +1,49 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('sends a pageload root span with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('nuxt-3-min', async transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/test-param/:param()' - ); + const pageloadSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/test-param/1234`); - const rootSpan = await transactionPromise; - - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.pageload.vue', - 'sentry.op': 'pageload', - 'params.param': '1234', - 'url.template': '/test-param/:param()', - 'url.path': '/test-param/1234', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/), - }, - op: 'pageload', - origin: 'auto.pageload.vue', - }, - }, - transaction: '/test-param/:param()', - transaction_info: { - source: 'route', - }, + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/test-param/:param()'); + expect(pageloadSpan.status).toBe('ok'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, + 'sentry.op': { type: 'string', value: 'pageload' }, + 'params.param': { type: 'string', value: '1234' }, + 'url.template': { type: 'string', value: '/test-param/:param()' }, + 'url.path': { type: 'string', value: '/test-param/1234' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/) }, }); }); test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => { - const transactionPromise = waitForTransaction('nuxt-3-min', async transactionEvent => { - return transactionEvent.transaction === '/client-error'; - }); + const spansPromise = collectStreamedSpans('nuxt-3-min', spans => + spans.some(span => span.name === '/client-error' && span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.goto(`/client-error`); - const rootSpan = await transactionPromise; - const errorButtonSpan = rootSpan.spans.find(span => span.description === 'Vue '); + const spans = await spansPromise; + const errorButtonSpan = spans.find(span => span.name === 'Vue '); - const expected = { - data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.mount' }, - description: 'Vue ', - op: 'ui.mount', + expect(errorButtonSpan).toMatchObject({ + name: 'Vue ', + is_segment: false, parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.vue', - }; - - expect(errorButtonSpan).toMatchObject(expected); + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'ui.mount' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.vue' }, + }), + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts index bd6c4fa61602..f4714f86121f 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.server.test.ts @@ -1,46 +1,46 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a server action transaction on pageload', async ({ page }) => { - const transactionPromise = waitForTransaction('nuxt-3-min', transactionEvent => { - return transactionEvent.transaction.includes('GET /test-param/'); +// Parametrization does not work in Nuxt 3.7 yet, so server segments keep a method-only name and +// have to be selected via their `url.path` attribute. + +test('sends a server root span on pageload', async ({ page }) => { + const serverSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return ( + span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/test-param/1234' + ); }); await page.goto('/test-param/1234'); - const transaction = await transactionPromise; + const serverSpan = await serverSpanPromise; - expect(transaction.contexts.trace).toEqual( - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server', - }), - }), - ); + expect(getSpanOp(serverSpan)).toBe('http.server'); + expect(serverSpan.attributes['sentry.origin']?.value).toBe('auto.http.http_server'); }); -test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => { +test('does not send spans for build asset folder "_nuxt"', async ({ page }) => { let buildAssetFolderOccurred = false; - waitForTransaction('nuxt-3-min', transactionEvent => { - if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + waitForStreamedSpan('nuxt-3-min', span => { + if (span.is_segment && `${span.attributes['url.path']?.value}`.startsWith('/_nuxt/')) { buildAssetFolderOccurred = true; } return false; // expects to return a boolean (but not relevant here) }); - const transactionEventPromise = waitForTransaction('nuxt-3-min', transactionEvent => { - return transactionEvent.transaction.includes('GET /test-param/'); + const serverSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return ( + span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/test-param/1234' + ); }); await page.goto('/test-param/1234'); - const transactionEvent = await transactionEventPromise; + const serverSpan = await serverSpanPromise; expect(buildAssetFolderOccurred).toBe(false); - // Parametrization does not work in Nuxt 3.7 yet (only in newer versions) - expect(transactionEvent.transaction).toBe('GET /test-param/1234'); + expect(serverSpan.name).toBe('GET'); // method-only because the URL cannot be parametrized in Nuxt 3.7 + expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('url'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts index cbd74b6d6df5..a3234d2a252f 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts @@ -1,22 +1,29 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('distributed tracing', () => { const PARAM = 's0me-param'; + const API_PATH = `/api/user/${PARAM}`; test('capture a distributed pageload trace', async ({ page }) => { - const clientTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction === '/test-param/:param()'; + const clientSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const serverTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction.includes('GET /test-param/'); + // Parametrization does not work in Nuxt 3.7 yet, so the server segment keeps a method-only + // name and has to be selected via its `url.path` attribute. + const serverSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return ( + span.is_segment && + getSpanOp(span) === 'http.server' && + span.attributes['url.path']?.value === `/test-param/${PARAM}` + ); }); - const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + const [_, clientSpan, serverSpan] = await Promise.all([ page.goto(`/test-param/${PARAM}`), - clientTxnEventPromise, - serverTxnEventPromise, + clientSpanPromise, + serverSpanPromise, expect(page.getByText(`Param: ${PARAM}`)).toBeVisible(), ]); @@ -25,7 +32,7 @@ test.describe('distributed tracing', () => { // Parametrization does not work in Nuxt 3.7 yet (only in newer versions) // Without route attribute, the transaction name is not set on the baggage expect(baggageMetaTagContent).not.toContain('sentry-transaction='); - expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`); + expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverSpan.trace_id}`); expect(baggageMetaTagContent).toContain('sentry-sampled=true'); expect(baggageMetaTagContent).toContain('sentry-sample_rate=1'); @@ -34,125 +41,120 @@ test.describe('distributed tracing', () => { expect(metaSampled).toBe('1'); - expect(clientTxnEvent).toMatchObject({ - transaction: '/test-param/:param()', - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.vue', - trace_id: metaTraceId, - parent_span_id: metaParentSpanId, - }, - }, + expect(clientSpan).toMatchObject({ + name: '/test-param/:param()', + is_segment: true, + trace_id: metaTraceId, + parent_span_id: metaParentSpanId, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + }), }); - expect(serverTxnEvent).toMatchObject({ - transaction: `GET /test-param/${PARAM}`, // Parametrization does not work in Nuxt 3.7 yet (only in newer versions) - transaction_info: { source: 'url' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.http_server', - }, - }, + expect(serverSpan).toMatchObject({ + name: 'GET', // method-only because the URL cannot be parametrized in Nuxt 3.7 + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + 'sentry.segment.name.source': { type: 'string', value: 'url' }, + }), }); // connected trace - expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); - expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBeDefined(); - - expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); - expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); - expect(serverTxnEvent.contexts?.trace?.trace_id).toBe(metaTraceId); + expect(clientSpan.trace_id).toBe(serverSpan.trace_id); + expect(clientSpan.parent_span_id).toBe(serverSpan.span_id); + expect(serverSpan.trace_id).toBe(metaTraceId); }); test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { - const clientTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction === '/test-param/user/:userId()'; + // The `http.client` span ends after the pageload segment, so it can be flushed in a later + // envelope. Accumulate until both spans have arrived. + const clientSpansPromise = collectStreamedSpans('nuxt-3-min', spans => { + return ( + spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && + spans.some( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), + ) + ); }); - const ssrTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction?.includes('GET /test-param/user') ?? false; + // Parametrization does not work in Nuxt 3.7 yet, so the SSR segment keeps a method-only name + const ssrSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return ( + span.is_segment && + getSpanOp(span) === 'http.server' && + span.attributes['url.path']?.value === `/test-param/user/${PARAM}` + ); }); - const serverReqTxnEventPromise = waitForTransaction('nuxt-3-min', txnEvent => { - return txnEvent.transaction?.includes('GET /api/user/') ?? false; + const serverReqSpanPromise = waitForStreamedSpan('nuxt-3-min', span => { + return span.is_segment && span.name.includes('GET /api/user/'); }); // Navigate to the page which will trigger an API call from the client-side await page.goto(`/test-param/user/${PARAM}`); - const [clientTxnEvent, ssrTxnEvent, serverReqTxnEvent] = await Promise.all([ - clientTxnEventPromise, - ssrTxnEventPromise, - serverReqTxnEventPromise, + const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ + clientSpansPromise, + ssrSpanPromise, + serverReqSpanPromise, ]); - const httpClientSpan = clientTxnEvent?.spans?.find(span => span.description === `GET /api/user/${PARAM}`); - - expect(clientTxnEvent).toEqual( - expect.objectContaining({ - type: 'transaction', - transaction: '/test-param/user/:userId()', // parametrized route - transaction_info: { source: 'route' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'pageload', - origin: 'auto.pageload.vue', - }), - }), - }), + const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); + const httpClientSpan = clientSpans.find( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), ); + expect(pageloadSpan).toMatchObject({ + name: '/test-param/user/:userId()', + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + }), + }); + expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan).toEqual( - expect.objectContaining({ - description: `GET /api/user/${PARAM}`, // fixme: parametrize - parent_span_id: clientTxnEvent.contexts?.trace?.span_id, // pageload span is parent - data: expect.objectContaining({ - 'url.full': expect.stringContaining(`/api/user/${PARAM}`), - type: 'fetch', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'http.request.method': 'GET', - }), + expect(httpClientSpan).toMatchObject({ + // A relative fetch has no domain of its own, so it resolves against the page origin. + name: 'GET localhost', + parent_span_id: pageloadSpan?.span_id, // pageload span is parent + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: expect.stringContaining(API_PATH) }, + 'url.domain': { type: 'string', value: 'localhost' }, }), - ); + }); - expect(ssrTxnEvent).toEqual( - expect.objectContaining({ - type: 'transaction', - transaction: `GET /test-param/user/${PARAM}`, // Parametrization does not work in Nuxt 3.7 yet (only in newer versions) - transaction_info: { source: 'url' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.http_server', - }), - }), + expect(ssrSpan).toMatchObject({ + name: 'GET', // method-only because the URL cannot be parametrized in Nuxt 3.7 + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + 'sentry.segment.name.source': { type: 'string', value: 'url' }, }), - ); + }); - expect(serverReqTxnEvent).toEqual( - expect.objectContaining({ - type: 'transaction', - transaction: `GET /api/user/:userId`, // parametrized route - transaction_info: { source: 'route' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.http_server', - parent_span_id: httpClientSpan?.span_id, // http.client span is parent - }), - }), + expect(serverReqSpan).toMatchObject({ + name: 'GET /api/user/:userId', // parametrized route + is_segment: true, + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, }), - ); + }); - // All 3 transactions and the http.client span should share the same trace_id - expect(clientTxnEvent.contexts?.trace?.trace_id).toBeDefined(); - expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(httpClientSpan?.trace_id); - expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(ssrTxnEvent.contexts?.trace?.trace_id); - expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverReqTxnEvent.contexts?.trace?.trace_id); + // All 3 root spans and the http.client span should share the same trace_id + expect(pageloadSpan?.trace_id).toBeDefined(); + expect(pageloadSpan?.trace_id).toBe(httpClientSpan?.trace_id); + expect(pageloadSpan?.trace_id).toBe(ssrSpan.trace_id); + expect(pageloadSpan?.trace_id).toBe(serverReqSpan.trace_id); }); });