diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx index 68e644bf1a3c..85c29d310c1a 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/app/entry.client.tsx @@ -20,7 +20,6 @@ import { StrictMode, startTransition, useEffect } from 'react'; import { hydrateRoot } from 'react-dom/client'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: window.ENV.SENTRY_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs b/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs index c231a4742dfb..6d211cac4592 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/instrument.server.cjs @@ -1,7 +1,6 @@ const Sentry = require('@sentry/remix'); Sentry.init({ - traceLifecycle: 'static', tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts index c379aeffa559..53970bad321e 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts @@ -1,21 +1,25 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; + +const APP_NAME = 'remix-server-timing'; test('propagates trace context from server-timing header to client pageload', async ({ page }) => { const testTag = crypto.randomUUID(); + // Streamed spans are buffered before they flush, so spans from an earlier page load can still be + // arriving here. The `Server-Timing` header advertises this response's own trace, 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; + }); + const responsePromise = page.waitForResponse( response => response.url().includes(`tag=${testTag}`) && response.status() === 200, ); - const pageLoadTransactionPromise = waitForTransaction('remix-server-timing', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; - }); - - const httpServerTransactionPromise = waitForTransaction('remix-server-timing', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server'; - }); - await page.goto(`/?tag=${testTag}`); const response = await responsePromise; @@ -33,31 +37,33 @@ test('propagates trace context from server-timing header to client pageload', as expect(headerSpanId).toHaveLength(16); expect(headerSampled).toBe('1'); - const pageloadTransaction = await pageLoadTransactionPromise; - const httpServerTransaction = await httpServerTransactionPromise; - - expect(pageloadTransaction).toBeDefined(); - expect(pageloadTransaction.transaction).toBe('/'); - - expect(httpServerTransaction.transaction).toBe('GET /'); - - expect(pageloadTransaction.contexts?.trace?.trace_id).toEqual(headerTraceId); - expect(pageloadTransaction.contexts?.trace?.parent_span_id).toEqual(headerSpanId); - - expect(httpServerTransaction.contexts?.trace?.trace_id).toEqual(headerTraceId); - expect(httpServerTransaction.contexts?.trace?.span_id).toEqual(headerSpanId); + const findServerSegmentSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === headerTraceId); + await expect.poll(findServerSegmentSpan).toBeDefined(); + // The index route has no path of its own, so the segment keeps the low-cardinality method-only + // name it starts with. + expect(findServerSegmentSpan()!.name).toBe('GET'); + expect(findServerSegmentSpan()!.span_id).toBe(headerSpanId); + + const findPageloadSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'pageload' && span.is_segment && span.trace_id === headerTraceId); + await expect.poll(findPageloadSpan).toBeDefined(); + expect(findPageloadSpan()!.name).toBe('/'); + expect(findPageloadSpan()!.parent_span_id).toBe(headerSpanId); }); test('includes server-timing header on redirect responses', async ({ page }) => { + const streamedSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); + return false; + }); + const redirectResponsePromise = page.waitForResponse(response => response.url().includes('/redirect-test')); const redirectedPageloadResponsePromise = page.waitForResponse(response => response.url().includes('/user/redirected'), ); - const pageLoadTransactionPromise = waitForTransaction('remix-server-timing', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload'; - }); - await page.goto('/redirect-test'); const redirectResponse = await redirectResponsePromise; @@ -83,10 +89,11 @@ test('includes server-timing header on redirect responses', async ({ page }) => await page.waitForURL(/\/user\/redirected/); await expect(page.locator('h1')).toContainText('User redirected'); - const pageLoadTransaction = await pageLoadTransactionPromise; - expect(pageLoadTransaction.transaction).toBe('/user/:id'); - expect(pageLoadTransaction.contexts?.trace?.trace_id).toEqual(traceId); - expect(pageLoadTransaction.contexts?.trace?.parent_span_id).toEqual(spanId); + const findPageloadSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'pageload' && span.is_segment && span.trace_id === traceId); + await expect.poll(findPageloadSpan).toBeDefined(); + expect(findPageloadSpan()!.name).toBe('/user/:id'); + expect(findPageloadSpan()!.parent_span_id).toBe(spanId); }); test('excludes server-timing header from client-side navigation data fetches', async ({ page }) => {