Skip to content

Commit bfb7ec4

Browse files
authored
feat(nestjs): Automatic instrumentation of nestjs pipes (#13137)
Adds automatic instrumentation of pipes to `@sentry/nestjs`. Pipes in nest have a `@Injectable` decorator and implement a `transform` function. So we can simply extend the existing instrumentation to add a proxy for `transform`.
1 parent 2306458 commit bfb7ec4

File tree

5 files changed

+182
-2
lines changed

5 files changed

+182
-2
lines changed

dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
1+
import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
22
import { AppService } from './app.service';
33
import { ExampleGuard } from './example.guard';
44

@@ -22,6 +22,11 @@ export class AppController {
2222
return {};
2323
}
2424

25+
@Get('test-pipe-instrumentation/:id')
26+
testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
27+
return { value: id };
28+
}
29+
2530
@Get('test-exception/:id')
2631
async testException(@Param('id') id: string) {
2732
return this.appService.testException(id);

dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,75 @@ test('API route transaction includes nest guard span and span started in guard i
266266
// 'ExampleGuard' is the parent of 'test-guard-span'
267267
expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId);
268268
});
269+
270+
test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => {
271+
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
272+
return (
273+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
274+
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id'
275+
);
276+
});
277+
278+
const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`);
279+
expect(response.status).toBe(200);
280+
281+
const transactionEvent = await transactionEventPromise;
282+
283+
expect(transactionEvent).toEqual(
284+
expect.objectContaining({
285+
spans: expect.arrayContaining([
286+
{
287+
span_id: expect.any(String),
288+
trace_id: expect.any(String),
289+
data: {
290+
'sentry.op': 'middleware.nestjs',
291+
'sentry.origin': 'auto.middleware.nestjs',
292+
},
293+
description: 'ParseIntPipe',
294+
parent_span_id: expect.any(String),
295+
start_timestamp: expect.any(Number),
296+
timestamp: expect.any(Number),
297+
status: 'ok',
298+
op: 'middleware.nestjs',
299+
origin: 'auto.middleware.nestjs',
300+
},
301+
]),
302+
}),
303+
);
304+
});
305+
306+
test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => {
307+
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
308+
return (
309+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
310+
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id'
311+
);
312+
});
313+
314+
const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`);
315+
expect(response.status).toBe(400);
316+
317+
const transactionEvent = await transactionEventPromise;
318+
319+
expect(transactionEvent).toEqual(
320+
expect.objectContaining({
321+
spans: expect.arrayContaining([
322+
{
323+
span_id: expect.any(String),
324+
trace_id: expect.any(String),
325+
data: {
326+
'sentry.op': 'middleware.nestjs',
327+
'sentry.origin': 'auto.middleware.nestjs',
328+
},
329+
description: 'ParseIntPipe',
330+
parent_span_id: expect.any(String),
331+
start_timestamp: expect.any(Number),
332+
timestamp: expect.any(Number),
333+
status: 'unknown_error',
334+
op: 'middleware.nestjs',
335+
origin: 'auto.middleware.nestjs',
336+
},
337+
]),
338+
}),
339+
);
340+
});

dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
1+
import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common';
22
import { AppService } from './app.service';
33
import { ExampleGuard } from './example.guard';
44

@@ -22,6 +22,11 @@ export class AppController {
2222
return {};
2323
}
2424

25+
@Get('test-pipe-instrumentation/:id')
26+
testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
27+
return { value: id };
28+
}
29+
2530
@Get('test-exception/:id')
2631
async testException(@Param('id') id: string) {
2732
return this.appService.testException(id);

dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,75 @@ test('API route transaction includes nest guard span and span started in guard i
266266
// 'ExampleGuard' is the parent of 'test-guard-span'
267267
expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId);
268268
});
269+
270+
test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => {
271+
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
272+
return (
273+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
274+
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id'
275+
);
276+
});
277+
278+
const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`);
279+
expect(response.status).toBe(200);
280+
281+
const transactionEvent = await transactionEventPromise;
282+
283+
expect(transactionEvent).toEqual(
284+
expect.objectContaining({
285+
spans: expect.arrayContaining([
286+
{
287+
span_id: expect.any(String),
288+
trace_id: expect.any(String),
289+
data: {
290+
'sentry.op': 'middleware.nestjs',
291+
'sentry.origin': 'auto.middleware.nestjs',
292+
},
293+
description: 'ParseIntPipe',
294+
parent_span_id: expect.any(String),
295+
start_timestamp: expect.any(Number),
296+
timestamp: expect.any(Number),
297+
status: 'ok',
298+
op: 'middleware.nestjs',
299+
origin: 'auto.middleware.nestjs',
300+
},
301+
]),
302+
}),
303+
);
304+
});
305+
306+
test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => {
307+
const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
308+
return (
309+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
310+
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id'
311+
);
312+
});
313+
314+
const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`);
315+
expect(response.status).toBe(400);
316+
317+
const transactionEvent = await transactionEventPromise;
318+
319+
expect(transactionEvent).toEqual(
320+
expect.objectContaining({
321+
spans: expect.arrayContaining([
322+
{
323+
span_id: expect.any(String),
324+
trace_id: expect.any(String),
325+
data: {
326+
'sentry.op': 'middleware.nestjs',
327+
'sentry.origin': 'auto.middleware.nestjs',
328+
},
329+
description: 'ParseIntPipe',
330+
parent_span_id: expect.any(String),
331+
start_timestamp: expect.any(Number),
332+
timestamp: expect.any(Number),
333+
status: 'unknown_error',
334+
op: 'middleware.nestjs',
335+
origin: 'auto.middleware.nestjs',
336+
},
337+
]),
338+
}),
339+
);
340+
});

packages/node/src/integrations/tracing/nest.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export interface InjectableTarget {
7272
use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void;
7373
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7474
canActivate?: (...args: any[]) => boolean | Promise<boolean> | Observable<boolean>;
75+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76+
transform?: (...args: any[]) => any;
7577
};
7678
}
7779

@@ -212,6 +214,30 @@ export class SentryNestInstrumentation extends InstrumentationBase {
212214
});
213215
}
214216

217+
// patch pipes
218+
if (typeof target.prototype.transform === 'function') {
219+
if (isPatched(target)) {
220+
return original(options)(target);
221+
}
222+
223+
target.prototype.transform = new Proxy(target.prototype.transform, {
224+
apply: (originalTransform, thisArgTransform, argsTransform) => {
225+
return startSpan(
226+
{
227+
name: target.name,
228+
attributes: {
229+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs',
230+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs',
231+
},
232+
},
233+
() => {
234+
return originalTransform.apply(thisArgTransform, argsTransform);
235+
},
236+
);
237+
},
238+
});
239+
}
240+
215241
return original(options)(target);
216242
};
217243
};

0 commit comments

Comments
 (0)