Skip to content

Commit 28b7d75

Browse files
authored
feat(fastify): Update scope transactionName when handling request (#11447)
This PR updates the isolation scope's `transactionName` by adding another hook callback in our `setupFastifyErrorHandler` fastify plugin. This is better than accessing the otel span for two reasons: 1. We can always update the transactionName, not just when a span is sampled and recording 2. The `onRequest` hook is executed earlier in the lifecycle than the `preHandler` hook that Otel uses to trigger the `requestHook` callback.
1 parent 0896e4f commit 28b7d75

File tree

4 files changed

+52
-8
lines changed

4 files changed

+52
-8
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ app.get('/test-error', async function (req, res) {
5555
res.send({ exceptionId });
5656
});
5757

58-
app.get('/test-exception', async function (req, res) {
59-
throw new Error('This is an exception');
58+
app.get('/test-exception/:id', async function (req, res) {
59+
throw new Error(`This is an exception with id ${req.params.id}`);
6060
});
6161

6262
app.get('/test-outgoing-fetch-external-allowed', async function (req, res) {

dev-packages/e2e-tests/test-applications/node-fastify-app/tests/errors.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,28 @@ test('Sends exception to Sentry', async ({ baseURL }) => {
4141

4242
test('Sends correct error event', async ({ baseURL }) => {
4343
const errorEventPromise = waitForError('node-fastify-app', event => {
44-
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception';
44+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
4545
});
4646

4747
try {
48-
await axios.get(`${baseURL}/test-exception`);
48+
await axios.get(`${baseURL}/test-exception/123`);
4949
} catch {
5050
// this results in an error, but we don't care - we want to check the error event
5151
}
5252

5353
const errorEvent = await errorEventPromise;
5454

5555
expect(errorEvent.exception?.values).toHaveLength(1);
56-
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception');
56+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
5757

5858
expect(errorEvent.request).toEqual({
5959
method: 'GET',
6060
cookies: {},
6161
headers: expect.any(Object),
62-
url: 'http://localhost:3030/test-exception',
62+
url: 'http://localhost:3030/test-exception/123',
6363
});
6464

65-
expect(errorEvent.transaction).toEqual('GET /test-exception');
65+
expect(errorEvent.transaction).toEqual('GET /test-exception/:id');
6666

6767
expect(errorEvent.contexts?.trace).toEqual({
6868
trace_id: expect.any(String),

dev-packages/e2e-tests/test-applications/node-fastify-app/tests/transactions.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ test('Sends an API route transaction', async ({ baseURL }) => {
5656
expect(transactionEvent).toEqual(
5757
expect.objectContaining({
5858
spans: [
59+
{
60+
data: {
61+
'plugin.name': 'fastify -> sentry-fastify-error-handler',
62+
'fastify.type': 'middleware',
63+
'hook.name': 'onRequest',
64+
'otel.kind': 'INTERNAL',
65+
'sentry.origin': 'manual',
66+
},
67+
description: 'middleware - fastify -> sentry-fastify-error-handler',
68+
parent_span_id: expect.any(String),
69+
span_id: expect.any(String),
70+
start_timestamp: expect.any(Number),
71+
status: 'ok',
72+
timestamp: expect.any(Number),
73+
trace_id: expect.any(String),
74+
origin: 'manual',
75+
},
5976
{
6077
data: {
6178
'plugin.name': 'fastify -> sentry-fastify-error-handler',

packages/node/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 { captureException, defineIntegration } from '@sentry/core';
3+
import { captureException, defineIntegration, getIsolationScope } from '@sentry/core';
44
import type { IntegrationFn } from '@sentry/types';
55

66
import { addOriginToSpan } from '../../utils/addOriginToSpan';
@@ -35,6 +35,19 @@ interface Fastify {
3535
addHook: (hook: string, handler: (request: unknown, reply: unknown, error: Error) => void) => void;
3636
}
3737

38+
/**
39+
* Minimal request type containing properties around route information.
40+
* Works for Fastify 3, 4 and presumably 5.
41+
*/
42+
interface FastifyRequestRouteInfo {
43+
44+
routeOptions?: {
45+
url?: string;
46+
method?: string;
47+
};
48+
routerPath?: string;
49+
}
50+
3851
/**
3952
* Setup an error handler for Fastify.
4053
*/
@@ -45,6 +58,20 @@ export function setupFastifyErrorHandler(fastify: Fastify): void {
4558
captureException(error);
4659
});
4760

61+
// registering `onRequest` hook here instead of using Otel `onRequest` callback b/c `onRequest` hook
62+
// is ironically called in the fastify `preHandler` hook which is called later in the lifecycle:
63+
// https://fastify.dev/docs/latest/Reference/Lifecycle/
64+
fastify.addHook('onRequest', async (request, _reply) => {
65+
const reqWithRouteInfo = request as FastifyRequestRouteInfo;
66+
67+
// Taken from Otel Fastify instrumentation:
68+
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96
69+
const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath;
70+
const method = reqWithRouteInfo.routeOptions?.method || 'GET';
71+
72+
getIsolationScope().setTransactionName(`${method} ${routeName}`);
73+
});
74+
4875
done();
4976
},
5077
{

0 commit comments

Comments
 (0)