Skip to content

feat(node): Add shouldHandleDiagnosticsChannelError option to fastifyIntegration #16845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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}`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
123 changes: 86 additions & 37 deletions packages/node/src/integrations/tracing/fastify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +62,7 @@ interface FastifyHandlerOptions {
*
* @example
*
*
* ```javascript
* setupFastifyErrorHandler(app, {
* shouldHandleError(_error, _request, reply) {
Expand All @@ -35,6 +71,7 @@ interface FastifyHandlerOptions {
* });
* ```
*
*
* If using TypeScript, you can cast the request and reply to get full type safety.
*
* ```typescript
Expand Down Expand Up @@ -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<FastifyIntegrationOptions> = {}) => {
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<InstrumentationConfig & FastifyHandlerOptions>;
});
// Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation
return fastifyOtelInstrumentationInstance as Instrumentation<InstrumentationConfig & FastifyIntegrationOptions>;
},
);

const _fastifyIntegration = (() => {
const _fastifyIntegration = (({ shouldHandleDiagnosticsChannelError }: Partial<FastifyIntegrationOptions>) => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentFastifyV3();
instrumentFastify();
instrumentFastify({
shouldHandleDiagnosticsChannelError,
});
},
};
}) satisfies IntegrationFn;
Expand All @@ -153,7 +200,9 @@ const _fastifyIntegration = (() => {
* })
* ```
*/
export const fastifyIntegration = defineIntegration(_fastifyIntegration);
export const fastifyIntegration = defineIntegration((options: Partial<FastifyIntegrationOptions> = {}) =>
_fastifyIntegration(options),
);

/**
* Default function to determine if an error should be sent to Sentry
Expand Down