From 24e9a7ec2dbae64449562bf8e41d0ae6a3f556cb Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:50:43 +0200 Subject: [PATCH] test(e2e): Migrate nuxt-3-dynamic-import to span streaming Co-Authored-By: Claude Fable 5 --- .../sentry.client.config.ts | 1 - .../sentry.server.config.ts | 1 - .../tests/tracing.client.test.ts | 71 +++---- .../tests/tracing.server.test.ts | 36 ++-- .../tests/tracing.test.ts | 197 +++++++++--------- 5 files changed, 139 insertions(+), 167 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.client.config.ts index e14d25bfc0c1..9a9566051452 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/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-dynamic-import/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts index 60138a64a4c8..729b2296c683 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/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-dynamic-import/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.client.test.ts index 9c0b8232b823..7172e69c052a 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.client.test.ts @@ -1,60 +1,49 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import type { Span } from '@sentry/nuxt'; +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-dynamic-import', async transactionEvent => { - return transactionEvent.transaction === '/test-param/:param()'; + const pageloadSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', 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-dynamic-import', async transactionEvent => { - return transactionEvent.transaction === '/client-error'; - }); + const spansPromise = collectStreamedSpans('nuxt-3-dynamic-import', 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) => 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-dynamic-import/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts index fc39ac094910..2320c83cfbb2 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.server.test.ts @@ -1,45 +1,39 @@ 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-dynamic-import', transactionEvent => { - return transactionEvent.transaction.includes('GET /test-param/'); +test('sends a server root span on pageload', async ({ page }) => { + const serverSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => { + return span.is_segment && span.name.includes('GET /test-param/'); }); 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-dynamic-import', transactionEvent => { - if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + waitForStreamedSpan('nuxt-3-dynamic-import', span => { + if (span.is_segment && /^GET \/_nuxt\//.test(span.name)) { buildAssetFolderOccurred = true; } return false; // expects to return a boolean (but not relevant here) }); - const transactionEventPromise = waitForTransaction('nuxt-3-dynamic-import', transactionEvent => { - return transactionEvent.transaction.includes('GET /test-param/'); + const serverSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => { + return span.is_segment && span.name.includes('GET /test-param/'); }); await page.goto('/test-param/1234'); - const transactionEvent = await transactionEventPromise; + const serverSpan = await serverSpanPromise; expect(buildAssetFolderOccurred).toBe(false); - expect(transactionEvent.transaction).toBe('GET /test-param/:param()'); + expect(serverSpan.name).toBe('GET /test-param/:param()'); + expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('route'); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts index 94c8df1642de..12d8e9d3a3e0 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts @@ -1,22 +1,23 @@ 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-dynamic-import', txnEvent => { - return txnEvent.transaction === '/test-param/:param()'; + const clientSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const serverTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { - return txnEvent.transaction?.includes('GET /test-param/') || false; + const serverSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => { + return span.is_segment && span.name.includes('GET /test-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(), ]); @@ -24,7 +25,7 @@ test.describe('distributed tracing', () => { // URL-encoded for parametrized 'GET /test-param/s0me-param' -> `GET /test-param/:param` expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`); - 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'); @@ -33,125 +34,115 @@ 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()`, // parametrized - transaction_info: { source: 'route' }, - type: 'transaction', - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.http_server', - }, - }, + expect(serverSpan).toMatchObject({ + name: 'GET /test-param/:param()', // parametrized + 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: 'route' }, + }), }); // 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-dynamic-import', 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-dynamic-import', 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-dynamic-import', txnEvent => { - return txnEvent.transaction?.includes('GET /test-param/user') ?? false; + const ssrSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', span => { + return span.is_segment && span.name.includes('GET /test-param/user'); }); - const serverReqTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => { - return txnEvent.transaction?.includes('GET /api/user/') ?? false; + const serverReqSpanPromise = waitForStreamedSpan('nuxt-3-dynamic-import', 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/:userId()`, // parametrized route - transaction_info: { source: 'route' }, - contexts: expect.objectContaining({ - trace: expect.objectContaining({ - op: 'http.server', - origin: 'auto.http.http_server', - }), - }), + expect(ssrSpan).toMatchObject({ + name: 'GET /test-param/user/:userId()', // parametrized route + 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: 'route' }, }), - ); + }); - 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); }); });