@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
1212import { generateInstrumentOnce } from '../../otel/instrument' ;
1313import { ensureIsWrapped } from '../../utils/ensureIsWrapped' ;
1414
15- // We inline the types we care about here
16- interface Fastify {
17- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18- register : ( plugin : any ) => void ;
19- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20- addHook : ( hook : string , handler : ( request : any , reply : any , error : Error ) => void ) => void ;
21- }
22-
2315/**
2416 * Minimal request type containing properties around route information.
2517 * Works for Fastify 3, 4 and presumably 5.
18+ *
19+ * Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
2620 */
27- interface FastifyRequestRouteInfo {
21+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22+ interface MinimalFastifyRequest extends Record < string , any > {
2823 method ?: string ;
29243025 routeOptions ?: {
@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
3328 routerPath ?: string ;
3429}
3530
31+ /**
32+ * Minimal reply type containing properties needed for error handling.
33+ *
34+ * Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/reply.d.ts
35+ */
36+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37+ interface MinimalFastifyReply extends Record < string , any > {
38+ statusCode : number ;
39+ }
40+
41+ // We inline the types we care about here
42+ interface Fastify {
43+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44+ register : ( plugin : any ) => void ;
45+ addHook : ( hook : string , handler : ( ...params : unknown [ ] ) => void ) => void ;
46+ }
47+
48+ interface FastifyWithHooks extends Omit < Fastify , 'addHook' > {
49+ addHook (
50+ hook : 'onError' ,
51+ handler : ( request : MinimalFastifyRequest , reply : MinimalFastifyReply , error : Error ) => void ,
52+ ) : void ;
53+ addHook ( hook : 'onRequest' , handler : ( request : MinimalFastifyRequest , reply : MinimalFastifyReply ) => void ) : void ;
54+ }
55+
56+ interface FastifyHandlerOptions {
57+ /**
58+ * Callback method deciding whether error should be captured and sent to Sentry
59+ *
60+ * @param error Captured Fastify error
61+ * @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath)
62+ * @param reply Fastify reply (or any object containing at least statusCode)
63+ *
64+ * @example
65+ *
66+ * ```javascript
67+ * setupFastifyErrorHandler(app, {
68+ * shouldHandleError(_error, _request, reply) {
69+ * return reply.statusCode >= 400;
70+ * },
71+ * });
72+ * ```
73+ *
74+ * If using TypeScript, you can cast the request and reply to get full type safety.
75+ *
76+ * ```typescript
77+ * import type { FastifyRequest, FastifyReply } from 'fastify';
78+ *
79+ * setupFastifyErrorHandler(app, {
80+ * shouldHandleError(error, minimalRequest, minimalReply) {
81+ * const request = minimalRequest as FastifyRequest;
82+ * const reply = minimalReply as FastifyReply;
83+ * return reply.statusCode >= 500;
84+ * },
85+ * });
86+ * ```
87+ */
88+ shouldHandleError : ( error : Error , request : MinimalFastifyRequest , reply : MinimalFastifyReply ) => boolean ;
89+ }
90+
3691const INTEGRATION_NAME = 'Fastify' ;
3792
3893export const instrumentFastify = generateInstrumentOnce (
@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
73128 */
74129export const fastifyIntegration = defineIntegration ( _fastifyIntegration ) ;
75130
131+ /**
132+ * Default function to determine if an error should be sent to Sentry
133+ *
134+ * 3xx and 4xx errors are not sent by default.
135+ */
136+ function defaultShouldHandleError ( _error : Error , _request : MinimalFastifyRequest , reply : MinimalFastifyReply ) : boolean {
137+ const statusCode = reply . statusCode ;
138+ // 3xx and 4xx errors are not sent by default.
139+ return statusCode >= 500 || statusCode <= 299 ;
140+ }
141+
76142/**
77143 * Add an Fastify error handler to capture errors to Sentry.
78144 *
79145 * @param fastify The Fastify instance to which to add the error handler
146+ * @param options Configuration options for the handler
80147 *
81148 * @example
82149 * ```javascript
@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
92159 * app.listen({ port: 3000 });
93160 * ```
94161 */
95- export function setupFastifyErrorHandler ( fastify : Fastify ) : void {
162+ export function setupFastifyErrorHandler ( fastify : Fastify , options ?: Partial < FastifyHandlerOptions > ) : void {
163+ const shouldHandleError = options ?. shouldHandleError || defaultShouldHandleError ;
164+
96165 const plugin = Object . assign (
97- function ( fastify : Fastify , _options : unknown , done : ( ) => void ) : void {
98- fastify . addHook ( 'onError' , async ( _request , _reply , error ) => {
99- captureException ( error ) ;
166+ function ( fastify : FastifyWithHooks , _options : unknown , done : ( ) => void ) : void {
167+ fastify . addHook ( 'onError' , async ( request , reply , error ) => {
168+ if ( shouldHandleError ( error , request , reply ) ) {
169+ captureException ( error ) ;
170+ }
100171 } ) ;
101172
102173 // registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
103174 // is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
104175 // https://fastify.dev/docs/latest/Reference/Lifecycle/
105176 fastify . addHook ( 'onRequest' , async ( request , _reply ) => {
106- const reqWithRouteInfo = request as FastifyRequestRouteInfo ;
107-
108177 // Taken from Otel Fastify instrumentation:
109178 // https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
110- const routeName = reqWithRouteInfo . routeOptions ?. url || reqWithRouteInfo . routerPath ;
111- const method = reqWithRouteInfo . method || 'GET' ;
179+ const routeName = request . routeOptions ?. url || request . routerPath ;
180+ const method = request . method || 'GET' ;
112181
113182 getIsolationScope ( ) . setTransactionName ( `${ method } ${ routeName } ` ) ;
114183 } ) ;
0 commit comments