Skip to content

Commit 0896e4f

Browse files
authored
feat(nestjs): Update scope transaction name with parameterized route (#11510)
This PR adds scope transactionName updating to our NestJS instrumentation. Similarly to Hapi and Fastify, we can use a framework-native component, an Interceptor, to assign the parameterized route. ref #10846
1 parent 1067868 commit 0896e4f

File tree

13 files changed

+365
-14
lines changed

13 files changed

+365
-14
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ export class AppController1 {
4040
return this.appService.testError();
4141
}
4242

43-
@Get('test-exception')
44-
async testException() {
45-
return this.appService.testException();
43+
@Get('test-exception/:id')
44+
async testException(@Param('id') id: string) {
45+
return this.appService.testException(id);
4646
}
4747

4848
@Get('test-outgoing-fetch-external-allowed')

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export class AppService1 {
4848
return { exceptionId };
4949
}
5050

51-
testException() {
52-
throw new Error('This is an exception');
51+
testException(id: string) {
52+
throw new Error(`This is an exception with id ${id}`);
5353
}
5454

5555
async testOutgoingFetchExternalAllowed() {

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

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

4444
test('Sends exception to Sentry', async ({ baseURL }) => {
4545
const errorEventPromise = waitForError('node-nestjs-app', event => {
46-
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception';
46+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
4747
});
4848

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

5555
const errorEvent = await errorEventPromise;
5656

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

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

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

6969
expect(errorEvent.contexts?.trace).toEqual({
7070
trace_id: expect.any(String),
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck These are only tests
3+
/* eslint-disable @typescript-eslint/naming-convention */
4+
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
5+
import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests';
6+
import * as Sentry from '@sentry/node';
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
release: '1.0',
11+
tracesSampleRate: 0,
12+
transport: loggingTransport,
13+
integrations: integrations => integrations.filter(i => i.name !== 'Express'),
14+
debug: true,
15+
});
16+
17+
import { Controller, Get, Injectable, Module, Param } from '@nestjs/common';
18+
import { NestFactory } from '@nestjs/core';
19+
20+
const port = 3480;
21+
22+
// Stop the process from exiting before the transaction is sent
23+
// eslint-disable-next-line @typescript-eslint/no-empty-function
24+
setInterval(() => {}, 1000);
25+
26+
@Injectable()
27+
class AppService {
28+
getHello(): string {
29+
return 'Hello World!';
30+
}
31+
}
32+
33+
@Controller()
34+
class AppController {
35+
constructor(private readonly appService: AppService) {}
36+
37+
@Get('test-exception/:id')
38+
async testException(@Param('id') id: string): void {
39+
Sentry.captureException(new Error(`error with id ${id}`));
40+
}
41+
}
42+
43+
@Module({
44+
imports: [],
45+
controllers: [AppController],
46+
providers: [AppService],
47+
})
48+
class AppModule {}
49+
50+
async function init(): Promise<void> {
51+
const app = await NestFactory.create(AppModule);
52+
Sentry.setupNestErrorHandler(app);
53+
await app.listen(port);
54+
sendPortToRunner(port);
55+
}
56+
57+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
58+
init();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { conditionalTest } from '../../../utils';
2+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
3+
4+
jest.setTimeout(20000);
5+
6+
const { TS_VERSION } = process.env;
7+
const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.');
8+
9+
// This is required to run the test with ts-node and decorators
10+
process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`;
11+
12+
conditionalTest({ min: 16 })('nestjs auto instrumentation', () => {
13+
afterAll(async () => {
14+
cleanupChildProcesses();
15+
});
16+
17+
test("should assign scope's transactionName if spans are not sampled and express integration is disabled", done => {
18+
if (isOldTS) {
19+
// Skipping test on old TypeScript
20+
return done();
21+
}
22+
23+
createRunner(__dirname, 'scenario.ts')
24+
.expect({
25+
event: {
26+
exception: {
27+
values: [
28+
{
29+
value: 'error with id 456',
30+
},
31+
],
32+
},
33+
transaction: 'GET /test-exception/:id',
34+
},
35+
})
36+
.start(done)
37+
.makeRequest('get', '/test-exception/456');
38+
});
39+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"include": ["scenario.ts"],
3+
"compilerOptions": {
4+
"module": "commonjs",
5+
"emitDecoratorMetadata": true,
6+
"experimentalDecorators": true,
7+
"target": "ES2021",
8+
}
9+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck These are only tests
3+
/* eslint-disable @typescript-eslint/naming-convention */
4+
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
5+
import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests';
6+
import * as Sentry from '@sentry/node';
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
release: '1.0',
11+
tracesSampleRate: 0,
12+
transport: loggingTransport,
13+
});
14+
15+
import { Controller, Get, Injectable, Module, Param } from '@nestjs/common';
16+
import { NestFactory } from '@nestjs/core';
17+
18+
const port = 3460;
19+
20+
// Stop the process from exiting before the transaction is sent
21+
// eslint-disable-next-line @typescript-eslint/no-empty-function
22+
setInterval(() => {}, 1000);
23+
24+
@Injectable()
25+
class AppService {
26+
getHello(): string {
27+
return 'Hello World!';
28+
}
29+
}
30+
31+
@Controller()
32+
class AppController {
33+
constructor(private readonly appService: AppService) {}
34+
35+
@Get('test-exception/:id')
36+
async testException(@Param('id') id: string): void {
37+
Sentry.captureException(new Error(`error with id ${id}`));
38+
}
39+
}
40+
41+
@Module({
42+
imports: [],
43+
controllers: [AppController],
44+
providers: [AppService],
45+
})
46+
class AppModule {}
47+
48+
async function init(): Promise<void> {
49+
const app = await NestFactory.create(AppModule);
50+
Sentry.setupNestErrorHandler(app);
51+
await app.listen(port);
52+
sendPortToRunner(port);
53+
}
54+
55+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
56+
init();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { conditionalTest } from '../../../utils';
2+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
3+
4+
jest.setTimeout(20000);
5+
6+
const { TS_VERSION } = process.env;
7+
const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.');
8+
9+
// This is required to run the test with ts-node and decorators
10+
process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`;
11+
12+
conditionalTest({ min: 16 })('nestjs auto instrumentation', () => {
13+
afterAll(async () => {
14+
cleanupChildProcesses();
15+
});
16+
17+
test("should assign scope's transactionName if spans are not sampled", done => {
18+
if (isOldTS) {
19+
// Skipping test on old TypeScript
20+
return done();
21+
}
22+
23+
createRunner(__dirname, 'scenario.ts')
24+
.expect({
25+
event: {
26+
exception: {
27+
values: [
28+
{
29+
value: 'error with id 123',
30+
},
31+
],
32+
},
33+
transaction: 'GET /test-exception/:id',
34+
},
35+
})
36+
.start(done)
37+
.makeRequest('get', '/test-exception/123');
38+
});
39+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"include": ["scenario.ts"],
3+
"compilerOptions": {
4+
"module": "commonjs",
5+
"emitDecoratorMetadata": true,
6+
"experimentalDecorators": true,
7+
"target": "ES2021",
8+
}
9+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-nocheck These are only tests
3+
/* eslint-disable @typescript-eslint/naming-convention */
4+
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
5+
import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests';
6+
import * as Sentry from '@sentry/node';
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
release: '1.0',
11+
tracesSampleRate: 1,
12+
transport: loggingTransport,
13+
integrations: integrations => integrations.filter(i => i.name !== 'Express'),
14+
debug: true,
15+
});
16+
17+
import { Controller, Get, Injectable, Module, Param } from '@nestjs/common';
18+
import { NestFactory } from '@nestjs/core';
19+
20+
const port = 3470;
21+
22+
// Stop the process from exiting before the transaction is sent
23+
// eslint-disable-next-line @typescript-eslint/no-empty-function
24+
setInterval(() => {}, 1000);
25+
26+
@Injectable()
27+
class AppService {
28+
getHello(): string {
29+
return 'Hello World!';
30+
}
31+
}
32+
33+
@Controller()
34+
class AppController {
35+
constructor(private readonly appService: AppService) {}
36+
37+
@Get('test-exception/:id')
38+
async testException(@Param('id') id: string): void {
39+
Sentry.captureException(new Error(`error with id ${id}`));
40+
}
41+
}
42+
43+
@Module({
44+
imports: [],
45+
controllers: [AppController],
46+
providers: [AppService],
47+
})
48+
class AppModule {}
49+
50+
async function init(): Promise<void> {
51+
const app = await NestFactory.create(AppModule);
52+
Sentry.setupNestErrorHandler(app);
53+
await app.listen(port);
54+
sendPortToRunner(port);
55+
}
56+
57+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
58+
init();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { conditionalTest } from '../../../utils';
2+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
3+
4+
jest.setTimeout(20000);
5+
6+
const { TS_VERSION } = process.env;
7+
const isOldTS = TS_VERSION && TS_VERSION.startsWith('3.');
8+
9+
// This is required to run the test with ts-node and decorators
10+
process.env.TS_NODE_PROJECT = `${__dirname}/tsconfig.json`;
11+
12+
conditionalTest({ min: 16 })('nestjs auto instrumentation', () => {
13+
afterAll(async () => {
14+
cleanupChildProcesses();
15+
});
16+
17+
test("should assign scope's transactionName if express integration is disabled", done => {
18+
if (isOldTS) {
19+
// Skipping test on old TypeScript
20+
return done();
21+
}
22+
23+
createRunner(__dirname, 'scenario.ts')
24+
.ignore('transaction')
25+
.expect({
26+
event: {
27+
exception: {
28+
values: [
29+
{
30+
value: 'error with id 456',
31+
},
32+
],
33+
},
34+
transaction: 'GET /test-exception/:id',
35+
},
36+
})
37+
.start(done)
38+
.makeRequest('get', '/test-exception/456');
39+
});
40+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"include": ["scenario.ts"],
3+
"compilerOptions": {
4+
"module": "commonjs",
5+
"emitDecoratorMetadata": true,
6+
"experimentalDecorators": true,
7+
"target": "ES2021",
8+
}
9+
}

0 commit comments

Comments
 (0)