From f344b92de2b3e40d56a0a5bae2aec3855a69660f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 1 Oct 2026 17:20:49 +0200 Subject: [PATCH 1/2] fix(nitro): Capture errors only through the Nitro error hook Co-Authored-By: OpenAI Codex --- .../nitro-3/server/api/test-error-400.ts | 5 +++ .../nitro-3/tests/errors.test.ts | 42 ++++++++++++------- .../src/runtime/hooks/captureTracingEvents.ts | 6 --- 3 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts new file mode 100644 index 000000000000..760a6ad17852 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts @@ -0,0 +1,5 @@ +import { defineHandler, HTTPError } from 'nitro/h3'; + +export default defineHandler(() => { + throw new HTTPError({ status: 400, message: 'Explicit 400 test error' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts index 1441ecd5ecab..408e2590a11a 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts @@ -2,15 +2,9 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; test('Sends an error event to Sentry', async ({ request }) => { - // The thrown error is reported twice: once via the h3 tracing channel and once via Nitro's `error` - // hook (which sees it wrapped in an `HTTPError`). Match on the mechanism so we deterministically - // await the event under test instead of whichever arrives first. const errorEventPromise = waitForError('nitro-3', event => { - return ( - !event.type && - !!event.exception?.values?.some( - v => v.value === 'This is a test error' && v.mechanism?.type === 'auto.http.nitro.onTraceError', - ) + return !!event.exception?.values?.some( + v => v.value === 'This is a test error' && v.mechanism?.type === 'auto.function.nitro.captureErrorHook', ); }); @@ -20,16 +14,32 @@ test('Sends an error event to Sentry', async ({ request }) => { const errorEvent = await errorEventPromise; - expect(errorEvent.exception?.values).toHaveLength(1); - - expect(errorEvent.exception?.values?.[0]?.type).toBe('Error'); - expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error'); - expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual( + expect(errorEvent.exception?.values).toEqual([ + expect.objectContaining({ + type: 'Error', + value: 'This is a test error', + }), expect.objectContaining({ - handled: false, - type: 'auto.http.nitro.onTraceError', + type: 'HTTPError', + value: 'This is a test error', + mechanism: expect.objectContaining({ + handled: false, + type: 'auto.function.nitro.captureErrorHook', + }), }), - ); + ]); +}); + +test('Does not send an explicitly thrown 400 error to Sentry', async ({ request }) => { + const errorEventPromise = waitForError('nitro-3', event => { + return !!event.exception?.values?.some(v => v.value === 'Explicit 400 test error'); + }); + + const response = await request.get('/api/test-error-400'); + + expect(response.status()).toBe(400); + const errorEvent = await Promise.race([errorEventPromise, new Promise(resolve => setTimeout(resolve, 3000))]); + expect(errorEvent).toBeUndefined(); }); test('Does not send 404 errors to Sentry', async ({ request }) => { diff --git a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts index bcfad9b3679b..da1f517acc7c 100644 --- a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts +++ b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts @@ -131,12 +131,6 @@ function setupH3TracingChannels(): void { return span; }, { - captureError: () => ({ - mechanism: { - handled: false, - type: 'auto.http.nitro.onTraceError', - }, - }), beforeSpanEnd(span, data) { applyResponseStatus(span, data); From 78f305eb3951d04ee048752f8e3c62a6772a88bb Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 1 Oct 2026 18:42:54 +0200 Subject: [PATCH 2/2] test(nitro): Wait for flush completion when checking ignored errors Co-Authored-By: OpenAI Codex --- .../nitro-3/server/api/flush.ts | 7 ++++++ .../nitro-3/tests/errors.test.ts | 25 ++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts new file mode 100644 index 000000000000..1bb06c6e1a6d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts @@ -0,0 +1,7 @@ +import { flush } from '@sentry/nitro'; +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(async () => { + await flush(); + return { status: 'ok' }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts index 408e2590a11a..18cbd7ca6709 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends an error event to Sentry', async ({ request }) => { const errorEventPromise = waitForError('nitro-3', event => { @@ -31,15 +31,28 @@ test('Sends an error event to Sentry', async ({ request }) => { }); test('Does not send an explicitly thrown 400 error to Sentry', async ({ request }) => { - const errorEventPromise = waitForError('nitro-3', event => { - return !!event.exception?.values?.some(v => v.value === 'Explicit 400 test error'); + let errorReceived = false; + + void waitForError('nitro-3', event => { + if (event.exception?.values?.some(v => v.value === 'Explicit 400 test error')) { + errorReceived = true; + return true; + } + return false; }); - const response = await request.get('/api/test-error-400'); + const flushSpanPromise = waitForStreamedSpan('nitro-3', span => { + return span.is_segment && span.name === 'GET /api/flush'; + }); + const response = await request.get('/api/test-error-400'); expect(response.status()).toBe(400); - const errorEvent = await Promise.race([errorEventPromise, new Promise(resolve => setTimeout(resolve, 3000))]); - expect(errorEvent).toBeUndefined(); + + const flushResponse = await request.get('/api/flush'); + expect(flushResponse.status()).toBe(200); + await flushSpanPromise; + + expect(errorReceived).toBe(false); }); test('Does not send 404 errors to Sentry', async ({ request }) => {