From 357be37634f0510e3b77d72e17db45591a944909 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 8 Jul 2025 16:09:18 +0100 Subject: [PATCH 1/4] feat(node): Add `shouldHandleError` option to `fastifyIntegration` --- .../node-fastify-5/src/app.ts | 18 ++- .../node-fastify-5/tests/errors.test.ts | 13 +++ .../src/integrations/tracing/fastify/index.ts | 105 ++++++++++++------ 3 files changed, 98 insertions(+), 38 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts index db2e9bf9cc5f..83f7e53a45ce 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts @@ -17,7 +17,18 @@ console.warn = new Proxy(console.warn, { Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, - integrations: [], + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, + }), + ], tracesSampleRate: 1, tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], @@ -79,6 +90,11 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { throw new Error(`This is an exception with id ${req.params.id}`); }); 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 f79eb30e9b4c..6b4f31564ed2 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 @@ -28,3 +28,16 @@ test('Sends correct error event', async ({ baseURL }) => { parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); + +test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-5', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured'; + }); + + await fetch(`${baseURL}/test-error-not-captured`); + + // Wait for a short time to ensure no error is sent + await new Promise(resolve => setTimeout(resolve, 1000)); + + expect(errorEventPromise).rejects.toBeDefined(); +}); diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index b514cb80d32e..4304612227c6 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -27,6 +27,7 @@ interface FastifyHandlerOptions { * * @example * + * * ```javascript * setupFastifyErrorHandler(app, { * shouldHandleError(_error, _request, reply) { @@ -35,6 +36,20 @@ interface FastifyHandlerOptions { * }); * ``` * + * or if you use Fastify v5 you can set options in the Sentry.init call: + * + * ```javascript + * Sentry.init({ + * integrations: [ + * Sentry.fastifyIntegration({ + * shouldHandleError(_error, _request, reply) { + * return reply.statusCode >= 500; + * }, + * }); + * }, + * }); + * ``` + * * If using TypeScript, you can cast the request and reply to get full type safety. * * ```typescript @@ -88,51 +103,65 @@ function handleFastifyError( } } -export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => { - const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); - const plugin = fastifyOtelInstrumentationInstance.plugin(); - const options = fastifyOtelInstrumentationInstance.getConfig(); - const shouldHandleError = (options as FastifyHandlerOptions)?.shouldHandleError || defaultShouldHandleError; - - // This message handler works for Fastify versions 3, 4 and 5 - diagnosticsChannel.subscribe('fastify.initialization', message => { - const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; - - fastifyInstance?.register(plugin).after(err => { - if (err) { - DEBUG_BUILD && logger.error('Failed to setup Fastify instrumentation', err); - } else { - instrumentClient(); - - if (fastifyInstance) { - instrumentOnRequest(fastifyInstance); +export const instrumentFastify = generateInstrumentOnce( + INTEGRATION_NAME, + ( + options: Partial = { + shouldHandleError: defaultShouldHandleError, + }, + ) => { + const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); + const plugin = fastifyOtelInstrumentationInstance.plugin(); + + // This message handler works for Fastify versions 3, 4 and 5 + diagnosticsChannel.subscribe('fastify.initialization', message => { + const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; + + fastifyInstance?.register(plugin).after(err => { + if (err) { + DEBUG_BUILD && logger.error('Failed to setup Fastify instrumentation', err); + } else { + instrumentClient(); + + if (fastifyInstance) { + instrumentOnRequest(fastifyInstance); + } } - } + }); }); - }); - - // This diagnostics channel only works on Fastify version 5 - // For versions 3 and 4, we use `setupFastifyErrorHandler` instead - diagnosticsChannel.subscribe('tracing:fastify.request.handler:error', message => { - const { error, request, reply } = message as { - error: Error; - request: FastifyRequest & { opentelemetry?: () => { span?: Span } }; - reply: FastifyReply; - }; - handleFastifyError.call(handleFastifyError, error, request, reply, shouldHandleError, 'diagnostics-channel'); - }); + // This diagnostics channel only works on Fastify version 5 + // For versions 3 and 4, we use `setupFastifyErrorHandler` instead + diagnosticsChannel.subscribe('tracing:fastify.request.handler:error', message => { + const { error, request, reply } = message as { + error: Error; + request: FastifyRequest & { opentelemetry?: () => { span?: Span } }; + reply: FastifyReply; + }; + + handleFastifyError.call( + handleFastifyError, + error, + request, + reply, + options.shouldHandleError, + 'diagnostics-channel', + ); + }); - // Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation - return fastifyOtelInstrumentationInstance as Instrumentation; -}); + // Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation + return fastifyOtelInstrumentationInstance as Instrumentation; + }, +); -const _fastifyIntegration = (() => { +const _fastifyIntegration = (({ shouldHandleError }) => { return { name: INTEGRATION_NAME, setupOnce() { instrumentFastifyV3(); - instrumentFastify(); + instrumentFastify({ + shouldHandleError, + }); }, }; }) satisfies IntegrationFn; @@ -153,7 +182,9 @@ const _fastifyIntegration = (() => { * }) * ``` */ -export const fastifyIntegration = defineIntegration(_fastifyIntegration); +export const fastifyIntegration = defineIntegration((options: Partial = {}) => + _fastifyIntegration(options), +); /** * Default function to determine if an error should be sent to Sentry From 1bd2c6a59fb2b37ed5fd7b827bee17c4212c826e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 8 Jul 2025 16:43:56 +0100 Subject: [PATCH 2/4] Apply cursor's suggestions --- .../test-applications/node-fastify-5/tests/errors.test.ts | 2 +- packages/node/src/integrations/tracing/fastify/index.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) 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 6b4f31564ed2..0f422eff3c52 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 @@ -39,5 +39,5 @@ test('Does not send error when shouldHandleError returns false', async ({ baseUR // Wait for a short time to ensure no error is sent await new Promise(resolve => setTimeout(resolve, 1000)); - expect(errorEventPromise).rejects.toBeDefined(); + await expect(errorEventPromise).rejects.toBeDefined(); }); diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index 4304612227c6..84c299b2cafa 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -105,11 +105,7 @@ function handleFastifyError( export const instrumentFastify = generateInstrumentOnce( INTEGRATION_NAME, - ( - options: Partial = { - shouldHandleError: defaultShouldHandleError, - }, - ) => { + (options: Partial = {}) => { const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); const plugin = fastifyOtelInstrumentationInstance.plugin(); @@ -144,7 +140,7 @@ export const instrumentFastify = generateInstrumentOnce( error, request, reply, - options.shouldHandleError, + options?.shouldHandleError || defaultShouldHandleError, 'diagnostics-channel', ); }); From f513f8a521969f2d51888af84ca4ddc5f004f46c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 8 Jul 2025 17:10:37 +0100 Subject: [PATCH 3/4] Update the test case --- .../test-applications/node-fastify-5/tests/errors.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 0f422eff3c52..2eb7d47ea934 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 @@ -34,10 +34,12 @@ test('Does not send error when shouldHandleError returns false', async ({ baseUR return !event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured'; }); + errorEventPromise.then(() => { + test.fail(); + }); + await fetch(`${baseURL}/test-error-not-captured`); - // Wait for a short time to ensure no error is sent + // wait for a short time to ensure the error is not captured await new Promise(resolve => setTimeout(resolve, 1000)); - - await expect(errorEventPromise).rejects.toBeDefined(); }); From fc2159eb68b9be1f160a68daba51fc7a692f6163 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 8 Jul 2025 21:39:03 +0100 Subject: [PATCH 4/4] Rename option to `shouldHandleDiagnosticsChannelError` --- .../node-fastify-5/src/app.ts | 2 +- .../src/integrations/tracing/fastify/index.ts | 60 +++++++++++++------ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts index 83f7e53a45ce..0d78be69a8d3 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts @@ -19,7 +19,7 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, integrations: [ Sentry.fastifyIntegration({ - shouldHandleError: (error, _request, _reply) => { + shouldHandleDiagnosticsChannelError: (error, _request, _reply) => { if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { // Errors from this path will not be captured by Sentry return false; diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index 84c299b2cafa..c8b7475336f1 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -17,6 +17,41 @@ import { FastifyOtelInstrumentation } from './fastify-otel/index'; import type { FastifyInstance, FastifyReply, FastifyRequest } from './types'; import { FastifyInstrumentationV3 } from './v3/instrumentation'; +/** + * Options for the Fastify integration. + * + * `shouldHandleDiagnosticsChannelError` - Callback method deciding whether error should be captured and sent to Sentry + * This is used on Fastify v5 where Sentry handles errors in the diagnostics channel. + * Fastify v3 and v4 use `setupFastifyErrorHandler` instead. + * + * @example + * + * ```javascript + * Sentry.init({ + * integrations: [ + * Sentry.fastifyIntegration({ + * shouldHandleDiagnosticsChannelError(_error, _request, reply) { + * return reply.statusCode >= 500; + * }, + * }); + * }, + * }); + * ``` + * + */ +interface FastifyIntegrationOptions { + /** + * Callback method deciding whether error should be captured and sent to Sentry + * This is used on Fastify v5 where Sentry handles errors in the diagnostics channel. + * Fastify v3 and v4 use `setupFastifyErrorHandler` instead. + * + * @param error Captured Fastify error + * @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath) + * @param reply Fastify reply (or any object containing at least statusCode) + */ + shouldHandleDiagnosticsChannelError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; +} + interface FastifyHandlerOptions { /** * Callback method deciding whether error should be captured and sent to Sentry @@ -36,19 +71,6 @@ interface FastifyHandlerOptions { * }); * ``` * - * or if you use Fastify v5 you can set options in the Sentry.init call: - * - * ```javascript - * Sentry.init({ - * integrations: [ - * Sentry.fastifyIntegration({ - * shouldHandleError(_error, _request, reply) { - * return reply.statusCode >= 500; - * }, - * }); - * }, - * }); - * ``` * * If using TypeScript, you can cast the request and reply to get full type safety. * @@ -105,7 +127,7 @@ function handleFastifyError( export const instrumentFastify = generateInstrumentOnce( INTEGRATION_NAME, - (options: Partial = {}) => { + (options: Partial = {}) => { const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); const plugin = fastifyOtelInstrumentationInstance.plugin(); @@ -140,23 +162,23 @@ export const instrumentFastify = generateInstrumentOnce( error, request, reply, - options?.shouldHandleError || defaultShouldHandleError, + options?.shouldHandleDiagnosticsChannelError || defaultShouldHandleError, 'diagnostics-channel', ); }); // Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation - return fastifyOtelInstrumentationInstance as Instrumentation; + return fastifyOtelInstrumentationInstance as Instrumentation; }, ); -const _fastifyIntegration = (({ shouldHandleError }) => { +const _fastifyIntegration = (({ shouldHandleDiagnosticsChannelError }: Partial) => { return { name: INTEGRATION_NAME, setupOnce() { instrumentFastifyV3(); instrumentFastify({ - shouldHandleError, + shouldHandleDiagnosticsChannelError, }); }, }; @@ -178,7 +200,7 @@ const _fastifyIntegration = (({ shouldHandleError }) => { * }) * ``` */ -export const fastifyIntegration = defineIntegration((options: Partial = {}) => +export const fastifyIntegration = defineIntegration((options: Partial = {}) => _fastifyIntegration(options), );