Skip to content

Commit e3af1ce

Browse files
authored
feat(nestjs): Automatic instrumentation of nestjs middleware (#13065)
Adds middleware instrumentation to the `@sentry/nestjs`. The implementation lives in `@sentry/node` so that both users using `@sentry/nestjs` directly as well as users still on `@sentry/node` benefit. The instrumentation is automatic without requiring any additional setup. The idea is to hook into the Injectable decorator (every class middleware is annotated with `@Injectable` and patch the `use` method if it is implemented. Caveat: This implementation only works for class middleware, which implements the `use` method, which seems to be the standard for implementing middleware in nest. However, nest also provides functional middleware, for which this implementation does not work.
1 parent b7e62c4 commit e3af1ce

File tree

12 files changed

+394
-6
lines changed

12 files changed

+394
-6
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export class AppController {
1010
return this.appService.testTransaction();
1111
}
1212

13+
@Get('test-middleware-instrumentation')
14+
testMiddlewareInstrumentation() {
15+
return this.appService.testMiddleware();
16+
}
17+
1318
@Get('test-exception/:id')
1419
async testException(@Param('id') id: string) {
1520
return this.appService.testException(id);
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { Module } from '@nestjs/common';
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
22
import { ScheduleModule } from '@nestjs/schedule';
33
import { SentryModule } from '@sentry/nestjs/setup';
44
import { AppController } from './app.controller';
55
import { AppService } from './app.service';
6+
import { ExampleMiddleware } from './example.middleware';
67

78
@Module({
89
imports: [SentryModule.forRoot(), ScheduleModule.forRoot()],
910
controllers: [AppController],
1011
providers: [AppService],
1112
})
12-
export class AppModule {}
13+
export class AppModule {
14+
configure(consumer: MiddlewareConsumer): void {
15+
consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
16+
}
17+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export class AppService {
2121
});
2222
}
2323

24+
testMiddleware() {
25+
// span that should not be a child span of the middleware span
26+
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
27+
}
28+
2429
testException(id: string) {
2530
throw new Error(`This is an exception with id ${id}`);
2631
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import * as Sentry from '@sentry/nestjs';
3+
import { NextFunction, Request, Response } from 'express';
4+
5+
@Injectable()
6+
export class ExampleMiddleware implements NestMiddleware {
7+
use(req: Request, res: Response, next: NextFunction) {
8+
// span that should be a child span of the middleware span
9+
Sentry.startSpan({ name: 'test-middleware-span' }, () => {});
10+
next();
11+
}
12+
}

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,82 @@ test('Sends an API route transaction', async ({ baseURL }) => {
121121
}),
122122
);
123123
});
124+
125+
test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({
126+
baseURL,
127+
}) => {
128+
const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
129+
return (
130+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
131+
transactionEvent?.transaction === 'GET /test-middleware-instrumentation'
132+
);
133+
});
134+
135+
await fetch(`${baseURL}/test-middleware-instrumentation`);
136+
137+
const transactionEvent = await pageloadTransactionEventPromise;
138+
139+
expect(transactionEvent).toEqual(
140+
expect.objectContaining({
141+
spans: expect.arrayContaining([
142+
{
143+
span_id: expect.any(String),
144+
trace_id: expect.any(String),
145+
data: {
146+
'sentry.op': 'middleware.nestjs',
147+
'sentry.origin': 'auto.middleware.nestjs',
148+
},
149+
description: 'ExampleMiddleware',
150+
parent_span_id: expect.any(String),
151+
start_timestamp: expect.any(Number),
152+
timestamp: expect.any(Number),
153+
status: 'ok',
154+
op: 'middleware.nestjs',
155+
origin: 'auto.middleware.nestjs',
156+
},
157+
]),
158+
}),
159+
);
160+
161+
const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware');
162+
const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id;
163+
164+
expect(transactionEvent).toEqual(
165+
expect.objectContaining({
166+
spans: expect.arrayContaining([
167+
{
168+
span_id: expect.any(String),
169+
trace_id: expect.any(String),
170+
data: expect.any(Object),
171+
description: 'test-controller-span',
172+
parent_span_id: expect.any(String),
173+
start_timestamp: expect.any(Number),
174+
timestamp: expect.any(Number),
175+
status: 'ok',
176+
origin: 'manual',
177+
},
178+
{
179+
span_id: expect.any(String),
180+
trace_id: expect.any(String),
181+
data: expect.any(Object),
182+
description: 'test-middleware-span',
183+
parent_span_id: expect.any(String),
184+
start_timestamp: expect.any(Number),
185+
timestamp: expect.any(Number),
186+
status: 'ok',
187+
origin: 'manual',
188+
},
189+
]),
190+
}),
191+
);
192+
193+
// verify correct span parent-child relationships
194+
const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span');
195+
const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
196+
197+
// 'ExampleMiddleware' is the parent of 'test-middleware-span'
198+
expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId);
199+
200+
// 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
201+
expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
202+
});

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export class AppController {
1010
return this.appService.testTransaction();
1111
}
1212

