From 3ec48173a2fa67ad2f3bf526cf2d302c598cb091 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 21 Oct 2026 13:06:52 +0200 Subject: [PATCH 1/2] test(react-router): Match error and segment span by trace in instrumentation API e2e test Streamed spans flush after errors, so the segment span listener could pick up a late span from an earlier test that hit the same route, making the trace id assertion flaky (mostly in dev mode). Co-Authored-By: Claude Opus 5.5 --- .../tests/errors/errors.server.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts index 10cf2e5be8e0..f3a52a0b8e46 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; import { collectStreamedSpansUntilSegment, getSpanOp, @@ -72,18 +73,25 @@ test.describe('server - instrumentation API error capture', () => { return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing'; }); - const spanPromise = waitForStreamedSpan(APP_NAME, span => { - return span.name === 'GET /performance/error-loader' && span.is_segment; + // Streamed spans flush later than errors, so a segment span from an earlier test hitting the same + // route can still be arriving here. Collect them and look for the one in the error's trace. + const segmentSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpan(APP_NAME, span => { + if (span.name === 'GET /performance/error-loader' && span.is_segment) { + segmentSpans.push(span); + } + return false; }); await page.goto(`/performance/error-loader`).catch(() => { // Expected to fail due to loader error }); - const [error, span] = await Promise.all([errorPromise, spanPromise]); + const error = await errorPromise; + const traceId = error.contexts?.trace?.trace_id; + expect(traceId).toMatch(/^[a-f0-9]{32}$/); - // Error and segment span should have the same trace_id - expect(error.contexts?.trace?.trace_id).toBe(span.trace_id); + await expect.poll(() => segmentSpans.some(span => span.trace_id === traceId)).toBe(true); }); // Skipped in dev: the action error is sometimes captured via the client instrumentation path From 64e2618ff6daaf4b8fe80bf84bbb6dc79c20b0c8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 28 Oct 2026 08:38:26 +0100 Subject: [PATCH 2/2] test(react-router): Give each loader error test its own URL Co-Authored-By: Claude Opus 5.5 --- .../app/routes.ts | 2 +- .../app/routes/performance/error-loader.tsx | 6 ++- .../tests/errors/errors.server.test.ts | 44 +++++++++---------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts index 9f0dd2325c42..5a9fa9c841aa 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts @@ -11,7 +11,7 @@ export default [ route('server-action', 'routes/performance/server-action.tsx'), route('with-middleware', 'routes/performance/with-middleware.tsx'), route('multi-middleware', 'routes/performance/multi-middleware.tsx'), - route('error-loader', 'routes/performance/error-loader.tsx'), + route('error-loader/:id', 'routes/performance/error-loader.tsx'), route('error-action', 'routes/performance/error-action.tsx'), route('error-middleware', 'routes/performance/error-middleware.tsx'), route('lazy-route', 'routes/performance/lazy-route.tsx'), diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/error-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/error-loader.tsx index 6dd3d3013f37..b94363688a95 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/error-loader.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/error-loader.tsx @@ -1,5 +1,7 @@ -export function loader(): never { - throw new Error('Loader error for testing'); +import type { Route } from './+types/error-loader'; + +export function loader({ params }: Route.LoaderArgs): never { + throw new Error(`Loader error for testing (${params.id})`); } export default function ErrorLoaderPage() { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts index f3a52a0b8e46..08863ed8d9c5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/errors/errors.server.test.ts @@ -8,17 +8,24 @@ import { } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; +function isErrorLoaderSegment(span: SerializedStreamedSpan, id: string): boolean { + const url = span.attributes['url.full']?.value; + return span.is_segment && typeof url === 'string' && url.endsWith(`/performance/error-loader/${id}`); +} + test.describe('server - instrumentation API error capture', () => { + // Each loader test hits its own URL: streamed spans flush later than errors, so a segment span + // (or error) from an earlier test on the same URL could otherwise be matched by a later test. test('should capture loader errors with instrumentation API mechanism', async ({ page }) => { const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing'; + return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing (mechanism)'; }); const spanPromise = waitForStreamedSpan(APP_NAME, span => { - return span.name === 'GET /performance/error-loader' && span.is_segment; + return isErrorLoaderSegment(span, 'mechanism'); }); - await page.goto(`/performance/error-loader`).catch(() => { + await page.goto(`/performance/error-loader/mechanism`).catch(() => { // Expected to fail due to loader error }); @@ -30,7 +37,7 @@ test.describe('server - instrumentation API error capture', () => { values: [ { type: 'Error', - value: 'Loader error for testing', + value: 'Loader error for testing (mechanism)', mechanism: { type: 'react_router.loader', handled: false, @@ -38,19 +45,19 @@ test.describe('server - instrumentation API error capture', () => { }, ], }, - transaction: 'GET /performance/error-loader', + transaction: 'GET /performance/error-loader/:id', }); // Verify the transaction was also created with correct attributes - expect(span.name).toBe('GET /performance/error-loader'); + expect(span.name).toBe('GET /performance/error-loader/:id'); expect(getSpanOp(span)).toBe('http.server'); expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api'); }); test('should include loader span in the segment even when loader throws', async ({ page }) => { - const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /performance/error-loader'); + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, span => isErrorLoaderSegment(span, 'loader-span')); - await page.goto(`/performance/error-loader`).catch(() => { + await page.goto(`/performance/error-loader/loader-span`).catch(() => { // Expected to fail due to loader error }); @@ -70,28 +77,21 @@ test.describe('server - instrumentation API error capture', () => { test('error and segment span should share the same trace', async ({ page }) => { const errorPromise = waitForError(APP_NAME, async errorEvent => { - return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing'; + return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing (same-trace)'; }); - // Streamed spans flush later than errors, so a segment span from an earlier test hitting the same - // route can still be arriving here. Collect them and look for the one in the error's trace. - const segmentSpans: SerializedStreamedSpan[] = []; - void waitForStreamedSpan(APP_NAME, span => { - if (span.name === 'GET /performance/error-loader' && span.is_segment) { - segmentSpans.push(span); - } - return false; + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return isErrorLoaderSegment(span, 'same-trace'); }); - await page.goto(`/performance/error-loader`).catch(() => { + await page.goto(`/performance/error-loader/same-trace`).catch(() => { // Expected to fail due to loader error }); - const error = await errorPromise; - const traceId = error.contexts?.trace?.trace_id; - expect(traceId).toMatch(/^[a-f0-9]{32}$/); + const [error, span] = await Promise.all([errorPromise, spanPromise]); - await expect.poll(() => segmentSpans.some(span => span.trace_id === traceId)).toBe(true); + expect(error.contexts?.trace?.trace_id).toMatch(/^[a-f0-9]{32}$/); + expect(error.contexts?.trace?.trace_id).toBe(span.trace_id); }); // Skipped in dev: the action error is sometimes captured via the client instrumentation path