From 045f7c4bb657acac9f21bf99383d8fa4d26384d6 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 4 Sep 2026 10:45:55 +0200 Subject: [PATCH] test(e2e): Port the hydrogen-react-router-7 E2E app to span streaming Drops the `traceLifecycle: 'static'` pin and rewrites the specs to assert on streamed spans. --- .../app/entry.client.tsx | 1 - .../instrument.server.mjs | 1 - .../hydrogen-react-router-7/server.ts | 1 - .../tests/client-errors.test.ts | 16 ++--- .../tests/client-transactions.test.ts | 35 ++++------ .../tests/server-transactions.test.ts | 68 +++++++++---------- 6 files changed, 54 insertions(+), 68 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.client.tsx index 99607247c7be..bd606bbe7c08 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/app/entry.client.tsx @@ -4,7 +4,6 @@ import { StrictMode, startTransition } from 'react'; import { hydrateRoot } from 'react-dom/client'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions // Could not find a working way to set the DSN in the browser side from the environment variables dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/instrument.server.mjs index 39bcf6e1000b..195ecfcc6f33 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/instrument.server.mjs +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/instrument.server.mjs @@ -1,6 +1,5 @@ import * as Sentry from '@sentry/react-router'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', // Adds request headers and IP for users, for more info visit: // https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#dataCollection diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts index 36037b5d4894..9bb5529320b6 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/server.ts @@ -34,7 +34,6 @@ export default { return wrapRequestHandler( { options: { - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-errors.test.ts index db87e3c0b11b..19657b1bfc0e 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-errors.test.ts @@ -1,12 +1,12 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends a client-side exception to Sentry', async ({ page }) => { - // The pageload transaction only completes once the client SDK and the router have hydrated. + // The pageload span only completes once the client SDK and the router have hydrated. // Awaiting it before clicking guarantees the button's onClick handler is attached — a click that // lands before hydration would do nothing, and the exception would never be captured. - const pageloadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/'; + const pageloadSpanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/'; }); const errorPromise = waitForError('hydrogen-react-router-7', errorEvent => { @@ -15,7 +15,7 @@ test('Sends a client-side exception to Sentry', async ({ page }) => { await page.goto('/'); - await pageloadTransactionPromise; + await pageloadSpanPromise; const exceptionButton = page.locator('id=exception-button'); await exceptionButton.click(); @@ -27,8 +27,8 @@ test('Sends a client-side exception to Sentry', async ({ page }) => { test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => { // Wait for hydration (see above) before clicking, so the button's onClick handler is attached. - const pageloadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/client-error'; + const pageloadSpanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/client-error'; }); const errorPromise = waitForError('hydrogen-react-router-7', errorEvent => { @@ -37,7 +37,7 @@ test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) = await page.goto('/client-error'); - await pageloadTransactionPromise; + await pageloadSpanPromise; const throwButton = page.locator('id=throw-on-click'); await throwButton.click(); diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-transactions.test.ts index a23e49338f2c..f0f8cd0850e0 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/client-transactions.test.ts @@ -1,44 +1,39 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('Sends a pageload transaction to Sentry', async ({ page }) => { - const transactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/'; +test('Sends a pageload span to Sentry', async ({ page }) => { + const spanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/'; }); await page.goto('/'); - const transactionEvent = await transactionPromise; - - expect(transactionEvent).toBeDefined(); + expect(await spanPromise).toBeDefined(); }); -test('Sends a navigation transaction to Sentry', async ({ page }) => { - // Wait for the initial pageload transaction first. This ensures the client SDK and router are fully +test('Sends a navigation span to Sentry', async ({ page }) => { + // Wait for the initial pageload span first. This ensures the client SDK and router are fully // hydrated before we click the link. Clicking before hydration completes makes the `` behave - // like a plain anchor, triggering a full page navigation (a `pageload` transaction) instead of a + // like a plain anchor, triggering a full page navigation (a `pageload` span) instead of a // client-side `navigation` one, which makes this test flaky. - const pageloadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/'; + const pageloadSpanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/'; }); - const transactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id'; + const spanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/user/:id'; }); await page.goto('/'); - await pageloadTransactionPromise; + await pageloadSpanPromise; const linkElement = page.locator('id=navigation'); await linkElement.click(); - const transactionEvent = await transactionPromise; + const span = await spanPromise; - expect(transactionEvent).toBeDefined(); - expect(transactionEvent).toMatchObject({ - transaction: '/user/:id', - }); + expect(span.name).toBe('/user/:id'); }); test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/server-transactions.test.ts index 1dca64548e83..1c1b7aa054f5 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/tests/server-transactions.test.ts @@ -1,54 +1,48 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; + +const APP_NAME = 'hydrogen-react-router-7'; test.describe.configure({ mode: 'serial' }); -test('Sends parameterized transaction name to Sentry', async ({ page }) => { - const transactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server'; +test('Sends a parameterized span name to Sentry', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + // The span name is parameterized (route pattern, not the actual URL). + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /user/:id'; }); await page.goto('/user/123'); - const transaction = await transactionPromise; + const span = await spanPromise; - expect(transaction).toBeDefined(); - // Transaction name should be parameterized (route pattern, not actual URL) - expect(transaction.transaction).toBe('GET /user/:id'); + expect(span.attributes['sentry.segment.name.source']?.value).toBe('route'); }); -test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { - // We use this to identify the transactions - const testTag = crypto.randomUUID(); - - const httpServerTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; - }); - - const pageLoadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; +test('Sends two linked spans (server & client) to Sentry', async ({ page }) => { + // Streamed spans are buffered before they flush, so spans from an earlier page load can still be + // arriving here. The document advertises its own trace in the `sentry-trace` meta tag, so that is + // what tells this page load's spans apart rather than the op or the URL. + const streamedSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); + return false; }); - page.goto(`/?tag=${testTag}`); - - const pageloadTransaction = await pageLoadTransactionPromise; - const httpServerTransaction = await httpServerTransactionPromise; - - expect(pageloadTransaction).toBeDefined(); - expect(httpServerTransaction).toBeDefined(); - - const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id; - const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id; - - const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id; - const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; + await page.goto('/'); - expect(httpServerTransaction.transaction).toBe('GET /'); - expect(pageloadTransaction.transaction).toBe('/'); + const sentryTrace = await page.getAttribute('meta[name="sentry-trace"]', 'content'); + const [traceId] = (sentryTrace ?? '').split('-'); + expect(traceId).toMatch(/^[a-f0-9]{32}$/); - expect(httpServerTraceId).toBeDefined(); - expect(httpServerSpanId).toBeDefined(); + const findServerSegmentSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === traceId); + await expect.poll(findServerSegmentSpan).toBeDefined(); + expect(findServerSegmentSpan()!.name).toBe('GET /'); - expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadSpanId).not.toEqual(httpServerSpanId); + const findPageloadSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'pageload' && span.is_segment && span.trace_id === traceId); + await expect.poll(findPageloadSpan).toBeDefined(); + expect(findPageloadSpan()!.name).toBe('/'); + expect(findPageloadSpan()!.span_id).not.toBe(findServerSegmentSpan()!.span_id); });