Skip to content

Commit eb6a3c5

Browse files
authored
feat(node): Add setupFastifyErrorHandler utility (#11061)
For easier setup of error handling in fastify.
1 parent 2b69368 commit eb6a3c5

File tree

17 files changed

+116
-26
lines changed

17 files changed

+116
-26
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ jobs:
10211021
'sveltekit',
10221022
'sveltekit-2',
10231023
'generic-ts3.8',
1024-
'node-experimental-fastify-app',
1024+
'node-fastify-app',
10251025
# TODO(v8): Re-enable hapi tests
10261026
# 'node-hapi-app',
10271027
'node-exports-test-app',

dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const DEPENDENTS: Dependent[] = [
4646
package: '@sentry/astro',
4747
compareWith: nodeExports,
4848
exports: Object.keys(SentryAstro),
49+
ignoreExports: [
50+
// Not needed for Astro
51+
'setupFastifyErrorHandler',
52+
],
4953
},
5054
{
5155
package: '@sentry/bun',
@@ -82,13 +86,23 @@ const DEPENDENTS: Dependent[] = [
8286
package: '@sentry/aws-serverless',
8387
compareWith: nodeExports,
8488
exports: Object.keys(SentryAWS),
85-
ignoreExports: ['makeMain'],
89+
ignoreExports: [
90+
// legacy, to be removed...
91+
'makeMain',
92+
// Not needed for Serverless
93+
'setupFastifyErrorHandler',
94+
],
8695
},
8796
{
8897
package: '@sentry/google-cloud-serverless',
8998
compareWith: nodeExports,
9099
exports: Object.keys(SentryGoogleCloud),
91-
ignoreExports: ['makeMain'],
100+
ignoreExports: [
101+
// legacy, to be removed...
102+
'makeMain',
103+
// Not needed for Serverless
104+
'setupFastifyErrorHandler',
105+
],
92106
},
93107
{
94108
package: '@sentry/sveltekit',

dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json renamed to dev-packages/e2e-tests/test-applications/node-fastify-app/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "node-experimental-fastify-app",
2+
"name": "node-fastify-app",
33
"version": "1.0.0",
44
"private": true,
55
"scripts": {
@@ -17,7 +17,6 @@
1717
"@sentry/opentelemetry": "latest || *",
1818
"@types/node": "18.15.1",
1919
"fastify": "4.23.2",
20-
"fastify-plugin": "4.5.1",
2120
"typescript": "4.9.5",
2221
"ts-node": "10.9.1"
2322
},

dev-packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js renamed to dev-packages/e2e-tests/test-applications/node-fastify-app/src/app.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,12 @@ require('./tracing');
22

33
const Sentry = require('@sentry/node');
44
const { fastify } = require('fastify');
5-
const fastifyPlugin = require('fastify-plugin');
65
const http = require('http');
76

8-
const FastifySentry = fastifyPlugin(async (fastify, options) => {
9-
fastify.decorateRequest('_sentryContext', null);
10-
11-
fastify.addHook('onError', async (_request, _reply, error) => {
12-
Sentry.captureException(error);
13-
});
14-
});
15-
167
const app = fastify();
178
const port = 3030;
189

19-
app.register(FastifySentry);
10+
Sentry.setupFastifyErrorHandler(app);
2011

2112
app.get('/test-success', function (req, res) {
2213
res.send({ version: 'v1' });
@@ -61,6 +52,10 @@ app.get('/test-error', async function (req, res) {
6152
res.send({ exceptionId });
6253
});
6354

55+
app.get('/test-exception', async function (req, res) {
56+
throw new Error('This is an exception');
57+
});
58+
6459
app.listen({ port: port });
6560

6661
function makeHttpRequest(url) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { startEventProxyServer } from './event-proxy-server';
22

33
startEventProxyServer({
44
port: 3031,
5-
proxyServerName: 'node-experimental-fastify-app',
5+
proxyServerName: 'node-fastify-app',
66
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import axios, { AxiosError } from 'axios';
3+
import { waitForError } from '../event-proxy-server';
34

45
const authToken = process.env.E2E_TEST_AUTH_TOKEN;
56
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
@@ -37,3 +38,35 @@ test('Sends exception to Sentry', async ({ baseURL }) => {
3738
)
3839
.toBe(200);
3940
});
41+
42+
test('Sends correct error event', async ({ baseURL }) => {
43+
const errorEventPromise = waitForError('node-fastify-app', event => {
44+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception';
45+
});
46+
47+
try {
48+
await axios.get(`${baseURL}/test-exception`);
49+
} catch {
50+
// this results in an error, but we don't care - we want to check the error event
51+
}
52+
53+
const errorEvent = await errorEventPromise;
54+
55+
expect(errorEvent.exception?.values).toHaveLength(1);
56+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception');
57+
58+
expect(errorEvent.request).toEqual({
59+
method: 'GET',
60+
cookies: {},
61+
headers: expect.any(Object),
62+
url: 'http://localhost:3030/test-exception',
63+
});
64+
65+
expect(errorEvent.transaction).toEqual('GET /test-exception');
66+
67+
expect(errorEvent.contexts?.trace).toEqual({
68+
trace_id: expect.any(String),
69+
span_id: expect.any(String),
70+
parent_span_id: expect.any(String),
71+
});
72+
});
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import axios from 'axios';
44
import { waitForTransaction } from '../event-proxy-server';
55

66
test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
7-
const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
7+
const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => {
88
return (
99
transactionEvent?.contexts?.trace?.op === 'http.server' &&
1010
transactionEvent?.transaction === 'GET /test-inbound-headers'
1111
);
1212
});
1313

14-
const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
14+
const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => {
1515
return (
1616
transactionEvent?.contexts?.trace?.op === 'http.server' &&
1717
transactionEvent?.transaction === 'GET /test-outgoing-http'
@@ -118,14 +118,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
118118
});
119119

120120
test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
121-
const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
121+
const inboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => {
122122
return (
123123
transactionEvent?.contexts?.trace?.op === 'http.server' &&
124124
transactionEvent?.transaction === 'GET /test-inbound-headers'
125125
);
126126
});
127127

128-
const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
128+
const outboundTransactionPromise = waitForTransaction('node-fastify-app', transactionEvent => {
129129
return (
130130
transactionEvent?.contexts?.trace?.op === 'http.server' &&
131131
transactionEvent?.transaction === 'GET /test-outgoing-fetch'
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
88
const EVENT_POLLING_TIMEOUT = 90_000;
99

1010
test('Sends an API route transaction', async ({ baseURL }) => {
11-
const pageloadTransactionEventPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => {
11+
const pageloadTransactionEventPromise = waitForTransaction('node-fastify-app', transactionEvent => {
1212
return (
1313
transactionEvent?.contexts?.trace?.op === 'http.server' &&
1414
transactionEvent?.transaction === 'GET /test-transaction'
@@ -58,13 +58,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
5858
spans: [
5959
{
6060
data: {
61-
'plugin.name': 'fastify -> app-auto-0',
61+
'plugin.name': 'fastify -> sentry-fastify-error-handler',
6262
'fastify.type': 'request_handler',
6363
'http.route': '/test-transaction',
6464
'otel.kind': 'INTERNAL',
6565
'sentry.origin': 'auto.http.otel.fastify',
6666
},
67-
description: 'request handler - fastify -> app-auto-0',
67+
description: 'request handler - fastify -> sentry-fastify-error-handler',
6868
parent_span_id: expect.any(String),
6969
span_id: expect.any(String),
7070
start_timestamp: expect.any(Number),

docs/v8-node.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ If you want, you can use OpenTelemetry-native APIs to start spans, and Sentry wi
1515
We support the following Node Frameworks out of the box:
1616

1717
- [Express](#express)
18-
- Fastify
18+
- [Fastify](#fastify)
1919
- Koa
2020
- Nest.js
2121
- Hapi
@@ -101,3 +101,25 @@ Sentry.setupExpressErrorHandler(app);
101101

102102
app.listen(3000);
103103
```
104+
105+
## Fastify
106+
107+
The following shows how you can setup Fastify instrumentation in v8. This will capture performance data & errors for
108+
your Fastify app.
109+
110+
```js
111+
const Sentry = require('@sentry/node');
112+
113+
Sentry.init({
114+
dsn: '__DSN__',
115+
tracesSampleRate: 1,
116+
});
117+
118+
const { fastify } = require('fastify');
119+
const app = fastify();
120+
Sentry.setupFastifyErrorHandler(app);
121+
122+
// add routes etc. here
123+
124+
app.listen();
125+
```

packages/node-experimental/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejec
1111
export { anrIntegration } from './integrations/anr';
1212

1313
export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express';
14-
export { fastifyIntegration } from './integrations/tracing/fastify';
14+
export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify';
1515
export { graphqlIntegration } from './integrations/tracing/graphql';
1616
export { mongoIntegration } from './integrations/tracing/mongo';
1717
export { mongooseIntegration } from './integrations/tracing/mongoose';

packages/node-experimental/src/integrations/tracing/fastify.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { registerInstrumentations } from '@opentelemetry/instrumentation';
22
import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify';
3-
import { defineIntegration } from '@sentry/core';
3+
import { captureException, defineIntegration } from '@sentry/core';
44
import type { IntegrationFn } from '@sentry/types';
55

66
import { addOriginToSpan } from '../../utils/addOriginToSpan';
@@ -28,3 +28,30 @@ const _fastifyIntegration = (() => {
2828
* Capture tracing data for fastify.
2929
*/
3030
export const fastifyIntegration = defineIntegration(_fastifyIntegration);
31+
32+
// We inline the types we care about here
33+
interface Fastify {
34+
register: (plugin: unknown) => void;
35+
addHook: (hook: string, handler: (request: unknown, reply: unknown, error: Error) => void) => void;
36+
}
37+
38+
/**
39+
* Setup an error handler for Fastify.
40+
*/
41+
export function setupFastifyErrorHandler(fastify: Fastify): void {
42+
const plugin = Object.assign(
43+
function (fastify: Fastify, options: unknown, done: () => void): void {
44+
fastify.addHook('onError', async (_request, _reply, error) => {
45+
captureException(error);
46+
});
47+
48+
done();
49+
},
50+
{
51+
[Symbol.for('skip-override')]: true,
52+
[Symbol.for('fastify.display-name')]: 'sentry-fastify-error-handler',
53+
},
54+
);
55+
56+
fastify.register(plugin);
57+
}

0 commit comments

Comments
 (0)