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..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 @@ -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({ + 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; + } + + 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..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 @@ -28,3 +28,18 @@ 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'; + }); + + errorEventPromise.then(() => { + test.fail(); + }); + + await fetch(`${baseURL}/test-error-not-captured`); + + // wait for a short time to ensure the error is not captured + await new Promise(resolve => setTimeout(resolve, 1000)); +}); diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index b514cb80d32e..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 @@ -27,6 +62,7 @@ interface FastifyHandlerOptions { * * @example * + * * ```javascript * setupFastifyErrorHandler(app, { * shouldHandleError(_error, _request, reply) { @@ -35,6 +71,7 @@ interface FastifyHandlerOptions { * }); * ``` * + * * If using TypeScript, you can cast the request and reply to get full type safety. * * ```typescript @@ -88,51 +125,61 @@ 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 = {}) => { + 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?.shouldHandleDiagnosticsChannelError || defaultShouldHandleError, + '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 = (({ shouldHandleDiagnosticsChannelError }: Partial) => { return { name: INTEGRATION_NAME, setupOnce() { instrumentFastifyV3(); - instrumentFastify(); + instrumentFastify({ + shouldHandleDiagnosticsChannelError, + }); }, }; }) satisfies IntegrationFn; @@ -153,7 +200,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