8000 feat(nestjs): Automatic instrumentation of nestjs interceptors before… · benjick/sentry-javascript@964d050 · GitHub
[go: up one dir, main page]

Skip to content

Commit 964d050

Browse files
authored
feat(nestjs): Automatic instrumentation of nestjs interceptors before route execution (getsentry#13153)
Adds automatic instrumentation of interceptors to `@sentry/nestjs`. Interceptors in nest have a `@Injectable` decorator and implement a `intercept` function. So we can simply extend the existing instrumentation to add a proxy for `intercept`. Remark: Interceptors allow users to add functionality before and after a route handler is called. This PR adds tracing to whatever happens before the route is executed. I am still figuring out how to trace any instructions after the route was executed. Will do that in a separate PR.
1 parent 98160a5 commit 964d050

File tree

10 files changed

+288
-56
lines changed

10 files changed

+288
-56
lines changed

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

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

56
@Controller()
67
export class AppController {
@@ -13,7 +14,7 @@ export class AppController {
1314

1415
@Get('test-middleware-instrumentation')
1516
testMiddlewareInstrumentation() {
16-
return this.appService.testMiddleware();
17+
return this.appService.testSpan();
1718
}
1819

1920
@Get('test-guard-instrumentation')
@@ -22,6 +23,12 @@ export class AppController {
2223
return {};
2324
}
2425

26+
@Get('test-interceptor-instrumentation')
27+
@UseInterceptors(ExampleInterceptor)
28+
testInterceptorInstrumentation() {
29+
return this.appService.testSpan();
30+
}
31+
2532
@Get('test-pipe-instrumentation/:id')
2633
testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
2734
return { value: id };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class AppService {
2121
});
2222
}
2323

24-
testMiddleware() {
24+
testSpan() {
2525
// span that should not be a child span of the middleware span
2626
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
2727
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2+
import * as Sentry from '@sentry/nestjs';
3+
4+
@Injectable()
5+
export class ExampleInterceptor implements NestInterceptor {
6+
intercept(context: ExecutionContext, next: CallHandler) {
7+
Sentry.startSpan({ name: 'test-interceptor-span' }, () => {});
8+
return next.handle().pipe();
9+
}
10+
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,83 @@ test('API route transaction includes nest pipe span for invalid request', async
338338
}),
339339
);
340340
});
341+
342+
test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({
343+
baseURL,
344+
}) => {
345+
const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
346+
return (
347+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
348+
transactionEvent?.transaction === 'GET /test-interceptor-instrumentation'
349+
);
350+
});
351+
352+
const response = await fetch(`${baseURL}/test-interceptor-instrumentation`);
353+
expect(response.status).toBe(200);
354+
355+
const transactionEvent = await pageloadTransactionEventPromise;
356+
357+
expect(transactionEvent).toEqual(
358+
expect.objectContaining({
359+
spans: expect.arrayContaining([
360+
{
361+
span_id: expect.any(String),
362+
trace_id: expect.any(String),
363+
data: {
364+
'sentry.op': 'middleware.nestjs',
365+
'sentry.origin': 'auto.middleware.nestjs',
366+
},
367+
description: 'ExampleInterceptor',
368+
parent_span_id: expect.any(String),
369+
start_timestamp: expect.any(Number),
370+
timestamp: expect.any(Number),
371+
status: 'ok',
372+
op: 'middleware.nestjs',
373+
origin: 'auto.middleware.nestjs',
374+
},
375+
]),
376+
}),
377+
);
378+
379+
const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor');
380+
const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id;
381+
382+
expect(transactionEvent).toEqual(
383+
expect.objectContaining({
384+
spans: expect.arrayContaining([
385+
{
386+
span_id: expect.any(String),
387+
trace_id: expect.any(String),
388+
data: expect.any(Object),
389+
description: 'test-controller-span',
390+
parent_span_id: expect.any(String),
391+
start_timestamp: expect.any(Number),
392+
timestamp: expect.any(Number),
393+
status: 'ok',
394+
origin: 'manual',
395+
},
396+
{
397+
span_id: expect.any(String),
398+
trace_id: expect.any(String),
399+
data: expect.any(Object),
400+
description: 'test-interceptor-span',
401+
parent_span_id: expect.any(String),
402+
start_timestamp: expect.any(Number),
403+
timestamp: expect.any(Number),
404+
status: 'ok',
405+
origin: 'manual',
406+
},
407+
]),
408+
}),
409+
);
410+
411+
// verify correct span parent-child relationships
412+
const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span');
413+
const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
414+
415+
// 'ExampleInterceptor' is the parent of 'test-interceptor-span'
416+
expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId);
417+
418+
// 'ExampleInterceptor' is NOT the parent of 'test-controller-span'
419+
expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId);
420+
});

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

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

