|
| 1 | +import { captureException, getCurrentHub, runWithAsyncContext } from '@sentry/core'; |
| 2 | +import type { Integration } from '@sentry/types'; |
| 3 | +import { logger } from '@sentry/utils'; |
| 4 | +import type * as http from 'http'; |
| 5 | + |
| 6 | +import type { NodeClient } from '../client'; |
| 7 | +import { isAutoSessionTrackingEnabled } from '../sdk'; |
| 8 | + |
| 9 | +// We do not want to have fastify as a dependency, so we mock the type here |
| 10 | + |
| 11 | +type FastifyPlugin = (fastify: FastifyInstance, _options: unknown, pluginDone: () => void) => void; |
| 12 | + |
| 13 | +type RequestHookHandler = ( |
| 14 | + this: FastifyInstance, |
| 15 | + request: http.IncomingMessage, |
| 16 | + reply: unknown, |
| 17 | + done: () => void, |
| 18 | +) => void; |
| 19 | +interface FastifyInstance { |
| 20 | + register: (plugin: FastifyPlugin) => void; |
| 21 | + |
| 22 | + /** |
| 23 | + * `onRequest` is the first hook to be executed in the request lifecycle. There was no previous hook, the next hook will be `preParsing`. |
| 24 | + * Notice: in the `onRequest` hook, request.body will always be null, because the body parsing happens before the `preHandler` hook. |
| 25 | + */ |
| 26 | + /** |
| 27 | + * `onResponse` is the seventh and last hook in the request hook lifecycle. The previous hook was `onSend`, there is no next hook. |
| 28 | + * The onResponse hook is executed when a response has been sent, so you will not be able to send more data to the client. It can however be useful for sending data to external services, for example to gather statistics. |
| 29 | + */ |
| 30 | + addHook(name: 'onRequest' | 'onResponse', hook: RequestHookHandler): void; |
| 31 | + |
| 32 | + /** |
| 33 | + * This hook is useful if you need to do some custom error logging or add some specific header in case of error. |
| 34 | + * It is not intended for changing the error, and calling reply.send will throw an exception. |
| 35 | + * This hook will be executed only after the customErrorHandler has been executed, and only if the customErrorHandler sends an error back to the user (Note that the default customErrorHandler always sends the error back to the user). |
| 36 | + * Notice: unlike the other hooks, pass an error to the done function is not supported. |
| 37 | + */ |
| 38 | + addHook( |
| 39 | + name: 'onError', |
| 40 | + hook: ( |
| 41 | + this: FastifyInstance, |
| 42 | + request: http.IncomingMessage, |
| 43 | + reply: unknown, |
| 44 | + error: Error, |
| 45 | + done: () => void, |
| 46 | + ) => void, |
| 47 | + ): void; |
| 48 | +} |
| 49 | + |
| 50 | +const SKIP_OVERRIDE = Symbol.for('skip-override'); |
| 51 | +const FASTIFY_DISPLAY_NAME = Symbol.for('fastify.display-name'); |
| 52 | + |
| 53 | +interface FastifyOptions { |
| 54 | + fastify: FastifyInstance; |
| 55 | +} |
| 56 | + |
| 57 | +const fastifyRequestPlugin = (): FastifyPlugin => |
| 58 | + Object.assign( |
| 59 | + (fastify: FastifyInstance, _options: unknown, pluginDone: () => void) => { |
| 60 | + fastify.addHook('onRequest', (request, _reply, done) => { |
| 61 | + runWithAsyncContext(() => { |
| 62 | + const currentHub = getCurrentHub(); |
| 63 | + currentHub.configureScope(scope => { |
| 64 | + scope.setSDKProcessingMetadata({ |
| 65 | + request, |
| 66 | + }); |
| 67 | + |
| 68 | + const client = currentHub.getClient<NodeClient>(); |
| 69 | + if (isAutoSessionTrackingEnabled(client)) { |
| 70 | + const scope = currentHub.getScope(); |
| 71 | + // Set `status` of `RequestSession` to Ok, at the beginning of the request |
| 72 | + scope.setRequestSession({ status: 'ok' }); |
| 73 | + } |
| 74 | + }); |
| 75 | + |
| 76 | + done(); |
| 77 | + }); |
| 78 | + }); |
| 79 | + |
| 80 | + fastify.addHook('onResponse', (_request, _reply, done) => { |
| 81 | + const client = getCurrentHub().getClient<NodeClient>(); |
| 82 | + if (isAutoSessionTrackingEnabled(client)) { |
| 83 | + setImmediate(() => { |
| 84 | + if (client && client['_captureRequestSession']) { |
| 85 | + // Calling _captureRequestSession to capture request session at the end of the request by incrementing |
| 86 | + // the correct SessionAggregates bucket i.e. crashed, errored or exited |
| 87 | + client['_captureRequestSession'](); |
| 88 | + } |
| 89 | + }); |
| 90 | + } |
| 91 | + |
| 92 | + done(); |
| 93 | + }); |
| 94 | + |
| 95 | + pluginDone(); |
| 96 | + }, |
| 97 | + { |
| 98 | + [SKIP_OVERRIDE]: true, |
| 99 | + [FASTIFY_DISPLAY_NAME]: 'SentryFastifyRequestPlugin', |
| 100 | + }, |
| 101 | + ); |
| 102 | + |
| 103 | +export const fastifyErrorPlugin = (): FastifyPlugin => |
| 104 | + Object.assign( |
| 105 | + (fastify: FastifyInstance, _options: unknown, pluginDone: () => void) => { |
| 106 | + fastify.addHook('onError', (_request, _reply, error, done) => { |
| 107 | + captureException(error); |
| 108 | + done(); |
| 109 | + }); |
| 110 | + |
| 111 | + pluginDone(); |
| 112 | + }, |
| 113 | + { |
| 114 | + [SKIP_OVERRIDE]: true, |
| 115 | + [FASTIFY_DISPLAY_NAME]: 'SentryFastifyErrorPlugin', |
| 116 | + }, |
| 117 | + ); |
| 118 | + |
| 119 | +/** Capture errors for your fastify app. */ |
| 120 | +export class Fastify implements Integration { |
| 121 | + public static id: string = 'Fastify'; |
| 122 | + public name: string = Fastify.id; |
| 123 | + |
| 124 | + private _fastify?: FastifyInstance; |
| 125 | + |
| 126 | + public constructor(options?: FastifyOptions) { |
| 127 | + const fastify = options?.fastify; |
| 128 | + this._fastify = fastify && typeof fastify.register === 'function' ? fastify : undefined; |
| 129 | + |
| 130 | + if (__DEBUG_BUILD__ && !this._fastify) { |
| 131 | + logger.warn('The Fastify integration expects a fastify instance to be passed. No errors will be captured.'); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + /** |
| 136 | + * @inheritDoc |
| 137 | + */ |
| 138 | + public setupOnce(): void { |
| 139 | + if (!this._fastify) { |
| 140 | + return; |
| 141 | + } |
| 142 | + |
| 143 | + void this._fastify.register(fastifyErrorPlugin()); |
| 144 | + void this._fastify.register(fastifyRequestPlugin()); |
| 145 | + } |
| 146 | +} |
0 commit comments