From f5ad9141b17a60ad6f947d75e37b8193c1628ec2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 09:05:25 +0200 Subject: [PATCH 1/2] test(e2e): Assert error events link to their request transaction For the Node express/fastify/koa "Sends correct error event" tests, also fetch the request's transaction and assert the error shares its trace_id and is anchored to a span belonging to that transaction (its root span or a child), instead of only matching the trace context against loose regexes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../node-express-streaming/tests/errors.test.ts | 15 ++++++++++++++- .../node-express-v5/tests/errors.test.ts | 15 ++++++++++++++- .../node-express/tests/errors.test.ts | 15 ++++++++++++++- .../node-fastify-3/tests/errors.test.ts | 15 ++++++++++++++- .../node-fastify-4/tests/errors.test.ts | 15 ++++++++++++++- .../node-fastify-5/tests/errors.test.ts | 13 +++++++++++++ .../node-koa/tests/errors.test.ts | 15 ++++++++++++++- .../tsx-express/tests/errors.test.ts | 15 ++++++++++++++- 8 files changed, 111 insertions(+), 7 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts index 628a48c56456..6976e8db832f 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-express-streaming', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-express-streaming', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); const exception = errorEvent.exception?.values?.[0]; @@ -31,6 +36,14 @@ test('Sends correct error event', async ({ baseURL }) => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Should record caught exceptions with local variable', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts index 56b4f51d228d..a7bc9b497c63 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-express-v5', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-express-v5', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); @@ -26,6 +31,14 @@ test('Sends correct error event', async ({ baseURL }) => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Should record caught exceptions with local variable', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts index 3a3c821a927d..a791bbc29189 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-express', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-express', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); const exception = errorEvent.exception?.values?.[0]; @@ -31,6 +36,14 @@ test('Sends correct error event', async ({ baseURL }) => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Should record caught exceptions with local variable', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts index c0be1b0292a3..6a548391eea5 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-fastify-3', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-fastify-3', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); @@ -27,6 +32,14 @@ test('Sends correct error event', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts index 46453e4749e0..4e905a12098e 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-fastify-4', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-fastify-4', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); @@ -27,6 +32,14 @@ test('Sends correct error event', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Does not send 4xx errors by default', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts index 8699b26d490b..a50d9f3020b5 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts @@ -6,9 +6,14 @@ test('Sends correct error event', async ({ baseURL }) => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-fastify-5', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); const exception = errorEvent.exception?.values?.[0]; @@ -32,6 +37,14 @@ test('Sends correct error event', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts index dd0dfa4a084d..821e939ff8ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-koa', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('node-koa', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); const exception = errorEvent.exception?.values?.[0]; @@ -33,4 +38,12 @@ test('Sends correct error event', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts index 5d596b9b8226..1c59e2173092 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts @@ -1,14 +1,19 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('tsx-express', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); + const transactionEventPromise = waitForTransaction('tsx-express', event => { + return event.transaction === 'GET /test-exception/:id'; + }); + await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; + const transactionEvent = await transactionEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); @@ -26,4 +31,12 @@ test('Sends correct error event', async ({ baseURL }) => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); + + // The error is attached to the same trace as the request transaction, and to a + // span that belongs to that transaction (its root span or one of its children). + const transactionTrace = transactionEvent.contexts?.trace; + expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + + const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; + expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); }); From cafa2e36a86e76adda1ba0b5838c9b6901a90001 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 11:30:48 +0200 Subject: [PATCH 2/2] fix test --- .../tests/errors.test.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts index 6976e8db832f..21f0c4e2c30f 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts @@ -1,19 +1,21 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, waitForError } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-express-streaming', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); - const transactionEventPromise = waitForTransaction('node-express-streaming', event => { - return event.transaction === 'GET /test-exception/:id'; - }); + // In streaming mode there is no transaction event; the request's spans are streamed individually. + // The root segment span flushes last, so collecting until it arrives captures the whole trace. + const spansPromise = collectStreamedSpans('node-express-streaming', spans => + spans.some(span => span.name === 'GET /test-exception/:id' && span.is_segment), + ); await fetch(`${baseURL}/test-exception/123`); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const spans = await spansPromise; expect(errorEvent.exception?.values).toHaveLength(1); const exception = errorEvent.exception?.values?.[0]; @@ -37,13 +39,13 @@ test('Sends correct error event', async ({ baseURL }) => { span_id: expect.stringMatching(/[a-f0-9]{16}/), }); - // The error is attached to the same trace as the request transaction, and to a - // span that belongs to that transaction (its root span or one of its children). - const transactionTrace = transactionEvent.contexts?.trace; - expect(errorEvent.contexts?.trace?.trace_id).toBe(transactionTrace?.trace_id); + // The error is attached to the same trace as the streamed request spans, and to a + // span that belongs to that trace (its root segment span or one of its children). + const rootSpan = spans.find(span => span.name === 'GET /test-exception/:id' && span.is_segment); + expect(errorEvent.contexts?.trace?.trace_id).toBe(rootSpan?.trace_id); - const transactionSpanIds = [transactionTrace?.span_id, ...(transactionEvent.spans ?? []).map(span => span.span_id)]; - expect(transactionSpanIds).toContain(errorEvent.contexts?.trace?.span_id); + const spanIds = spans.map(span => span.span_id); + expect(spanIds).toContain(errorEvent.contexts?.trace?.span_id); }); test('Should record caught exceptions with local variable', async ({ baseURL }) => {