@@ -12,19 +12,14 @@ import type { IntegrationFn, Span } from '@sentry/core';
12
12
import { generateInstrumentOnce } from '../../otel/instrument' ;
13
13
import { ensureIsWrapped } from '../../utils/ensureIsWrapped' ;
14
14
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
-
23
15
/**
24
16
* Minimal request type containing properties around route information.
25
17
* Works for Fastify 3, 4 and presumably 5.
18
+ *
19
+ * Based on https://github.com/fastify/fastify/blob/ce3811f5f718be278bbcd4392c615d64230065a6/types/request.d.ts
26
20
*/
27
- interface FastifyRequestRouteInfo {
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ interface MinimalFastifyRequest extends Record < string , any > {
28
23
method ?: string ;
29
24
30
25
routeOptions ?: {
@@ -33,6 +28,66 @@ interface FastifyRequestRouteInfo {
33
28
routerPath ?: string ;
34
29
}
35
30
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
+
36
91
const INTEGRATION_NAME = 'Fastify' ;
37
92
38
93
export const instrumentFastify = generateInstrumentOnce (
@@ -73,10 +128,22 @@ const _fastifyIntegration = (() => {
73
128
*/
74
129
export const fastifyIntegration = defineIntegration ( _fastifyIntegration ) ;
75
130
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
+
76
142
/**
77
143
* Add an Fastify error handler to capture errors to Sentry.
78
144
*
79
145
* @param fastify The Fastify instance to which to add the error handler
146
+ * @param options Configuration options for the handler
80
147
*
81
148
* @example
82
149
* ```javascript
@@ -92,23 +159,25 @@ export const fastifyIntegration = defineIntegration(_fastifyIntegration);
92
159
* app.listen({ port: 3000 });
93
160
* ```
94
161
*/
95
- export function setupFastifyErrorHandler ( fastify : Fastify ) : void {
162
+ export function setupFastifyErrorHandler ( fastify : Fastify , options ?: Partial < FastifyHandlerOptions > ) : void {
163
+ const shouldHandleError = options ?. shouldHandleError || defaultShouldHandleError ;
164
+
96
165
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
+ }
100
171
} ) ;
101
172
102
173
// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
103
174
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
104
175
// https://fastify.dev/docs/latest/Reference/Lifecycle/
105
176
fastify . addHook ( 'onRequest' , async ( request , _reply ) => {
106
- const reqWithRouteInfo = request as FastifyRequestRouteInfo ;
107
-
108
177
// Taken from Otel Fastify instrumentation:
109
178
// 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' ;
112
181
113
182
getIsolationScope ( ) . setTransactionName ( `${ method } ${ routeName } ` ) ;
114
183
} ) ;
0 commit comments