From d539ec74fd0abdc1ac78559f514c29bcce850073 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 1 Sep 2026 16:17:42 +0200 Subject: [PATCH 1/2] test(e2e): Port React E2E test apps to span streaming Removes the `traceLifecycle: 'static'` pin from `create-react-app`, `lighthouse-react`, `react-17`, `react-19` and `react-send-to-sentry`, and rewrites the `react-17` specs against streamed span v2. `react-send-to-sentry` needed more than a helper swap. Pageload and navigation segments never become transaction events under streaming, so the app records them from `beforeSendSpan` instead of the event processor, and the trace lookup matches a span id rather than a transaction event id. `react-19` keeps its specs unchanged: they only cover error handling and the profiler, neither of which depends on the trace lifecycle. --- .../create-react-app/src/index.tsx | 1 - .../lighthouse-react/src/main.tsx | 11 +-- .../test-applications/react-17/src/index.tsx | 1 - .../react-17/tests/errors.test.ts | 14 ++-- .../{transactions.test.ts => spans.test.ts} | 78 +++++++------------ .../test-applications/react-19/src/index.tsx | 1 - .../react-send-to-sentry/src/globals.d.ts | 9 ++- .../react-send-to-sentry/src/index.tsx | 24 +++--- .../tests/send-to-sentry.test.ts | 48 ++++++------ .../tests/utils/sentry-api.ts | 12 +-- 10 files changed, 93 insertions(+), 106 deletions(-) rename dev-packages/e2e-tests/test-applications/react-17/tests/{transactions.test.ts => spans.test.ts} (57%) diff --git a/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx b/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx index 51770b97da5a..3dcb4c4fd07f 100644 --- a/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/create-react-app/src/index.tsx @@ -5,7 +5,6 @@ import App from './App'; import './index.css'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], diff --git a/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx b/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx index 3c3d5e729156..352c53c7208b 100644 --- a/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/lighthouse-react/src/main.tsx @@ -16,7 +16,6 @@ performance.mark('sentry-sdk-init-start', { if (import.meta.env.MODE === 'tracing-replay') { Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -28,7 +27,6 @@ if (import.meta.env.MODE === 'tracing-replay') { } else if (import.meta.env.MODE === 'tracing') { // Tracing + errors, but no replay — isolates the replay integration's cost. Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -40,7 +38,6 @@ if (import.meta.env.MODE === 'tracing-replay') { // (We don't recommend this setup anywhere and neither will it work well. // this is purely for testing if it changes anything about overhead.) Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -53,7 +50,6 @@ if (import.meta.env.MODE === 'tracing-replay') { } else if (import.meta.env.MODE === 'errors-only') { // Default integrations only — errors are always captured, no tracing or replay. Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -61,7 +57,6 @@ if (import.meta.env.MODE === 'tracing-replay') { } else if (import.meta.env.MODE === 'minimal-integrations') { // Minimal integratoins setup only (everything necessary to automatically get errors) Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -79,7 +74,6 @@ if (import.meta.env.MODE === 'tracing-replay') { // DSN set but every integration disabled. Isolates the cost of the enabled // client itself from the default instrumentation that wraps DOM/timer/network APIs. Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -91,7 +85,6 @@ if (import.meta.env.MODE === 'tracing-replay') { // removeEventListener on ~32 prototypes plus setTimeout/setInterval/rAF/XHR. // Isolates that global monkey-patching cost from the rest of the defaults. Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -102,7 +95,6 @@ if (import.meta.env.MODE === 'tracing-replay') { // Default integrations minus Breadcrumbs, which adds a lot of monkey patching to // DOM and Network APIs as well as event targets and listeners Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -112,7 +104,6 @@ if (import.meta.env.MODE === 'tracing-replay') { // Default integrations minus Breadcrumbs, which adds a lot of monkey patching to // DOM and Network APIs as well as event targets and listeners Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.VITE_E2E_TEST_DSN as string | undefined, release: 'lighthouse-fixture', environment: 'qa', @@ -122,7 +113,7 @@ if (import.meta.env.MODE === 'tracing-replay') { } else if (import.meta.env.MODE === 'init-only') { // enabled: false makes the SDK a guaranteed no-op (no transport allocation, // no DSN warning). We're measuring pure SDK-loading + tree-shaking cost. - Sentry.init({ traceLifecycle: 'static', enabled: false }); + Sentry.init({ enabled: false }); } performance.measure('sentry-sdk-init-duration', { diff --git a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx index c303ff60fe80..49609a988202 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx @@ -16,7 +16,6 @@ import User from './pages/User'; const replay = Sentry.replayIntegration(); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts index 7088cd12e3bf..626ac20485f8 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts @@ -1,5 +1,5 @@ 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 correct error event', async ({ page }) => { const errorEventPromise = waitForError('react-17', event => { @@ -30,8 +30,8 @@ test('Sends correct error event', async ({ page }) => { }); test('Sets correct transactionName', async ({ page }) => { - const transactionPromise = waitForTransaction('react-17', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-17', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); const errorEventPromise = waitForError('react-17', event => { @@ -39,9 +39,9 @@ test('Sets correct transactionName', async ({ page }) => { }); await page.goto('/'); - const transactionEvent = await transactionPromise; + const pageloadSpan = await pageloadSpanPromise; - // Only capture error once transaction was sent + // Only capture error once the pageload span was sent const exceptionButton = page.locator('id=exception-button'); await exceptionButton.click(); @@ -53,7 +53,7 @@ test('Sets correct transactionName', async ({ page }) => { expect(errorEvent.transaction).toEqual('/'); expect(errorEvent.contexts?.trace).toEqual({ - trace_id: transactionEvent.contexts?.trace?.trace_id, - span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''), + trace_id: pageloadSpan.trace_id, + span_id: expect.not.stringContaining(pageloadSpan.span_id), }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-17/tests/spans.test.ts similarity index 57% rename from dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/react-17/tests/spans.test.ts index 2f0d922ea53b..aac9854a292e 100644 --- a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-17/tests/spans.test.ts @@ -1,69 +1,51 @@ import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('react-17', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-17', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/`); - const rootSpan = await transactionPromise; - - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter_v6', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, + const span = await spanPromise; + + expect(span.name).toBe('/'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { +test('sends a navigation span with a parameterized URL', async ({ page }) => { page.on('console', msg => console.log(msg.text())); - const pageloadTxnPromise = waitForTransaction('react-17', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-17', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('react-17', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('react-17', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; const linkElement = page.locator('id=navigation'); - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter_v6', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/user/5', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), - }, - }, - }, - transaction: '/user/:id', - transaction_info: { - source: 'route', - }, + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect(navigationSpan.name).toBe('/user/:id'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, + 'url.path': { value: '/user/5', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' }, }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx index c43ee75a22c8..16209793ae07 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -4,7 +4,6 @@ import ReactDOM from 'react-dom/client'; import Index from './pages/Index'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, release: 'e2e-test', diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/globals.d.ts index 4c48f7834434..7e15b6961e45 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/globals.d.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/globals.d.ts @@ -4,8 +4,15 @@ interface RecordedEvent { op?: string; } +/** A streamed segment span, identified by its span id rather than an event id. */ +interface RecordedSpan { + spanId: string; + traceId: string; + op?: string; +} + interface Window { - recordedTransactions?: RecordedEvent[]; + recordedSegmentSpans?: RecordedSpan[]; capturedException?: RecordedEvent; sentryReplayId?: string; } diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx index 80cca0f0e55e..137cd74ea4bd 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/src/index.tsx @@ -16,7 +16,6 @@ import User from './pages/User'; const replay = Sentry.replayIntegration(); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ @@ -37,6 +36,19 @@ Sentry.init({ // Always capture replays, so we can test this properly replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, + + // Streamed spans never become transaction events, so the pageload and navigation segments are + // recorded here instead of in the event processor below. They are looked up by span id. + beforeSendSpan(span) { + const op = span.attributes['sentry.op']; + + if (span.is_segment && typeof op === 'string' && (op === 'pageload' || op === 'navigation')) { + window.recordedSegmentSpans = window.recordedSegmentSpans || []; + window.recordedSegmentSpans.push({ spanId: span.span_id, traceId: span.trace_id, op }); + } + + return span; + }, }); Object.defineProperty(window, 'sentryReplayId', { @@ -50,16 +62,8 @@ Object.defineProperty(window, 'sentryReplayId', { Sentry.addEventProcessor(event => { const eventId = event.event_id; const traceId = event.contexts?.trace?.trace_id; - const op = event.contexts?.trace?.op; - - if (!eventId || !traceId) { - return event; - } - if (event.type === 'transaction' && (op === 'pageload' || op === 'navigation')) { - window.recordedTransactions = window.recordedTransactions || []; - window.recordedTransactions.push({ eventId, traceId, op }); - } else if (!event.type && event.exception) { + if (eventId && traceId && !event.type && event.exception) { window.capturedException = { eventId, traceId }; } diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts index 4ddd7af0ce29..abcfa676bf8c 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { ReplayRecordingData } from './fixtures/ReplayRecordingData'; -import { EVENT_POLLING_OPTIONS, findErrorInTrace, findTransactionInTrace } from './utils/sentry-api'; +import { EVENT_POLLING_OPTIONS, findErrorInTrace, findSegmentSpanInTrace } from './utils/sentry-api'; const EVENT_POLLING_TIMEOUT = 90_000; @@ -28,52 +28,56 @@ test('Sends an exception to Sentry', async ({ page }) => { await expect.poll(() => findErrorInTrace(traceId, eventId), EVENT_POLLING_OPTIONS).toBeDefined(); }); -test('Sends a pageload transaction to Sentry', async ({ page }) => { +test('Sends a pageload span to Sentry', async ({ page }) => { await page.goto('/'); - const transactionHandle = await page.waitForFunction(() => - window.recordedTransactions?.find(transaction => transaction.op === 'pageload'), + const spanHandle = await page.waitForFunction(() => + window.recordedSegmentSpans?.find(span => span.op === 'pageload'), ); - const pageloadTransaction = await transactionHandle.jsonValue(); + const pageloadSpan = await spanHandle.jsonValue(); - if (pageloadTransaction === undefined) { - throw new Error("Application didn't record a pageload transaction."); + if (pageloadSpan === undefined) { + throw new Error("Application didn't record a pageload span."); } - const { eventId, traceId } = pageloadTransaction; + const { spanId, traceId } = pageloadSpan; - console.log(`Polling for pageload transaction eventId: ${eventId} in trace: ${traceId}`); + console.log(`Polling for pageload spanId: ${spanId} in trace: ${traceId}`); await expect - .poll(() => findTransactionInTrace(traceId, eventId), EVENT_POLLING_OPTIONS) - .toMatchObject({ op: 'pageload' }); + .poll(() => findSegmentSpanInTrace(traceId, spanId), EVENT_POLLING_OPTIONS) + .toMatchObject({ + op: 'pageload', + }); }); -test('Sends a navigation transaction to Sentry', async ({ page }) => { +test('Sends a navigation span to Sentry', async ({ page }) => { await page.goto('/'); - // Give pageload transaction time to finish + // Give the pageload span time to finish await page.waitForTimeout(4000); const linkElement = page.locator('id=navigation'); await linkElement.click(); - const transactionHandle = await page.waitForFunction(() => - window.recordedTransactions?.find(transaction => transaction.op === 'navigation'), + const spanHandle = await page.waitForFunction(() => + window.recordedSegmentSpans?.find(span => span.op === 'navigation'), ); - const navigationTransaction = await transactionHandle.jsonValue(); + const navigationSpan = await spanHandle.jsonValue(); - if (navigationTransaction === undefined) { - throw new Error("Application didn't record a navigation transaction."); + if (navigationSpan === undefined) { + throw new Error("Application didn't record a navigation span."); } - const { eventId, traceId } = navigationTransaction; + const { spanId, traceId } = navigationSpan; - console.log(`Polling for navigation transaction eventId: ${eventId} in trace: ${traceId}`); + console.log(`Polling for navigation spanId: ${spanId} in trace: ${traceId}`); await expect - .poll(() => findTransactionInTrace(traceId, eventId), EVENT_POLLING_OPTIONS) - .toMatchObject({ op: 'navigation' }); + .poll(() => findSegmentSpanInTrace(traceId, spanId), EVENT_POLLING_OPTIONS) + .toMatchObject({ + op: 'navigation', + }); }); test('Sends a Replay recording to Sentry', async ({ browser }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts index 31e2adbd21c5..d451b0261992 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts @@ -12,12 +12,10 @@ export const EVENT_POLLING_OPTIONS = { timeout: 180_000, intervals: [5_000] }; * occurrences all share this shape and are discriminated by `event_type`. */ export interface TraceItem { + /** On a span this is the span id. */ event_id?: string; - /** On spans this is the event id of the transaction the span belongs to. */ - transaction_id?: string; event_type?: 'span' | 'error' | 'occurrence' | 'uptime_check'; op?: string; - is_transaction?: boolean; children?: TraceItem[]; errors?: TraceItem[]; occurrences?: TraceItem[]; @@ -67,6 +65,10 @@ export async function findErrorInTrace(traceId: string, eventId: string): Promis return flattenTrace(await fetchTrace(traceId)).find(item => item.event_type === 'error' && item.event_id === eventId); } -export async function findTransactionInTrace(traceId: string, eventId: string): Promise { - return flattenTrace(await fetchTrace(traceId)).find(item => item.is_transaction && item.transaction_id === eventId); +/** + * Streamed spans never become transaction events, so a segment span is looked up by its span id + * rather than by the event id of an enclosing transaction. + */ +export async function findSegmentSpanInTrace(traceId: string, spanId: string): Promise { + return flattenTrace(await fetchTrace(traceId)).find(item => item.event_type === 'span' && item.event_id === spanId); } From c729238b70b6f6cf19a741ca1430644056c4a60a Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 1 Sep 2026 17:45:39 +0200 Subject: [PATCH 2/2] Match the streamed segment in Sentry by op instead of span id Matching `event_id` against the recorded span id never resolved, so both span lookups polled for the full three minutes. The trace is already unique to the pageload or navigation under test, so the op identifies the segment within it without depending on which field carries the span id. Log the trace's item shapes once when the op is missing, so a repeat failure is diagnosable. --- .../tests/send-to-sentry.test.ts | 14 ++-------- .../tests/utils/sentry-api.ts | 28 ++++++++++++++++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts index abcfa676bf8c..1041b2d0a303 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { ReplayRecordingData } from './fixtures/ReplayRecordingData'; -import { EVENT_POLLING_OPTIONS, findErrorInTrace, findSegmentSpanInTrace } from './utils/sentry-api'; +import { EVENT_POLLING_OPTIONS, findErrorInTrace, findSpanInTrace } from './utils/sentry-api'; const EVENT_POLLING_TIMEOUT = 90_000; @@ -44,11 +44,7 @@ test('Sends a pageload span to Sentry', async ({ page }) => { console.log(`Polling for pageload spanId: ${spanId} in trace: ${traceId}`); - await expect - .poll(() => findSegmentSpanInTrace(traceId, spanId), EVENT_POLLING_OPTIONS) - .toMatchObject({ - op: 'pageload', - }); + await expect.poll(() => findSpanInTrace(traceId, 'pageload'), EVENT_POLLING_OPTIONS).toBeDefined(); }); test('Sends a navigation span to Sentry', async ({ page }) => { @@ -73,11 +69,7 @@ test('Sends a navigation span to Sentry', async ({ page }) => { console.log(`Polling for navigation spanId: ${spanId} in trace: ${traceId}`); - await expect - .poll(() => findSegmentSpanInTrace(traceId, spanId), EVENT_POLLING_OPTIONS) - .toMatchObject({ - op: 'navigation', - }); + await expect.poll(() => findSpanInTrace(traceId, 'navigation'), EVENT_POLLING_OPTIONS).toBeDefined(); }); test('Sends a Replay recording to Sentry', async ({ browser }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts index d451b0261992..943f9a39eb5a 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/utils/sentry-api.ts @@ -65,10 +65,30 @@ export async function findErrorInTrace(traceId: string, eventId: string): Promis return flattenTrace(await fetchTrace(traceId)).find(item => item.event_type === 'error' && item.event_id === eventId); } +let loggedTraceShape = false; + /** - * Streamed spans never become transaction events, so a segment span is looked up by its span id - * rather than by the event id of an enclosing transaction. + * Streamed spans never become transaction events, so the segment is matched by its op rather than by + * the event id of an enclosing transaction. The trace is already unique to the pageload or + * navigation under test, so the op identifies the segment within it. */ -export async function findSegmentSpanInTrace(traceId: string, spanId: string): Promise { - return flattenTrace(await fetchTrace(traceId)).find(item => item.event_type === 'span' && item.event_id === spanId); +export async function findSpanInTrace(traceId: string, op: string): Promise { + const items = flattenTrace(await fetchTrace(traceId)); + const match = items.find(item => item.op === op); + + // The trace endpoint's exact span shape is what this lookup depends on, so report it once when a + // non-empty trace does not contain the op we are waiting for. + if (!match && items.length && !loggedTraceShape) { + loggedTraceShape = true; + console.log( + `Trace ${traceId} has no "${op}" item yet. Items so far:`, + JSON.stringify( + items.map(item => ({ event_type: item.event_type, op: item.op, event_id: item.event_id })), + null, + 2, + ), + ); + } + + return match; }