13+
@Get('test-middleware-instrumentation')
14+
testMiddlewareInstrumentation() {
15+
return this.appService.testMiddleware();
16+
}
17+
1318
@Get('test-exception/:id')
1419
async testException(@Param('id') id: string) {
1520
return this.appService.testException(id);
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { Module } from '@nestjs/common';
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
22
import { ScheduleModule } from '@nestjs/schedule';
33
import { AppController } from './app.controller';
44
import { AppService } from './app.service';
5+
import { ExampleMiddleware } from './example.middleware';
56

67
@Module({
78
imports: [ScheduleModule.forRoot()],
89
controllers: [AppController],
910
providers: [AppService],
1011
})
11-
export class AppModule {}
12+
export class AppModule {
13+
configure(consumer: MiddlewareConsumer): void {
14+
consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
15+
}
16+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export class AppService {
2121
});
2222
}
2323

24+
testMiddleware() {
25+
// span that should not be a child span of the middleware span
26+
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
27+
}
28+
2429
testException(id: string) {
2530
throw new Error(`This is an exception with id ${id}`);
2631
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import * as Sentry from '@sentry/nestjs';
3+
import { NextFunction, Request, Response } from 'express';
4+
5+
@Injectable()
6+
export class ExampleMiddleware implements NestMiddleware {
7+
use(req: Request, res: Response, next: NextFunction) {
8+
// span that should be a child span of the middleware span
9+
Sentry.startSpan({ name: 'test-middleware-span' }, () => {});
10+
next();
11+
}
12+
}

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,82 @@ test('Sends an API route transaction', async ({ baseURL }) => {
121121
}),
122122
);
123123
});
124+
125+
test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({
126+
baseURL,
127+
}) => {
128+
const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
129+
return (
130+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
131+
transactionEvent?.transaction === 'GET /test-middleware-instrumentation'
132+
);
133+
});
134+
135+
await fetch(`${baseURL}/test-middleware-instrumentation`);
136+
137+
const transactionEvent = await pageloadTransactionEventPromise;
138+
139+
expect(transactionEvent).toEqual(
140+
expect.objectContaining({
141+
spans: expect.arrayContaining([
142+
{
143+
span_id: expect.any(String),
144+
trace_id: expect.any(String),
145+
data: {
146+
'sentry.op': 'middleware.nestjs',
147+
'sentry.origin': 'auto.middleware.nestjs',
148+
},
149+
description: 'ExampleMiddleware',
150+
parent_span_id: expect.any(String),
151+
start_timestamp: expect.any(Number),
152+
timestamp: expect.any(Number),
153+
status: 'ok',
154+
op: 'middleware.nestjs',
155+
origin: 'auto.middleware.nestjs',
156+
},
157+
]),
158+
}),
159+
);
160+
161+
const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware');
162+
const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id;
163+
164+
expect(transactionEvent).toEqual(
165+
expect.objectContaining({
166+
spans: expect.arrayContaining([
167+
{
168+
span_id: expect.any(String),
169+
trace_id: expect.any(String),
170+
data: expect.any(Object),
171+
description: 'test-controller-span',
172+
parent_span_id: expect.any(String),
173+
start_timestamp: expect.any(Number),
174+
timestamp: expect.any(Number),
175+
status: 'ok',
176+
origin: 'manual',
177+
},
178+
{
179+
span_id: expect.any(String),
180+
trace_id: expect.any(String),
181+
data: expect.any(Object),
182+
description: 'test-middleware-span',
183+
parent_span_id: expect.any(String),
184+
start_timestamp: expect.any(Number),
185+
timestamp: expect.any(Number),
186+
status: 'ok',
187+
origin: 'manual',
188+
},
189+
]),
190+
}),
191+
);
192+
193+
// verify correct span parent-child relationships
194+
const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span');
195+
const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
196+
197+
// 'ExampleMiddleware' is the parent of 'test-middleware-span'
198+
expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId);
199+
200+
// 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
201+
expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
202+
});

0 commit comments

Comments
 (0)