diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 843e8e764207..ec44bc343fc1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1021,7 +1021,7 @@ jobs: 'sveltekit', 'sveltekit-2', 'generic-ts3.8', - 'node-experimental-fastify-app', + 'node-fastify-app', # TODO(v8): Re-enable hapi tests # 'node-hapi-app', 'node-exports-test-app', diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index f376a67bdc68..0635b129442b 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -46,6 +46,10 @@ const DEPENDENTS: Dependent[] = [ package: '@sentry/astro', compareWith: nodeExports, exports: Object.keys(SentryAstro), + ignoreExports: [ + // Not needed for Astro + 'setupFastifyErrorHandler', + ], }, { package: '@sentry/bun', @@ -82,13 +86,23 @@ const DEPENDENTS: Dependent[] = [ package: '@sentry/aws-serverless', compareWith: nodeExports, exports: Object.keys(SentryAWS), - ignoreExports: ['makeMain'], + ignoreExports: [ + // legacy, to be removed... + 'makeMain', + // Not needed for Serverless + 'setupFastifyErrorHandler', + ], }, { package: '@sentry/google-cloud-serverless', compareWith: nodeExports, exports: Object.keys(SentryGoogleCloud), - ignoreExports: ['makeMain'], + ignoreExports: [ + // legacy, to be removed... + 'makeMain', + // Not needed for Serverless + 'setupFastifyErrorHandler', + ], }, { package: '@sentry/sveltekit', diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore b/dev-packages/e2e-tests/test-applications/node-fastify-app/.gitignore similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore rename to dev-packages/e2e-tests/test-applications/node-fastify-app/.gitignore diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-fastify-app/.npmrc similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc rename to dev-packages/e2e-tests/test-applications/node-fastify-app/.npmrc diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/event-proxy-server.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/event-proxy-server.ts diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json similarity index 90% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json rename to dev-packages/e2e-tests/test-applications/node-fastify-app/package.json index cfa2c8be0f61..c7ea9cac71ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/package.json @@ -1,5 +1,5 @@ { - "name": "node-experimental-fastify-app", + "name": "node-fastify-app", "version": "1.0.0", "private": true, "scripts": { @@ -17,7 +17,6 @@ "@sentry/opentelemetry": "latest || *", "@types/node": "18.15.1", "fastify": "4.23.2", - "fastify-plugin": "4.5.1", "typescript": "4.9.5", "ts-node": "10.9.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/playwright.config.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/playwright.config.ts diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js similarity index 84% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js rename to dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js index 22f17ca4695c..7135fb33d91a 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js @@ -2,21 +2,12 @@ require('./tracing'); const Sentry = require('@sentry/node'); const { fastify } = require('fastify'); -const fastifyPlugin = require('fastify-plugin'); const http = require('http'); -const FastifySentry = fastifyPlugin(async (fastify, options) => { - fastify.decorateRequest('_sentryContext', null); - - fastify.addHook('onError', async (_request, _reply, error) => { - Sentry.captureException(error); - }); -}); - const app = fastify(); const port = 3030; -app.register(FastifySentry); +Sentry.setupFastifyErrorHandler(app); app.get('/test-success', function (req, res) { res.send({ version: 'v1' }); @@ -61,6 +52,10 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-exception', async function (req, res) { + throw new Error('This is an exception'); +}); + app.listen({ port: port }); function makeHttpRequest(url) { diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js b/dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js rename to dev-packages/e2e-tests/test-applications/node-fastify-app/src/tracing.js diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts similarity index 66% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts index 7ae352993f3c..2ab9be450dcd 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/start-event-proxy.ts @@ -2,5 +2,5 @@ import { startEventProxyServer } from './event-proxy-server'; startEventProxyServer({ port: 3031, - proxyServerName: 'node-experimental-fastify-app', + proxyServerName: 'node-fastify-app', }); diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts similarity index 54% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts index 60bef3fb0607..b2ef0649472f 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import axios, { AxiosError } from 'axios'; +import { waitForError } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; @@ -37,3 +38,35 @@ test('Sends exception to Sentry', async ({ baseURL }) => { ) .toBe(200); }); + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-app', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception'; + }); + + try { + await axios.get(`${baseURL}/test-exception`); + } catch { + // this results in an error, but we don't care - we want to check the error event + } + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + parent_span_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts similarity index 94% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts index 0afc4ff09b06..89a6320f725a 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/propagation.test.ts @@ -4,14 +4,14 @@ import axios from 'axios'; import { waitForTransaction } from '../event-proxy-server'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-inbound-headers' ); }); - const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-outgoing-http' @@ -118,14 +118,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-inbound-headers' ); }); - const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-outgoing-fetch' diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts similarity index 95% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts index 4ff9b5df632c..54f6916de36c 100644 --- a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts @@ -8,7 +8,7 @@ const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; const EVENT_POLLING_TIMEOUT = 90_000; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-fastify-app', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -58,13 +58,13 @@ test('Sends an API route transaction', async ({ baseURL }) => { spans: [ { data: { - 'plugin.name': 'fastify -> app-auto-0', + 'plugin.name': 'fastify -> sentry-fastify-error-handler', 'fastify.type': 'request_handler', 'http.route': '/test-transaction', 'otel.kind': 'INTERNAL', 'sentry.origin': 'auto.http.otel.fastify', }, - description: 'request handler - fastify -> app-auto-0', + description: 'request handler - fastify -> sentry-fastify-error-handler', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), diff --git a/dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-fastify-app/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json rename to dev-packages/e2e-tests/test-applications/node-fastify-app/tsconfig.json diff --git a/docs/v8-node.md b/docs/v8-node.md index 960541889c3e..858acbd94bed 100644 --- a/docs/v8-node.md +++ b/docs/v8-node.md @@ -15,7 +15,7 @@ If you want, you can use OpenTelemetry-native APIs to start spans, and Sentry wi We support the following Node Frameworks out of the box: - [Express](#express) -- Fastify +- [Fastify](#fastify) - Koa - Nest.js - Hapi @@ -101,3 +101,25 @@ Sentry.setupExpressErrorHandler(app); app.listen(3000); ``` + +## Fastify + +The following shows how you can setup Fastify instrumentation in v8. This will capture performance data & errors for +your Fastify app. + +```js +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1, +}); + +const { fastify } = require('fastify'); +const app = fastify(); +Sentry.setupFastifyErrorHandler(app); + +// add routes etc. here + +app.listen(); +``` diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 0db75fa312a3..68f663e42a60 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -11,7 +11,7 @@ export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejec export { anrIntegration } from './integrations/anr'; export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; -export { fastifyIntegration } from './integrations/tracing/fastify'; +export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; export { graphqlIntegration } from './integrations/tracing/graphql'; export { mongoIntegration } from './integrations/tracing/mongo'; export { mongooseIntegration } from './integrations/tracing/mongoose'; diff --git a/packages/node-experimental/src/integrations/tracing/fastify.ts b/packages/node-experimental/src/integrations/tracing/fastify.ts index 11742d960dff..3da89599401a 100644 --- a/packages/node-experimental/src/integrations/tracing/fastify.ts +++ b/packages/node-experimental/src/integrations/tracing/fastify.ts @@ -1,6 +1,6 @@ import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; -import { defineIntegration } from '@sentry/core'; +import { captureException, defineIntegration } from '@sentry/core'; import type { IntegrationFn } from '@sentry/types'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; @@ -28,3 +28,30 @@ const _fastifyIntegration = (() => { * Capture tracing data for fastify. */ export const fastifyIntegration = defineIntegration(_fastifyIntegration); + +// We inline the types we care about here +interface Fastify { + register: (plugin: unknown) => void; + addHook: (hook: string, handler: (request: unknown, reply: unknown, error: Error) => void) => void; +} + +/** + * Setup an error handler for Fastify. + */ +export function setupFastifyErrorHandler(fastify: Fastify): void { + const plugin = Object.assign( + function (fastify: Fastify, options: unknown, done: () => void): void { + fastify.addHook('onError', async (_request, _reply, error) => { + captureException(error); + }); + + done(); + }, + { + [Symbol.for('skip-override')]: true, + [Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler', + }, + ); + + fastify.register(plugin); +}