From 1094e2c7890be5273b1b07b0b47c6bb157ff7d34 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 27 Jul 2026 15:00:51 +0200 Subject: [PATCH] test(nextjs): Add webpack variant for span-streaming e2e app Adds a webpack build variant to the nextjs-16-streaming e2e app so the webpack + span-streaming combination has coverage. Adds a reusable `collectStreamedSpans` test-util that accumulates streamed spans across envelopes, and uses it in the server-component tests (streamed child spans can arrive in a different envelope than their segment root). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../nextjs-16-streaming/package.json | 16 +++++++++- .../tests/server-components.test.ts | 25 ++++++++------- .../test-utils/src/event-proxy-server.ts | 31 +++++++++++++++++++ dev-packages/test-utils/src/index.ts | 1 + 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json index 4fbc3bf64c27..64e43ae8ba30 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json @@ -6,12 +6,17 @@ "dev": "next dev", "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs", + "dev:webpack": "next dev --webpack", + "build-webpack": "next build --webpack > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", "start": "next start", "lint": "eslint", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", + "test:dev-webpack": "TEST_ENV=development-webpack playwright test", "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test:prod && pnpm test:dev" + "test:build-webpack": "pnpm install && pnpm build-webpack", + "test:assert": "pnpm test:prod && pnpm test:dev", + "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", @@ -37,5 +42,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "build-command": "pnpm test:build-webpack", + "label": "nextjs-16-streaming (webpack)", + "assert-command": "pnpm test:assert-webpack" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/server-components.test.ts index 0eafb218aa1d..e0469b42af34 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/server-components.test.ts @@ -1,7 +1,16 @@ import { expect, test } from '@playwright/test'; -import { waitForStreamedSpan, waitForStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, waitForStreamedSpan, getSpanOp } from '@sentry-internal/test-utils'; import { isDevMode } from './isDevMode'; +// Streamed spans are flushed across multiple envelopes as they end, so the server-component child spans +// can arrive in a different (earlier) envelope than the `is_segment` root span. Accumulate spans across +// envelopes until the root span (which ends last) is seen. +function collectSpanNamesUntilSegment(segmentName: string): Promise { + return collectStreamedSpans('nextjs-16-streaming', spans => + spans.some(span => span.name === segmentName && span.is_segment), + ).then(spans => spans.map(span => span.name)); +} + test('Sends a streamed span for a request to app router with URL', async ({ page }) => { test.skip(isDevMode, 'Turbopack intermittently returns 404 for nested dynamic routes in dev mode'); @@ -22,14 +31,11 @@ test('Will create streamed spans for every server component and metadata generat }) => { test.skip(isDevMode, 'Turbopack intermittently returns 404 for nested dynamic routes in dev mode'); - const spansPromise = waitForStreamedSpans('nextjs-16-streaming', spans => { - return spans.some(span => span.name === 'GET /nested-layout' && span.is_segment); - }); + const spanNamesPromise = collectSpanNamesUntilSegment('GET /nested-layout'); await page.goto('/nested-layout'); - const spans = await spansPromise; - const spanNames = spans.map(span => span.name); + const spanNames = await spanNamesPromise; expect(spanNames).toContainEqual('render route (app) /nested-layout'); expect(spanNames).toContainEqual('build component tree'); @@ -46,14 +52,11 @@ test('Will create streamed spans for every server component and metadata generat }) => { test.skip(isDevMode, 'Turbopack intermittently returns 404 for nested dynamic routes in dev mode'); - const spansPromise = waitForStreamedSpans('nextjs-16-streaming', spans => { - return spans.some(span => span.name === 'GET /nested-layout/[dynamic]' && span.is_segment); - }); + const spanNamesPromise = collectSpanNamesUntilSegment('GET /nested-layout/[dynamic]'); await page.goto('/nested-layout/123'); - const spans = await spansPromise; - const spanNames = spans.map(span => span.name); + const spanNames = await spanNamesPromise; expect(spanNames).toContainEqual('resolve page components'); expect(spanNames).toContainEqual('render route (app) /nested-layout/[dynamic]'); diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 16f080d10b77..6b615168c122 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -606,6 +606,37 @@ export function waitForStreamedSpans( }); } +/** + * Accumulate streamed Span V2 spans across multiple envelopes until `isDone` returns true. + * + * Unlike {@link waitForStreamedSpans}, which resolves with the spans of a single envelope, this + * collects spans from every Span V2 envelope as they arrive and resolves with the full set once + * `isDone` is satisfied. Streamed spans are flushed in multiple envelopes as they end (a child span + * can be sent before its root segment span), so any assertion that needs the whole trace must + * accumulate rather than snapshot a single envelope. + * + * `isDone` receives all spans collected so far. A common predicate is "the segment/root span has + * arrived", since the root ends last and therefore flushes after its children: + * + * @example + * ```ts + * const spans = await collectStreamedSpans(PROXY_SERVER_NAME, allSpans => + * allSpans.some(span => span.name === 'GET /nested-layout' && span.is_segment), + * ); + * expect(spans.map(span => span.name)).toContainEqual('build component tree'); + * ``` + */ +export function collectStreamedSpans( + proxyServerName: string, + isDone: (spans: SerializedStreamedSpan[]) => boolean, +): Promise { + const collected: SerializedStreamedSpan[] = []; + return waitForStreamedSpans(proxyServerName, spans => { + collected.push(...spans); + return isDone(collected); + }).then(() => collected); +} + /** * Helper to get the span operation from a Span V2 JSON object. * diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index aaf4cc1d886a..d242a7615d3c 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -11,6 +11,7 @@ export { waitForStreamedSpan, waitForStreamedSpans, waitForStreamedSpanEnvelope, + collectStreamedSpans, getSpanOp, } from './event-proxy-server';