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 e25531d0f575..a7a754d3f1a6 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 @@ -15,7 +15,7 @@ "dependencies": { "@sentry/node": "latest || *", "@types/node": "^18.19.1", - "fastify": "5.3.2", + "fastify": "^5.7.0", "typescript": "5.6.3", "ts-node": "10.9.2" }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs index 31f2b913b58b..ee1f461c57c8 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs @@ -1,7 +1,7 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: `pnpm start`, + startCommand: `pnpm start:override`, }); export default config; 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 index 217201332e17..91f0353816bb 100644 --- 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 @@ -48,6 +48,11 @@ Sentry.setupFastifyErrorHandler(app, { 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; }, }); @@ -130,10 +135,30 @@ app.get('/test-outgoing-http-external-disallowed', async function (req, res) { 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 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 83f7e53a45ce..610a42f6fc00 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 @@ -25,6 +25,11 @@ Sentry.init({ 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; }, }), @@ -90,6 +95,21 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +// 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.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'); @@ -127,6 +147,11 @@ 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 diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts index a25f990748ea..8699b26d490b 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError } from '@sentry-internal/test-utils'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ baseURL }) => { const errorEventPromise = waitForError('node-fastify-5', event => { @@ -35,16 +35,54 @@ test('Sends correct error event', async ({ baseURL }) => { }); 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'; + let errorEventOccurred = false; + + waitForError('node-fastify-5', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured') { + errorEventOccurred = true; + } + return event?.transaction === 'GET /test-error-not-captured'; + }); + + const transactionEventPromise = waitForTransaction('node-fastify-5', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-error-not-captured'; + }); + + const response = await fetch(`${baseURL}/test-error-not-captured`); + + await transactionEventPromise; + + const flushResponse = await fetch(`${baseURL}/flush`); + + expect(response.status).toBe(500); + expect(flushResponse.status).toBe(200); + expect(errorEventOccurred).toBe(false); +}); + +// 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 +test('Error in child plugin with rethrown error handler reports correct 500 status', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('node-fastify-5', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured') { + errorEventOccurred = true; + } + return event?.transaction === 'GET /test-error-ignored'; }); - errorEventPromise.then(() => { - throw new Error('This error should not be captured'); + const transactionEventPromise = waitForTransaction('node-fastify-5', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-error-ignored'; }); - await fetch(`${baseURL}/test-error-not-captured`); + const response = await fetch(`${baseURL}/test-error-ignored`); + + await transactionEventPromise; + + const flushResponse = await fetch(`${baseURL}/flush`); - // wait for a short time to ensure the error is not captured - await new Promise(resolve => setTimeout(resolve, 1000)); + expect(response.status).toBe(500); + expect(flushResponse.status).toBe(200); + expect(errorEventOccurred).toBe(false); });