56
@Controller()
67
export class AppController {
@@ -13,7 +14,7 @@ export class AppController {
1314

1415
@Get('test-middleware-instrumentation')
1516
testMiddlewareInstrumentation() {
16-
return this.appService.testMiddleware();
17+
return this.appService.testSpan();
1718
}
1819

1920
@Get('test-guard-instrumentation')
@@ -22,6 +23,12 @@ export class AppController {
2223
return {};
2324
}
2425

26+
@Get('test-interceptor-instrumentation')
27+
@UseInterceptors(ExampleInterceptor)
28+
testInterceptorInstrumentation() {
29+
return this.appService.testSpan();
30+
}
31+
2532
@Get('test-pipe-instrumentation/:id')
2633
testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
2734
return { value: id };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ expor B41A t class AppService {
2121
});
2222
}
2323

24-
testMiddleware() {
24+
testSpan() {
2525
// span that should not be a child span of the middleware span
2626
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
2727
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2+
import * as Sentry from '@sentry/nestjs';
3+
4+
@Injectable()
5+
export class ExampleInterceptor implements NestInterceptor {
6+
intercept(context: ExecutionContext, next: CallHandler) {
7+
Sentry.startSpan({ name: 'test-interceptor-span' }, () => {});
8+
return next.handle().pipe();
9+
}
10+
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,83 @@ test('API route transaction includes nest pipe span for invalid request', async
338338
}),
339339
);
340340
< F438 span class=pl-kos>});
341+
342+
test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({
343+
baseURL,
344+
}) => {
345+
const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
346+
return (
347+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
348+
transactionEvent?.transaction === 'GET /test-interceptor-instrumentation'
349+
);
350+
});
351+
352+
const response = await fetch(`${baseURL}/test-interceptor-instrumentation`);
353+
expect(response.status).toBe(200);
354+
355+
const transactionEvent = await pageloadTransactionEventPromise;
356+
357+
expect(transactionEvent).toEqual(
358+
expect.objectContaining({
359+
spans: expect.arrayContaining([
360+
{
361+
span_id: expect.any(String),
362+
trace_id: expect.any(String),
363+
data: {
364+
'sentry.op': 'middleware.nestjs',
365+
'sentry.origin': 'auto.middleware.nestjs',
366+
},
367+
description: 'ExampleInterceptor',
368+
parent_span_id: expect.any(String),
369+
start_timestamp: expect.any(Number),
370+
timestamp: expect.any(Number),
371+
status: 'ok',
372+
op: 'middleware.nestjs',
373+
origin: 'auto.middleware.nestjs',
374+
},
375+
]),
376+
}),
377+
);
378+
379+
const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor');
380+
const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id;
381+
382+
expect(transactionEvent).toEqual(
383+
expect.objectContaining({
384+
spans: expect.arrayContaining([
385+
{
386+
span_id: expect.any(String),
387+
trace_id: expect.any(String),
388+
data: expect.any(Object),
389+
description: 'test-controller-span',
390+
parent_span_id: expect.any(String),
391+
start_timestamp: expect.any(Number),
392+
timestamp: expect.any(Number),
393+
status: 'ok',
394+
origin: 'manual',
395+
},
396+
{
397+
span_id: expect.any(String),
398+
trace_id: expect.any(String),
399+
data: expect.any(Object),
400+
description: 'test-interceptor-span',
401+
parent_span_id: expect.any(String),
402+
start_timestamp: expect.any(Number),
403+
timestamp: expect.any(Number),
404+
status: 'ok',
405+
origin: 'manual',
406+
},
407+
]),
408+
}),
409+
);
410+
411+
// verify correct span parent-child relationships
412+
const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span');
413+
const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
414+
415+
// 'ExampleInterceptor' is the parent of 'test-interceptor-span'
416+
expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId);
417+
418+
// 'ExampleInterceptor' is NOT the parent of 'test-controller-span'
419+
expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId);
420+
});

packages/nestjs/src/setup.ts

Expand all lines: packages/nestjs/src/setup.ts
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import type { Observable } from 'rxjs';
3232
* Interceptor to add Sentry tracing capabilities to Nest.js applications.
3333
*/
3434
class SentryTracingInterceptor implements NestInterceptor {
35+
// used to exclude this class from being auto-instrumented
36+
public static readonly __SENTRY_INTERNAL__ = true;
37+
3538
/**
3639
* Intercepts HTTP requests to set the transaction name for Sentry tracing.
3740
*/

0 commit comments

Comments
 (0)
0