From 0db9c452ffdd58e1efaf8ceda20745f50ce36dd0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 09:53:39 +0200 Subject: [PATCH 1/9] ref(node)!: Remove deprecated fastify exports, deprecate `setupFastifyErrorHandler` --- .../node-fastify-3/package.json | 2 - .../src/app-handle-error-override.ts | 179 --------------- .../node-fastify-3/src/app.ts | 2 - .../node-fastify-4/package.json | 4 +- .../src/app-handle-error-override.ts | 188 ---------------- .../node-fastify-4/src/app.ts | 6 +- .../node-fastify-5/package.json | 4 +- .../src/app-handle-error-override.ts | 204 ------------------ .../node-fastify-5/src/app.ts | 7 +- .../fastify/scenario-error-handler.mjs | 34 +++ .../suites/tracing/fastify/test.ts | 188 +++++++++------- packages/bun/src/index.ts | 1 + packages/elysia/src/index.ts | 1 + packages/node/src/index.ts | 4 +- .../src/integrations/tracing/fastify/index.ts | 164 -------------- .../src/integrations/tracing/fastify/types.ts | 51 ----- .../node/src/integrations/tracing/index.ts | 3 +- packages/server-utils/src/index.ts | 4 +- .../src/integrations/fastify/errors.ts | 2 +- .../src/integrations/fastify/index.ts | 30 +-- .../integrations/fastify/instrumentation.ts | 30 ++- 21 files changed, 195 insertions(+), 913 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts delete mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/fastify/scenario-error-handler.mjs delete mode 100644 packages/node/src/integrations/tracing/fastify/index.ts delete mode 100644 packages/node/src/integrations/tracing/fastify/types.ts diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json index 3fa36adbbbd5..54a810a45fa0 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json @@ -4,9 +4,7 @@ "private": true, "scripts": { "start": "ts-node src/app.ts", - "start:override": "ts-node src/app-handle-error-override.ts", "test": "playwright test", - "test:override": "playwright test --config playwright.override.config.mjs", "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts deleted file mode 100644 index 7f0890002772..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type * as S from '@sentry/node'; -const Sentry = require('@sentry/node') as typeof S; - -// We wrap console.warn to find out if a warning is incorrectly logged -console.warn = new Proxy(console.warn, { - apply: function (target, thisArg, argumentsList) { - const msg = argumentsList[0]; - if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { - console.error(`Sentry warning was triggered: ${msg}`); - process.exit(1); - } - - return target.apply(thisArg, argumentsList); - }, -}); - -Sentry.init({ - traceLifecycle: 'static', - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - integrations: [ - Sentry.fastifyIntegration({ - shouldHandleError: (error, _request, _reply) => { - return true; - }, - }), - ], - tracesSampleRate: 1, - tunnel: 'http://localhost:3031/', // proxy server - tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], -}); - -import type * as H from 'http'; -import type * as F from 'fastify'; - -// Make sure fastify is imported after Sentry is initialized -const { fastify } = require('fastify') as typeof F; -const http = require('http') as typeof H; - -const app = fastify(); -const port = 3030; -const port2 = 3040; - -Sentry.setupFastifyErrorHandler(app, { - shouldHandleError: (error, _request, _reply) => { - // @ts-ignore // Fastify V3 is not typed correctly - if (_request.url?.includes('/test-error-not-captured')) { - // Errors from this path will not be captured by Sentry - return false; - } - - return true; - }, -}); - -app.get('/test-success', function (_req, res) { - res.send({ version: 'v1' }); -}); - -app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { - res.send({ paramWas: req.params.param }); -}); - -app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { - const headers = req.headers; - - res.send({ headers, id: req.params.id }); -}); - -app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { - const id = req.params.id; - const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); - - res.send(data); -}); - -app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { - const id = req.params.id; - const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); - const data = await response.json(); - - res.send(data); -}); - -app.get('/test-transaction', async function (req, res) { - Sentry.startSpan({ name: 'test-span' }, () => { - Sentry.startSpan({ name: 'child-span' }, () => {}); - }); - - res.send({}); -}); - -app.get('/test-error', async function (req, res) { - const exceptionId = Sentry.captureException(new Error('This is an error')); - - await Sentry.flush(2000); - - 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}`); -}); - -app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { - const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); - const data = await fetchResponse.json(); - - res.send(data); -}); - -app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { - const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); - const data = await fetchResponse.json(); - - res.send(data); -}); - -app.get('/test-outgoing-http-external-allowed', async function (req, res) { - const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); - res.send(data); -}); - -app.get('/test-outgoing-http-external-disallowed', async function (req, res) { - const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); - res.send(data); -}); - -app.post('/test-post', function (req, res) { - res.send({ status: 'ok', body: req.body }); -}); - -app.listen({ port: port }); - -// A second app so we can test header propagation between external URLs -const app2 = fastify(); -app2.get('/external-allowed', function (req, res) { - const headers = req.headers; - - res.send({ headers, route: '/external-allowed' }); -}); - -app2.get('/external-disallowed', function (req, res) { - const headers = req.headers; - - res.send({ headers, route: '/external-disallowed' }); -}); - -app2.listen({ port: port2 }); - -function makeHttpRequest(url: string) { - return new Promise(resolve => { - const data: any[] = []; - - http - .request(url, httpRes => { - httpRes.on('data', chunk => { - data.push(chunk); - }); - httpRes.on('error', error => { - resolve({ error: error.message, url }); - }); - httpRes.on('end', () => { - try { - const json = JSON.parse(Buffer.concat(data).toString()); - resolve(json); - } catch { - resolve({ data: Buffer.concat(data).toString(), url }); - } - }); - }) - .end(); - }); -} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts index c16ca946da86..2725ed19b3a7 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts @@ -47,8 +47,6 @@ const app = fastify(); const port = 3030; const port2 = 3040; -Sentry.setupFastifyErrorHandler(app); - app.get('/test-success', function (_req, res) { res.send({ version: 'v1' }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json index 086ec85fac7a..5b26e3e12582 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json @@ -4,13 +4,11 @@ "private": true, "scripts": { "start": "ts-node src/app.ts", - "start:override": "ts-node src/app-handle-error-override.ts", "test": "playwright test", - "test:override": "playwright test --config playwright.override.config.mjs", "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", - "test:assert": "pnpm test && pnpm test:override" + "test:assert": "pnpm test" }, "dependencies": { "@sentry/node": "file:../../packed/sentry-node-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts deleted file mode 100644 index 5552e765b98b..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type * as S from '@sentry/node'; -const Sentry = require('@sentry/node') as typeof S; - -// We wrap console.warn to find out if a warning is incorrectly logged -console.warn = new Proxy(console.warn, { - apply: function (target, thisArg, argumentsList) { - const msg = argumentsList[0]; - if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { - console.error(`Sentry warning was triggered: ${msg}`); - process.exit(1); - } - - return target.apply(thisArg, argumentsList); - }, -}); - -Sentry.init({ - traceLifecycle: 'static', - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - integrations: [ - Sentry.fastifyIntegration({ - shouldHandleError: (error, _request, _reply) => { - return true; - }, - }), - ], - tracesSampleRate: 1, - tunnel: 'http://localhost:3031/', // proxy server - tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], -}); - -import type * as H from 'http'; -import type * as F from 'fastify'; - -// Make sure fastify is imported after Sentry is initialized -const { fastify } = require('fastify') as typeof F; -const http = require('http') as typeof H; - -const app = fastify(); -const port = 3030; -const port2 = 3040; - -Sentry.setupFastifyErrorHandler(app, { - shouldHandleError: (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; - }, -}); - -app.get('/test-success', function (_req, res) { - res.send({ version: 'v1' }); -}); - -app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { - res.send({ paramWas: req.params.param }); -}); - -app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { - const headers = req.headers; - - res.send({ headers, id: req.params.id }); -}); - -app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { - const id = req.params.id; - const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); - - res.send(data); -}); - -app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { - const id = req.params.id; - const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); - const data = await response.json(); - - res.send(data); -}); - -app.get('/test-transaction', async function (req, res) { - Sentry.startSpan({ name: 'test-span' }, () => { - Sentry.startSpan({ name: 'child-span' }, () => {}); - }); - - res.send({}); -}); - -app.get('/test-error', async function (req, res) { - const exceptionId = Sentry.captureException(new Error('This is an error')); - - await Sentry.flush(2000); - - 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('/test-4xx-error', async function (req, res) { - res.code(400); - throw new Error('This is a 4xx error'); -}); - -app.get('/test-5xx-error', async function (req, res) { - res.code(500); - throw new Error('This is a 5xx error'); -}); - -app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { - throw new Error(`This is an exception with id ${req.params.id}`); -}); - -app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { - const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); - const data = await fetchResponse.json(); - - res.send(data); -}); - -app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { - const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); - const data = await fetchResponse.json(); - - res.send(data); -}); - -app.get('/test-outgoing-http-external-allowed', async function (req, res) { - const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); - res.send(data); -}); - -app.get('/test-outgoing-http-external-disallowed', async function (req, res) { - const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); - res.send(data); -}); - -app.post('/test-post', function (req, res) { - res.send({ status: 'ok', body: req.body }); -}); - -app.listen({ port: port }); - -// A second app so we can test header propagation between external URLs -const app2 = fastify(); -app2.get('/external-allowed', function (req, res) { - const headers = req.headers; - - res.send({ headers, route: '/external-allowed' }); -}); - -app2.get('/external-disallowed', function (req, res) { - const headers = req.headers; - - res.send({ headers, route: '/external-disallowed' }); -}); - -app2.listen({ port: port2 }); - -function makeHttpRequest(url: string) { - return new Promise(resolve => { - const data: any[] = []; - - http - .request(url, httpRes => { - httpRes.on('data', chunk => { - data.push(chunk); - }); - httpRes.on('error', error => { - resolve({ error: error.message, url }); - }); - httpRes.on('end', () => { - try { - const json = JSON.parse(Buffer.concat(data).toString()); - resolve(json); - } catch { - resolve({ data: Buffer.concat(data).toString(), url }); - } - }); - }) - .end(); - }); -} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts index b519d42ae647..cbb38b3f0850 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts @@ -20,8 +20,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, integrations: [ Sentry.fastifyIntegration({ - shouldHandleError: (error, _request, _reply) => { - if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + shouldHandleError: (_error, request, _reply) => { + if (request.routeOptions?.url?.includes('/test-error-not-captured')) { // Errors from this path will not be captured by Sentry return false; } @@ -46,8 +46,6 @@ const app = fastify(); const port = 3030; const port2 = 3040; -Sentry.setupFastifyErrorHandler(app); - app.get('/test-success', function (_req, res) { res.send({ version: 'v1' }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json index 2668e22c4d01..fb96c9d178be 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json @@ -4,13 +4,11 @@ "private": true, "scripts": { "start": "ts-node src/app.ts", - "start:override": "ts-node src/app-handle-error-override.ts", "test": "playwright test", - "test:override": "playwright test --config playwright.override.config.mjs", "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", - "test:assert": "pnpm test && pnpm test:override" + "test:assert": "pnpm test" }, "dependencies": { "@sentry/node": "file:../../packed/sentry-node-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts deleted file mode 100644 index 30dbbc6cdca0..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts +++ /dev/null @@ -1,204 +0,0 @@ -import type * as S from '@sentry/node'; -const Sentry = require('@sentry/node') as typeof S; - -// We wrap console.warn to find out if a warning is incorrectly logged -console.warn = new Proxy(console.warn, { - apply: function (target, thisArg, argumentsList) { - const msg = argumentsList[0]; - if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { - console.error(`Sentry warning was triggered: ${msg}`); - process.exit(1); - } - - return target.apply(thisArg, argumentsList); - }, -}); - -Sentry.init({ - traceLifecycle: 'static', - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.E2E_TEST_DSN, - integrations: [ - Sentry.fastifyIntegration({ - shouldHandleError: (error, _request, _reply) => { - return true; - }, - }), - ], - tracesSampleRate: 1, - tunnel: 'http://localhost:3031/', // proxy server - tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], -}); - -import type * as H from 'http'; -import type * as F from 'fastify'; - -// Make sure fastify is imported after Sentry is initialized -const { fastify } = require('fastify') as typeof F; -const http = require('http') as typeof H; - -const app = fastify(); -const port = 3030; -const port2 = 3040; - -Sentry.setupFastifyErrorHandler(app, { - shouldHandleError: (error, _request, _reply) => { - // @ts-ignore // Fastify V5 is not typed correctly - if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { - // Errors from this path will not be captured by Sentry - return false; - } - - // @ts-ignore // Fastify V5 is not typed correctly - if (_request.routeOptions?.url?.includes('/test-error-ignored') && _reply.statusCode === 500) { - return false; - } - - return true; - }, -}); - -app.get('/test-success', function (_req, res) { - res.send({ version: 'v1' }); -}); - -app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { - res.send({ paramWas: req.params.param }); -}); - -app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { - const headers = req.headers; - - res.send({ headers, id: req.params.id }); -}); - -app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { - const id = req.params.id; - const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); - - res.send(data); -}); - -app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { - const id = req.params.id; - const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); - const data = await response.json(); - - res.send(data); -}); - -app.get('/test-transaction', async function (req, res) { - Sentry.startSpan({ name: 'test-span' }, () => { - Sentry.startSpan({ name: 'child-span' }, () => {}); - }); - - res.send({}); -}); - -app.get('/test-error', async function (req, res) { - const exceptionId = Sentry.captureException(new Error('This is an error')); - - await Sentry.flush(2000); - - 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}`); -}); - -app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { - const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); - const data = await fetchResponse.json(); - - res.send(data); -}); - -app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { - const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); - const data = await fetchResponse.json(); - - res.send(data); -}); - -app.get('/test-outgoing-http-external-allowed', async function (req, res) { - const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); - res.send(data); -}); - -app.get('/test-outgoing-http-external-disallowed', async function (req, res) { - const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); - res.send(data); -}); - -// Regression test for https://github.com/fastify/fastify/issues/6409 -// The error diagnostic channel was always sending 200 unless explicitly changed. -// This was fixed in Fastify 5.7.0 -app.register((childApp: F.FastifyInstance, _options: F.FastifyPluginOptions, next: (err?: Error) => void) => { - childApp.setErrorHandler((error: Error, _request: F.FastifyRequest, reply: F.FastifyReply) => { - reply.send({ ok: false }); - }); - - childApp.get('/test-error-ignored', async function () { - throw new Error('This is an error that will not be captured'); - }); - - next(); -}); - -app.post('/test-post', function (req, res) { - res.send({ status: 'ok', body: req.body }); -}); - -app.get('/flush', async function (_req, res) { - await Sentry.flush(); - res.send({ ok: true }); -}); - -app.listen({ port: port }); - -// A second app so we can test header propagation between external URLs -const app2 = fastify(); -app2.get('/external-allowed', function (req, res) { - const headers = req.headers; - - res.send({ headers, route: '/external-allowed' }); -}); - -app2.get('/external-disallowed', function (req, res) { - const headers = req.headers; - - res.send({ headers, route: '/external-disallowed' }); -}); - -app2.listen({ port: port2 }); - -function makeHttpRequest(url: string) { - return new Promise(resolve => { - const data: any[] = []; - - http - .request(url, httpRes => { - httpRes.on('data', chunk => { - data.push(chunk); - }); - httpRes.on('error', error => { - resolve({ error: error.message, url }); - }); - httpRes.on('end', () => { - try { - const json = JSON.parse(Buffer.concat(data).toString()); - resolve(json); - } catch { - resolve({ data: Buffer.concat(data).toString(), url }); - } - }); - }) - .end(); - }); -} 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 e3841d1eefe8..7055d0ed259e 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 @@ -20,14 +20,13 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, integrations: [ Sentry.fastifyIntegration({ - shouldHandleError: (error, _request, _reply) => { - if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + shouldHandleError: (_error, request, _reply) => { + if (request.routeOptions?.url?.includes('/test-error-not-captured')) { // Errors from this path will not be captured by Sentry return false; } - // @ts-ignore // Fastify V5 is not typed correctly - if (_request.routeOptions?.url?.includes('/test-error-ignored') && _reply.statusCode === 500) { + if (request.routeOptions?.url?.includes('/test-error-ignored') && _reply.statusCode === 500) { return false; } diff --git a/dev-packages/node-integration-tests/suites/tracing/fastify/scenario-error-handler.mjs b/dev-packages/node-integration-tests/suites/tracing/fastify/scenario-error-handler.mjs new file mode 100644 index 000000000000..7044edd2e53e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/fastify/scenario-error-handler.mjs @@ -0,0 +1,34 @@ +import * as Sentry from '@sentry/node'; +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import Fastify from 'fastify'; + +const app = Fastify(); + +let port; + +app.get('/test-exception/:id', async request => { + throw new Error(`This is an exception with id ${request.params.id}`); +}); + +app.get('/test-error-not-captured', async () => { + throw new Error('This is an error that will not be captured'); +}); + +Sentry.setupFastifyErrorHandler(app, { + shouldHandleError: (_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; + }, +}); + +const run = async () => { + await app.listen({ port: 0, host: 'localhost' }); + port = app.server.address().port; + sendPortToRunner(port); +}; + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts b/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts index 445aed9abb9f..c228df61e251 100644 --- a/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts @@ -1,101 +1,131 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -describe('fastify auto-instrumentation', () => { +describe('fastify v5 auto-instrumentation', () => { afterAll(() => { cleanupChildProcesses(); }); - describe('fastify v5', () => { - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { - test('creates transaction with fastify hook, request-handler and manual spans', async () => { - const runner = createRunner() - .expect({ - transaction: { - transaction: 'GET /test-transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - op: 'middleware', - origin: 'auto.http.otel.fastify', - data: expect.objectContaining({ - 'fastify.type': 'hook', - 'sentry.op': 'middleware', - 'sentry.origin': 'auto.http.otel.fastify', - }), - }), - expect.objectContaining({ - op: 'handler', - origin: 'auto.http.otel.fastify', - data: expect.objectContaining({ - 'sentry.op': 'handler', - 'sentry.origin': 'auto.http.otel.fastify', - }), - }), - expect.objectContaining({ - description: 'test-span', - origin: 'manual', + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('creates transaction with fastify hook, request-handler and manual spans', async () => { + const runner = createRunner() + .expect({ + transaction: { + transaction: 'GET /test-transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + op: 'middleware', + origin: 'auto.http.otel.fastify', + data: expect.objectContaining({ + 'fastify.type': 'hook', + 'sentry.op': 'middleware', + 'sentry.origin': 'auto.http.otel.fastify', }), - expect.objectContaining({ - description: 'child-span', - origin: 'manual', + }), + expect.objectContaining({ + op: 'handler', + origin: 'auto.http.otel.fastify', + data: expect.objectContaining({ + 'sentry.op': 'handler', + 'sentry.origin': 'auto.http.otel.fastify', }), - ]), + }), + expect.objectContaining({ + description: 'test-span', + origin: 'manual', + }), + expect.objectContaining({ + description: 'child-span', + origin: 'manual', + }), + ]), + }, + }) + .start(); + runner.makeRequest('get', '/test-transaction'); + await runner.completed(); + }); + + describe('error capture via diagnostics channel', () => { + test('captures errors thrown in route handlers', async () => { + const runner = createRunner() + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'This is an exception with id 123', + mechanism: { + type: 'auto.function.fastify', + handled: false, + }, + }, + ], + }, + transaction: 'GET /test-exception/:id', + // The error must be parented to the fastify request span (not the root `http.server` span), + // so the trace context carries a `parent_span_id`. + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, }, }) .start(); - runner.makeRequest('get', '/test-transaction'); + runner.makeRequest('get', '/test-exception/123', { expectError: true }); await runner.completed(); }); + }); - // Fastify v5 only publishes the `tracing:fastify.request.handler:error` diagnostics channel when - // `tracingChannel(...).hasSubscribers` is truthy, which is what enables automatic error capture - // without `setupFastifyErrorHandler`. - describe('error capture via diagnostics channel', () => { - test('captures errors thrown in route handlers', async () => { - const runner = createRunner() - .ignore('transaction') - .expect({ - event: { - exception: { - values: [ - { - type: 'Error', - value: 'This is an exception with id 123', - mechanism: { - type: 'auto.function.fastify', - handled: false, - }, - }, - ], - }, - transaction: 'GET /test-exception/:id', - // The error must be parented to the fastify request span (not the root `http.server` span), - // so the trace context carries a `parent_span_id`. - contexts: { - trace: { - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + test('propagates trace data to outgoing requests within a request handler', async () => { + const runner = createRunner().start(); + const response = await runner.makeRequest<{ headers: Record }>('get', '/test-outgoing-fetch/123'); + + expect(response?.headers?.['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-1$/); + expect(response?.headers?.['baggage']).toEqual(expect.any(String)); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-error-handler.mjs', 'instrument.mjs', (createRunner, test) => { + test('shouldHandleError override works', async () => { + const runner = createRunner() + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'This is an exception with id 123', + mechanism: { + type: 'auto.function.fastify', + handled: false, }, }, + ], + }, + transaction: 'GET /test-exception/:id', + // The error must be parented to the fastify request span (not the root `http.server` span), + // so the trace context carries a `parent_span_id`. + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }, - }) - .start(); - runner.makeRequest('get', '/test-exception/123', { expectError: true }); - await runner.completed(); - }); - }); - - test('propagates trace data to outgoing requests within a request handler', async () => { - const runner = createRunner().start(); - const response = await runner.makeRequest<{ headers: Record }>( - 'get', - '/test-outgoing-fetch/123', - ); + }, + }, + }) + .start(); + await runner.makeRequest('get', '/test-error-not-captured', { expectError: true }); + await runner.makeRequest('get', '/test-exception/123', { expectError: true }); - expect(response?.headers?.['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-1$/); - expect(response?.headers?.['baggage']).toEqual(expect.any(String)); - }); + await runner.completed(); }); }); }); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b54adc166a21..c10c8adf810a 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -117,6 +117,7 @@ export { expressErrorHandler, setupExpressErrorHandler, fastifyIntegration, + // oxlint-disable-next-line typescript/no-deprecated setupFastifyErrorHandler, firebaseIntegration, koaIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 8786ab737f17..6b2eb050e0b2 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -96,6 +96,7 @@ export { expressErrorHandler, setupExpressErrorHandler, fastifyIntegration, + // oxlint-disable-next-line typescript/no-deprecated setupFastifyErrorHandler, firebaseIntegration, koaIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 3483dbff8163..629ce5fdeaff 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -2,7 +2,6 @@ export { httpIntegration } from './integrations/http'; export { nativeNodeFetchIntegration } from './integrations/node-fetch'; export { fsIntegration } from './integrations/fs'; export { expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; -export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; export { amqplibIntegration, anthropicAIIntegration, @@ -41,6 +40,9 @@ export { instrumentLangChainEmbeddings, instrumentStateGraph, instrumentStateGraphCompile, + fastifyIntegration, + // oxlint-disable-next-line typescript/no-deprecated + setupFastifyErrorHandler, } from '@sentry/server-utils'; export { setupHapiErrorHandler } from './integrations/tracing/hapi'; export { setupKoaErrorHandler } from './integrations/tracing/koa'; diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts deleted file mode 100644 index 3b4977416908..000000000000 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { Integration } from '@sentry/core'; -import { defineIntegration, getClient } from '@sentry/core'; -import type { FastifyInstance, FastifyMinimal, FastifyReply, FastifyRequest } from './types'; -import { - fastifyIntegration as serverUtilsFastifyIntegration, - instrumentFastify, - handleFastifyError, -} from '@sentry/server-utils'; - -interface FastifyIntegration extends Integration { - getShouldHandleError: () => (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; - // todo(v11): Remove this - setShouldHandleError: ( - shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean, - ) => void; -} - -// oxlint-disable-next-line typescript/no-deprecated -export { instrumentFastify }; - -/** - * Options for the Fastify integration. - * - * `shouldHandleError` - 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({ - * shouldHandleError(_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) - */ - shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; -} - -interface FastifyHandlerOptions { - /** - * Callback method deciding whether error should be captured and sent to Sentry - * - * @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) - * - * @example - * - * - * ```javascript - * setupFastifyErrorHandler(app, { - * shouldHandleError(_error, _request, reply) { - * return reply.statusCode >= 400; - * }, - * }); - * ``` - * - * - * If using TypeScript, you can cast the request and reply to get full type safety. - * - * ```typescript - * import type { FastifyRequest, FastifyReply } from 'fastify'; - * - * setupFastifyErrorHandler(app, { - * shouldHandleError(error, minimalRequest, minimalReply) { - * const request = minimalRequest as FastifyRequest; - * const reply = minimalReply as FastifyReply; - * return reply.statusCode >= 500; - * }, - * }); - * ``` - */ - shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; -} - -const INTEGRATION_NAME = 'Fastify' as const; - -function getFastifyIntegration(): FastifyIntegration | undefined { - const client = getClient(); - if (!client) { - return undefined; - } else { - return client.getIntegrationByName(INTEGRATION_NAME); - } -} - -/** - * Adds Sentry tracing instrumentation for [Fastify](https://fastify.dev/). - * - * If you also want to capture errors, you need to call `setupFastifyErrorHandler(app)` after you set up your Fastify server. - * - * For more information, see the [fastify documentation](https://docs.sentry.io/platforms/javascript/guides/fastify/). - * - * @example - * ```javascript - * const Sentry = require('@sentry/node'); - * - * Sentry.init({ - * integrations: [Sentry.fastifyIntegration()], - * }) - * ``` - */ -export const fastifyIntegration = defineIntegration((options: Partial = {}) => - serverUtilsFastifyIntegration(options), -); - -/** - * Add an Fastify error handler to capture errors to Sentry. - * - * @param fastify The Fastify instance to which to add the error handler - * @param options Configuration options for the handler - * - * @example - * ```javascript - * const Sentry = require('@sentry/node'); - * const Fastify = require("fastify"); - * - * const app = Fastify(); - * - * Sentry.setupFastifyErrorHandler(app); - * - * // Add your routes, etc. - * - * app.listen({ port: 3000 }); - * ``` - */ -export function setupFastifyErrorHandler(fastify: FastifyMinimal, options?: Partial): void { - if (options?.shouldHandleError) { - getFastifyIntegration()?.setShouldHandleError(options.shouldHandleError); - } - - const plugin = Object.assign( - function (fastify: FastifyInstance, _options: unknown, done: () => void): void { - fastify.addHook('onError', async (request, reply, error) => { - // oxlint-disable-next-line typescript/no-deprecated - handleFastifyError.call(handleFastifyError, error, request, reply, 'onError-hook'); - }); - done(); - }, - { - [Symbol.for('skip-override')]: true, - [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', - }, - ); - - fastify.register(plugin); -} diff --git a/packages/node/src/integrations/tracing/fastify/types.ts b/packages/node/src/integrations/tracing/fastify/types.ts deleted file mode 100644 index 7068afabadb0..000000000000 --- a/packages/node/src/integrations/tracing/fastify/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export type HandlerOriginal = - | ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => Promise) - | ((request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => void); - -export type FastifyError = any; - -export type HookHandlerDoneFunction = (err?: TError) => void; - -export type FastifyErrorCodes = any; - -export type FastifyPlugin = ( - instance: FastifyInstance, - opts: any, - done: HookHandlerDoneFunction, -) => unknown | Promise; - -export interface FastifyInstance { - version: string; - register: (plugin: any) => FastifyInstance; - after: (listener?: (err: Error) => void) => FastifyInstance; - addHook(hook: string, handler: HandlerOriginal): FastifyInstance; - addHook( - hook: 'onError', - handler: (request: FastifyRequest, reply: FastifyReply, error: Error) => void, - ): FastifyInstance; - addHook(hook: 'onRequest', handler: (request: FastifyRequest, reply: FastifyReply) => void): FastifyInstance; -} - -/** - * Minimal type for `setupFastifyErrorHandler` parameter. - * Uses structural typing without overloads to avoid exactOptionalPropertyTypes issues. - * https://github.com/getsentry/sentry-javascript/issues/18619 - */ -export type FastifyMinimal = { - register: (plugin: (instance: any, opts: any, done: () => void) => void) => unknown; -}; - -export interface FastifyReply { - send: () => FastifyReply; - statusCode: number; -} - -export interface FastifyRequest { - method?: string; - // since fastify@4.10.0 - routeOptions?: { - url?: string; - }; - routerPath?: string; -} diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 2d7274326a40..ce17411905b1 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -1,5 +1,5 @@ import type { Integration } from '@sentry/core'; -import { prismaIntegration } from '@sentry/server-utils'; +import { prismaIntegration, fastifyIntegration } from '@sentry/server-utils'; import { amqplibIntegration, anthropicAIIntegration, @@ -25,7 +25,6 @@ import { tediousIntegration, vercelAIIntegration, } from '@sentry/server-utils/orchestrion'; -import { fastifyIntegration } from './fastify'; export function getAutoPerformanceIntegrations(): Integration[] { return [ diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 237693d45c14..93288611f8dc 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -10,7 +10,5 @@ export { vercelAIIntegration, type VercelAiOptions } from './vercel-ai'; export { fastifyIntegration, // oxlint-disable-next-line typescript/no-deprecated - handleFastifyError, - // oxlint-disable-next-line typescript/no-deprecated - instrumentFastify, + setupFastifyErrorHandler, } from './integrations/fastify'; diff --git a/packages/server-utils/src/integrations/fastify/errors.ts b/packages/server-utils/src/integrations/fastify/errors.ts index 490e960fa213..45c25bac0322 100644 --- a/packages/server-utils/src/integrations/fastify/errors.ts +++ b/packages/server-utils/src/integrations/fastify/errors.ts @@ -6,7 +6,7 @@ import { defaultShouldHandleError, INTEGRATION_NAME } from './utils'; function getFastifyIntegration(): FastifyIntegration | undefined { const client = getClient(); - return client?.getIntegrationByName(INTEGRATION_NAME); + return client?.getIntegrationByName(INTEGRATION_NAME) as FastifyIntegration | undefined; } /** diff --git a/packages/server-utils/src/integrations/fastify/index.ts b/packages/server-utils/src/integrations/fastify/index.ts index c690c4ce7407..d18dd256d5ff 100644 --- a/packages/server-utils/src/integrations/fastify/index.ts +++ b/packages/server-utils/src/integrations/fastify/index.ts @@ -1,7 +1,7 @@ import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; +import { defineIntegration, getClient } from '@sentry/core'; import type { FastifyIntegration, FastifyReply, FastifyRequest } from './types'; -import { instrumentFastify as _instrumentFastify } from './instrumentation'; +import { instrumentFastify } from './instrumentation'; import { defaultShouldHandleError, INTEGRATION_NAME } from './utils'; import { subscribeToFastifyErrorChannel, handleFastifyError as _handleFastifyError } from './errors'; @@ -30,9 +30,6 @@ import { subscribeToFastifyErrorChannel, handleFastifyError as _handleFastifyErr 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) @@ -49,7 +46,7 @@ const _fastifyIntegration = (({ shouldHandleError }: Partial boolean) { _shouldHandleError = shouldHandleError; }, - } satisfies FastifyIntegration; + }; }) satisfies IntegrationFn; /** * Adds Sentry tracing instrumentation for [Fastify](https://fastify.dev/). - * This integration supports Fastify v5 only. + * This integration supports Fastify v3.21.0-v5.0.0. * * For more information, see the [fastify documentation](https://docs.sentry.io/platforms/javascript/guides/fastify/). * @@ -78,11 +75,14 @@ const _fastifyIntegration = (({ shouldHandleError }: Partial): void { + if (options?.shouldHandleError) { + const integration = getClient()?.getIntegrationByName(INTEGRATION_NAME) as FastifyIntegration | undefined; + integration?.setShouldHandleError(options.shouldHandleError); + } +} diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index d9a400a23f25..8db79316940a 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -37,6 +37,7 @@ import { import type { FastifyInstance, FastifyRequest } from './types'; import { DEBUG_BUILD } from '../../debug-build'; import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; +import { handleFastifyError } from './errors'; const PACKAGE_NAME = '@sentry/instrumentation-fastify'; const SUPPORTED_VERSIONS = '>=3.21.0 <6'; @@ -101,7 +102,7 @@ function isFastifyRequest(arg: any): boolean { * The Fastify plugin that wires up the request/hook/handler spans. It is registered on every Fastify * instance via the `fastify.initialization` diagnostics channel. */ -function fastifyOtelPlugin(this: unknown, instance: any, _opts: unknown, done: () => void): void { +function fastifyTracingPlugin(this: unknown, instance: any, _opts: unknown, done: () => void): void { instance.decorate(kAddHookOriginal, instance.addHook); instance.decorate(kSetNotFoundOriginal, instance.setNotFoundHandler); instance.decorateRequest('opentelemetry', function opentelemetry(this: any) { @@ -109,7 +110,7 @@ function fastifyOtelPlugin(this: unknown, instance: any, _opts: unknown, done: ( }); instance.decorateRequest(kRequestSpan, null); - instance.addHook('onRoute', otelWireRoute); + instance.addHook('onRoute', onRoute); instance.addHook('onRequest', startRequestSpanHook); instance.addHook('onResponse', finalizeNotFoundSpanHook); @@ -119,7 +120,7 @@ function fastifyOtelPlugin(this: unknown, instance: any, _opts: unknown, done: ( done(); } -const pluginSymbols = fastifyOtelPlugin as unknown as Record; +const pluginSymbols = fastifyTracingPlugin as unknown as Record; pluginSymbols[Symbol.for('skip-override')] = true; pluginSymbols[Symbol.for('fastify.display-name')] = PACKAGE_NAME; pluginSymbols[Symbol.for('plugin-meta')] = { @@ -127,7 +128,7 @@ pluginSymbols[Symbol.for('plugin-meta')] = { name: PACKAGE_NAME, }; -function otelWireRoute(this: any, routeOptions: any): void { +function onRoute(this: any, routeOptions: any): void { if (routeOptions.config?.otel === false) { return; } @@ -388,9 +389,7 @@ let _isInstrumented = false; /** * Set up the Fastify (>= 3.21.0 < 6) instrumentation by subscribing to the `fastify.initialization` - * diagnostics channel and registering the span-creating plugin on every Fastify instance. - * - * Idempotent and exposes an `id` so it can participate in the OpenTelemetry preload list. + * diagnostics channel and registering the span-creating & error handler plugin on every Fastify instance. */ export const instrumentFastify = Object.assign( function instrumentFastify(): void { @@ -402,14 +401,29 @@ export const instrumentFastify = Object.assign( diagnosticsChannel.subscribe('fastify.initialization', message => { const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; - fastifyInstance?.register(fastifyOtelPlugin).after(err => { + fastifyInstance?.register(fastifyTracingPlugin).after(err => { if (err) { DEBUG_BUILD && debug.error('Failed to setup Fastify instrumentation', err); } else if (fastifyInstance) { instrumentOnRequest(fastifyInstance); } }); + + fastifyInstance?.register(fastifyErrorHandlerPlugin); }); }, { id: 'Fastify.v5' }, ); + +const fastifyErrorHandlerPlugin = Object.assign( + function (fastify: FastifyInstance, _options: unknown, done: () => void): void { + fastify.addHook('onError', async (request, reply, error) => { + handleFastifyError.call(handleFastifyError, error, request, reply, 'onError-hook'); + }); + done(); + }, + { + [Symbol.for('skip-override')]: true, + [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', + }, +); From 0776cdf1049a0ad96dde01f6c3bdce4b86d33ad6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 10:46:52 +0200 Subject: [PATCH 2/9] fixes and stuff --- .../test-applications/node-fastify-3/package.json | 2 +- .../server-utils/src/integrations/fastify/errors.ts | 10 ++-------- .../server-utils/src/integrations/fastify/index.ts | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json index 54a810a45fa0..f23e7be2be53 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json @@ -8,7 +8,7 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", - "test:assert": "pnpm test && pnpm test:override" + "test:assert": "pnpm test" }, "dependencies": { "@sentry/node": "file:../../packed/sentry-node-packed.tgz", diff --git a/packages/server-utils/src/integrations/fastify/errors.ts b/packages/server-utils/src/integrations/fastify/errors.ts index 45c25bac0322..0765079cdc92 100644 --- a/packages/server-utils/src/integrations/fastify/errors.ts +++ b/packages/server-utils/src/integrations/fastify/errors.ts @@ -1,7 +1,6 @@ import type { FastifyIntegration, FastifyReply, FastifyRequest } from './types'; import * as diagnosticsChannel from 'node:diagnostics_channel'; -import { DEBUG_BUILD } from '../../debug-build'; -import { getClient, debug, captureException } from '@sentry/core'; +import { getClient, captureException } from '@sentry/core'; import { defaultShouldHandleError, INTEGRATION_NAME } from './utils'; function getFastifyIntegration(): FastifyIntegration | undefined { @@ -43,13 +42,8 @@ export function handleFastifyError( } if (this.diagnosticsChannelExists && handlerOrigin === 'onError-hook') { - DEBUG_BUILD && - debug.warn( - 'Fastify error handler was already registered via diagnostics channel.', - 'You can safely remove `setupFastifyErrorHandler` call and set `shouldHandleError` on the integration options.', - ); - // If the diagnostics channel already exists, we don't need to handle the error again + // This is the case on fastivy v5 return; } diff --git a/packages/server-utils/src/integrations/fastify/index.ts b/packages/server-utils/src/integrations/fastify/index.ts index d18dd256d5ff..ee4c9b997bb7 100644 --- a/packages/server-utils/src/integrations/fastify/index.ts +++ b/packages/server-utils/src/integrations/fastify/index.ts @@ -3,7 +3,7 @@ import { defineIntegration, getClient } from '@sentry/core'; import type { FastifyIntegration, FastifyReply, FastifyRequest } from './types'; import { instrumentFastify } from './instrumentation'; import { defaultShouldHandleError, INTEGRATION_NAME } from './utils'; -import { subscribeToFastifyErrorChannel, handleFastifyError as _handleFastifyError } from './errors'; +import { subscribeToFastifyErrorChannel } from './errors'; /** * Options for the Fastify integration. From dd3127b1b29de6a416feb0b027ac792d5a794b39 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 10:55:26 +0200 Subject: [PATCH 3/9] skip perf instrumentation without spans --- .../src/integrations/fastify/instrumentation.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index 8db79316940a..1330b4ac273f 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -33,6 +33,7 @@ import { startInactiveSpan, startSpan, withActiveSpan, + hasSpansEnabled, } from '@sentry/core'; import type { FastifyInstance, FastifyRequest } from './types'; import { DEBUG_BUILD } from '../../debug-build'; @@ -401,13 +402,15 @@ export const instrumentFastify = Object.assign( diagnosticsChannel.subscribe('fastify.initialization', message => { const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; - fastifyInstance?.register(fastifyTracingPlugin).after(err => { - if (err) { - DEBUG_BUILD && debug.error('Failed to setup Fastify instrumentation', err); - } else if (fastifyInstance) { - instrumentOnRequest(fastifyInstance); - } - }); + if (hasSpansEnabled()) { + fastifyInstance?.register(fastifyTracingPlugin).after(err => { + if (err) { + DEBUG_BUILD && debug.error('Failed to setup Fastify instrumentation', err); + } else if (fastifyInstance) { + instrumentOnRequest(fastifyInstance); + } + }); + } fastifyInstance?.register(fastifyErrorHandlerPlugin); }); From 845f4e257f8f00f369fddba8398a1a62eafd0931 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 11:03:44 +0200 Subject: [PATCH 4/9] handle no-tracing scenario --- .../tracing/fastify/instrument-no-tracing.mjs | 10 ++ .../suites/tracing/fastify/test.ts | 91 +++++++++++++------ .../integrations/fastify/instrumentation.ts | 36 ++++---- 3 files changed, 89 insertions(+), 48 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/fastify/instrument-no-tracing.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/fastify/instrument-no-tracing.mjs b/dev-packages/node-integration-tests/suites/tracing/fastify/instrument-no-tracing.mjs new file mode 100644 index 000000000000..03ea9ecc571a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/fastify/instrument-no-tracing.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + integrations: [Sentry.fastifyIntegration()], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts b/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts index c228df61e251..d44c8293ad12 100644 --- a/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/fastify/test.ts @@ -46,40 +46,38 @@ describe('fastify v5 auto-instrumentation', () => { await runner.completed(); }); - describe('error capture via diagnostics channel', () => { - test('captures errors thrown in route handlers', async () => { - const runner = createRunner() - .ignore('transaction') - .expect({ - event: { - exception: { - values: [ - { - type: 'Error', - value: 'This is an exception with id 123', - mechanism: { - type: 'auto.function.fastify', - handled: false, - }, + test('captures errors thrown in route handlers', async () => { + const runner = createRunner() + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'This is an exception with id 123', + mechanism: { + type: 'auto.function.fastify', + handled: false, }, - ], - }, - transaction: 'GET /test-exception/:id', - // The error must be parented to the fastify request span (not the root `http.server` span), - // so the trace context carries a `parent_span_id`. - contexts: { - trace: { - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }, + ], + }, + transaction: 'GET /test-exception/:id', + // The error must be parented to the fastify request span (not the root `http.server` span), + // so the trace context carries a `parent_span_id`. + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }, }, - }) - .start(); - runner.makeRequest('get', '/test-exception/123', { expectError: true }); - await runner.completed(); - }); + }, + }) + .start(); + runner.makeRequest('get', '/test-exception/123', { expectError: true }); + await runner.completed(); }); test('propagates trace data to outgoing requests within a request handler', async () => { @@ -128,4 +126,37 @@ describe('fastify v5 auto-instrumentation', () => { await runner.completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-no-tracing.mjs', (createRunner, test) => { + test('captures errors thrown in route handlers without tracing', async () => { + const runner = createRunner() + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'This is an exception with id 456', + mechanism: { + type: 'auto.function.fastify', + handled: false, + }, + }, + ], + }, + transaction: 'GET /test-exception/:id', + // Has no parent_span_id because tracing is disabled + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .start(); + runner.makeRequest('get', '/test-exception/456', { expectError: true }); + await runner.completed(); + }); + }); }); diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index 1330b4ac273f..c6c0a0b6a2cd 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -26,7 +26,6 @@ import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import { isObjectLike, - debug, getIsolationScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, @@ -36,7 +35,6 @@ import { hasSpansEnabled, } from '@sentry/core'; import type { FastifyInstance, FastifyRequest } from './types'; -import { DEBUG_BUILD } from '../../debug-build'; import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; import { handleFastifyError } from './errors'; @@ -377,15 +375,6 @@ function stripFastifyPrefix(hookName = ''): string { .replace(/^@sentry\/instrumentation-fastify -> /, ''); } -function instrumentOnRequest(fastify: FastifyInstance): void { - fastify.addHook('onRequest', async (request: FastifyRequest, _reply) => { - const routeName = getRequestRouteUrl(request); - const method = request.method || 'GET'; - - getIsolationScope().setTransactionName(`${method} ${routeName}`); - }); -} - let _isInstrumented = false; /** @@ -403,15 +392,10 @@ export const instrumentFastify = Object.assign( const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; if (hasSpansEnabled()) { - fastifyInstance?.register(fastifyTracingPlugin).after(err => { - if (err) { - DEBUG_BUILD && debug.error('Failed to setup Fastify instrumentation', err); - } else if (fastifyInstance) { - instrumentOnRequest(fastifyInstance); - } - }); + fastifyInstance?.register(fastifyTracingPlugin); } + fastifyInstance?.register(fastifySetTransactionNamePlugin); fastifyInstance?.register(fastifyErrorHandlerPlugin); }); }, @@ -430,3 +414,19 @@ const fastifyErrorHandlerPlugin = Object.assign( [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', }, ); + +const fastifySetTransactionNamePlugin = Object.assign( + function (fastify: FastifyInstance, _options: unknown, done: () => void): void { + fastify.addHook('onRequest', async (request: FastifyRequest, _reply) => { + const routeName = getRequestRouteUrl(request); + const method = request.method || 'GET'; + + getIsolationScope().setTransactionName(`${method} ${routeName}`); + }); + done(); + }, + { + [Symbol.for('skip-override')]: true, + [Symbol.for('fastify.display-name')]: 'sentry-fastify-minimal-tracing', + }, +); From 5526bcc9f54cf43b5dad24dcd9ace5c394de6a3c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 11:27:11 +0200 Subject: [PATCH 5/9] combine into single plugin --- .../integrations/fastify/instrumentation.ts | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index c6c0a0b6a2cd..71a0abcf2d65 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -395,7 +395,6 @@ export const instrumentFastify = Object.assign( fastifyInstance?.register(fastifyTracingPlugin); } - fastifyInstance?.register(fastifySetTransactionNamePlugin); fastifyInstance?.register(fastifyErrorHandlerPlugin); }); }, @@ -403,19 +402,6 @@ export const instrumentFastify = Object.assign( ); const fastifyErrorHandlerPlugin = Object.assign( - function (fastify: FastifyInstance, _options: unknown, done: () => void): void { - fastify.addHook('onError', async (request, reply, error) => { - handleFastifyError.call(handleFastifyError, error, request, reply, 'onError-hook'); - }); - done(); - }, - { - [Symbol.for('skip-override')]: true, - [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', - }, -); - -const fastifySetTransactionNamePlugin = Object.assign( function (fastify: FastifyInstance, _options: unknown, done: () => void): void { fastify.addHook('onRequest', async (request: FastifyRequest, _reply) => { const routeName = getRequestRouteUrl(request); @@ -423,10 +409,15 @@ const fastifySetTransactionNamePlugin = Object.assign( getIsolationScope().setTransactionName(`${method} ${routeName}`); }); + + fastify.addHook('onError', async (request, reply, error) => { + handleFastifyError.call(handleFastifyError, error, request, reply, 'onError-hook'); + }); + done(); }, { [Symbol.for('skip-override')]: true, - [Symbol.for('fastify.display-name')]: 'sentry-fastify-minimal-tracing', + [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', }, ); From 487d2f461b98bab5a1b8ae9f175142b9c9f8aae2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 12:26:28 +0200 Subject: [PATCH 6/9] revert span only test --- .../src/integrations/fastify/instrumentation.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index 71a0abcf2d65..3bc0f7fb7ab0 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -32,7 +32,6 @@ import { startInactiveSpan, startSpan, withActiveSpan, - hasSpansEnabled, } from '@sentry/core'; import type { FastifyInstance, FastifyRequest } from './types'; import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; @@ -391,10 +390,7 @@ export const instrumentFastify = Object.assign( diagnosticsChannel.subscribe('fastify.initialization', message => { const fastifyInstance = (message as { fastify?: FastifyInstance }).fastify; - if (hasSpansEnabled()) { - fastifyInstance?.register(fastifyTracingPlugin); - } - + fastifyInstance?.register(fastifyTracingPlugin); fastifyInstance?.register(fastifyErrorHandlerPlugin); }); }, From 53d2843645baf7ea91b57e0c9e4c315038c9bf96 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 12:31:11 +0200 Subject: [PATCH 7/9] fix tests --- .../node-fastify-4/tests/transactions.test.ts | 4 ++-- .../node-fastify-5/tests/transactions.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts index 662ece5f348a..4f5a6143dbab 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts @@ -69,11 +69,11 @@ test('Sends an API route transaction', async ({ baseURL }) => { data: { 'fastify.type': 'hook', 'hook.callback.name': 'anonymous', - 'hook.name': 'fastify -> @sentry/instrumentation-fastify - onRequest', + 'hook.name': 'fastify -> @sentry/instrumentation-fastify -> sentry-fastify-error-handler - onRequest', 'sentry.op': 'middleware', 'sentry.origin': 'auto.http.otel.fastify', }, - description: '@sentry/instrumentation-fastify - onRequest', + description: 'sentry-fastify-error-handler - onRequest', op: 'middleware', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts index ec871f134b42..5d0d5fdd0d12 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts @@ -69,11 +69,11 @@ test('Sends an API route transaction', async ({ baseURL }) => { data: { 'fastify.type': 'hook', 'hook.callback.name': 'anonymous', - 'hook.name': 'fastify -> @sentry/instrumentation-fastify - onRequest', + 'hook.name': 'fastify -> @sentry/instrumentation-fastify -> sentry-fastify-error-handler - onRequest', 'sentry.op': 'middleware', 'sentry.origin': 'auto.http.otel.fastify', }, - description: '@sentry/instrumentation-fastify - onRequest', + description: 'sentry-fastify-error-handler - onRequest', op: 'middleware', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), From 81b0a37d63827826facb9fefd732296832ec0f42 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 12:56:40 +0200 Subject: [PATCH 8/9] migration entry --- MIGRATION.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index 5c3984abae46..2e5cf5be0066 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -616,6 +616,12 @@ Affected SDKs: All server-side SDKs. The LangGraph instrumentation no longer emits `gen_ai.create_agent` spans when a graph is compiled. `gen_ai.invoke_agent` and `gen_ai.execute_tool` spans are unaffected. If you reference `create_agent` spans in dashboards or alerts, update them accordingly. +### Fastify: `setupFastifyErrorHandler` is deprecated + +Affected SDKs: All server-side SDKs. + +`fastifyIntegration` is now a single, channel-based plugin that instruments Fastify v3.21 through v5, including error capture. Calling `setupFastifyErrorHandler(app)` is no longer required — errors are captured automatically once the integration is added `setupFastifyErrorHandler` is therefore deprecated and will be removed in the next major. + ### `@sentry/nextjs` **Tracing removed from generated templates:** Tracing was removed from the generated Pages Router API handler, Edge API handler, and Middleware wrapper templates. Route handlers and middleware are still instrumented automatically, so no action is required for most users. @@ -806,6 +812,7 @@ Sentry.init({ - (Remix) The `@sentry/remix/loader` entry point was removed. Use `node --import @sentry/remix/import` instead. - (TanStack Start) The `@sentry/tanstackstart-react/loader` entry point was removed. Use `node --import @sentry/tanstackstart-react/import` instead. - (Express) The deprecated `patchExpressModule(options)` signature was removed. Use `patchExpressModule(moduleExports, getOptions)` instead. +- (Fastify) The deprecated `instrumentFastify` and `handleFastifyError` exports were removed. `fastifyIntegration` now instruments Fastify (v3.21–v5) and captures errors on its own, so neither export is needed. See [Fastify: `setupFastifyErrorHandler` is deprecated](#fastify-setupfastifyerrorhandler-is-deprecated). - The `@sentry/node-core/light/otlp` entry point was removed, along with its optional `@opentelemetry/exporter-trace-otlp-http` peer dependency. `otlpIntegration` is now exported directly from every server-side SDK, so `Sentry.otlpIntegration()` needs no extra import or install. - The `otlpIntegration` options `setupOtlpTracesExporter` and `collectorUrl` were removed, and the integration no longer sets up a span exporter, span processor, or tracer provider. Configure your own exporter and point it at `Sentry.getOtlpTracesEndpoint(dsn)`, or at your collector's URL if you route through one. See [Connecting Sentry to your OpenTelemetry traces](#connecting-sentry-to-your-opentelemetry-traces). From 07fbd5863b7ac7b91d8b557199d0b0eb13c2c419 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 15:35:44 +0200 Subject: [PATCH 9/9] avoid dedupe, rely on base error dedupe --- .../src/integrations/fastify/errors.ts | 31 ++++++------------- .../integrations/fastify/instrumentation.ts | 2 +- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/server-utils/src/integrations/fastify/errors.ts b/packages/server-utils/src/integrations/fastify/errors.ts index 0765079cdc92..c9fd1c48ce55 100644 --- a/packages/server-utils/src/integrations/fastify/errors.ts +++ b/packages/server-utils/src/integrations/fastify/errors.ts @@ -1,6 +1,6 @@ import type { FastifyIntegration, FastifyReply, FastifyRequest } from './types'; import * as diagnosticsChannel from 'node:diagnostics_channel'; -import { getClient, captureException } from '@sentry/core'; +import { captureException, getClient } from '@sentry/core'; import { defaultShouldHandleError, INTEGRATION_NAME } from './utils'; function getFastifyIntegration(): FastifyIntegration | undefined { @@ -19,33 +19,22 @@ export function subscribeToFastifyErrorChannel(): void { reply: FastifyReply; }; - handleFastifyError.call(handleFastifyError, error, request, reply, 'diagnostics-channel'); + handleFastifyError(error, request, reply); }); } /** * Handle a Fastify error, and possibly send it to Sentry. + * + * On Fastify v5 a route handler error surfaces on both the diagnostics channel + * and the `onError` hook, so this runs twice for the same error. That's fine: + * `captureException` deduplicates by object identity (`__sentry_captured__`), so + * only the first call sends an event. Errors that reach only one path (e.g. + * thrown in an `onRequest` hook, or on Fastify v3/v4 which has no channel) are + * captured once. */ -export function handleFastifyError( - this: { - diagnosticsChannelExists?: boolean; - }, - error: Error, - request: FastifyRequest, - reply: FastifyReply, - handlerOrigin: 'diagnostics-channel' | 'onError-hook', -): void { +export function handleFastifyError(error: Error, request: FastifyRequest, reply: FastifyReply): void { const shouldHandleError = getFastifyIntegration()?.getShouldHandleError() || defaultShouldHandleError; - // Diagnostics channel runs before the onError hook, so we can use it to check if the handler was already registered - if (handlerOrigin === 'diagnostics-channel') { - this.diagnosticsChannelExists = true; - } - - if (this.diagnosticsChannelExists && handlerOrigin === 'onError-hook') { - // If the diagnostics channel already exists, we don't need to handle the error again - // This is the case on fastivy v5 - return; - } if (shouldHandleError(error, request, reply)) { captureException(error, { diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index 3bc0f7fb7ab0..3c8580215a8b 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -407,7 +407,7 @@ const fastifyErrorHandlerPlugin = Object.assign( }); fastify.addHook('onError', async (request, reply, error) => { - handleFastifyError.call(handleFastifyError, error, request, reply, 'onError-hook'); + handleFastifyError(error, request, reply); }); done();