Skip to content

Commit 55cccfd

Browse files
chargomes1gr1d
andauthored
feat(nestjs): Support v11 (#15114)
Co-authored-by: s1gr1d <[email protected]> Co-authored-by: Sigrid Huemer <[email protected]>
1 parent 2f17ef1 commit 55cccfd

32 files changed

+1953
-17
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# compiled output
2+
/dist
3+
/node_modules
4+
/build
5+
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
pnpm-debug.log*
11+
yarn-debug.log*
12+
yarn-error.log*
13+
lerna-debug.log*
14+
15+
# OS
16+
.DS_Store
17+
18+
# Tests
19+
/coverage
20+
/.nyc_output
21+
22+
# IDEs and editors
23+
/.idea
24+
.project
25+
.classpath
26+
.c9/
27+
*.launch
28+
.settings/
29+
*.sublime-workspace
30+
31+
# IDE - VSCode
32+
.vscode/*
33+
!.vscode/settings.json
34+
!.vscode/tasks.json
35+
!.vscode/launch.json
36+
!.vscode/extensions.json
37+
38+
# dotenv environment variable files
39+
.env
40+
.env.development.local
41+
.env.test.local
42+
.env.production.local
43+
.env.local
44+
45+
# temp directory
46+
.temp
47+
.tmp
48+
49+
# Runtime data
50+
pids
51+
*.pid
52+
*.seed
53+
*.pid.lock
54+
55+
# Diagnostic reports (https://nodejs.org/api/report.html)
56+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "https://json.schemastore.org/nest-cli",
3+
"collection": "@nestjs/schematics",
4+
"sourceRoot": "src",
5+
"compilerOptions": {
6+
"deleteOutDir": true
7+
}
8+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "nestjs-11",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"build": "nest build",
7+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
8+
"start": "nest start",
9+
"start:dev": "nest start --watch",
10+
"start:debug": "nest start --debug --watch",
11+
"start:prod": "node dist/main",
12+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
13+
"test": "playwright test",
14+
"test:build": "pnpm install",
15+
"test:assert": "pnpm test"
16+
},
17+
"dependencies": {
18+
"@nestjs/common": "^11.0.0",
19+
"@nestjs/core": "^11.0.0",
20+
"@nestjs/microservices": "^11.0.0",
21+
"@nestjs/schedule": "^5.0.0",
22+
"@nestjs/platform-express": "^11.0.0",
23+
"@sentry/nestjs": "latest || *",
24+
"reflect-metadata": "^0.2.0",
25+
"rxjs": "^7.8.1"
26+
},
27+
"devDependencies": {
28+
"@playwright/test": "^1.44.1",
29+
"@sentry-internal/test-utils": "link:../../../test-utils",
30+
"@nestjs/cli": "^11.0.0",
31+
"@nestjs/schematics": "^11.0.0",
32+
"@nestjs/testing": "^11.0.0",
33+
"@types/express": "^4.17.17",
34+
"@types/node": "^18.19.1",
35+
"@types/supertest": "^6.0.0",
36+
"@typescript-eslint/eslint-plugin": "^6.0.0",
37+
"@typescript-eslint/parser": "^6.0.0",
38+
"eslint": "^8.42.0",
39+
"eslint-config-prettier": "^9.0.0",
40+
"eslint-plugin-prettier": "^5.0.0",
41+
"prettier": "^3.0.0",
42+
"source-map-support": "^0.5.21",
43+
"supertest": "^6.3.3",
44+
"ts-loader": "^9.4.3",
45+
"tsconfig-paths": "^4.2.0",
46+
"typescript": "~5.0.0"
47+
}
48+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
2+
import { flush } from '@sentry/nestjs';
3+
import { AppService } from './app.service';
4+
import { AsyncInterceptor } from './async-example.interceptor';
5+
import { ExampleInterceptor1 } from './example-1.interceptor';
6+
import { ExampleInterceptor2 } from './example-2.interceptor';
7+
import { ExampleExceptionGlobalFilter } from './example-global-filter.exception';
8+
import { ExampleExceptionLocalFilter } from './example-local-filter.exception';
9+
import { ExampleLocalFilter } from './example-local.filter';
10+
import { ExampleGuard } from './example.guard';
11+
12+
@Controller()
13+
@UseFilters(ExampleLocalFilter)
14+
export class AppController {
15+
constructor(private readonly appService: AppService) {}
16+
17+
@Get('test-transaction')
18+
testTransaction() {
19+
return this.appService.testTransaction();
20+
}
21+
22+
@Get('test-middleware-instrumentation')
23+
testMiddlewareInstrumentation() {
24+
return this.appService.testSpan();
25+
}
26+
27+
@Get('test-guard-instrumentation')
28+
@UseGuards(ExampleGuard)
29+
testGuardInstrumentation() {
30+
return {};
31+
}
32+
33+
@Get('test-interceptor-instrumentation')
34+
@UseInterceptors(ExampleInterceptor1, ExampleInterceptor2)
35+
testInterceptorInstrumentation() {
36+
return this.appService.testSpan();
37+
}
38+
39+
@Get('test-async-interceptor-instrumentation')
40+
@UseInterceptors(AsyncInterceptor)
41+
testAsyncInterceptorInstrumentation() {
42+
return this.appService.testSpan();
43+
}
44+
45+
@Get('test-pipe-instrumentation/:id')
46+
testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
47+
return { value: id };
48+
}
49+
50+
@Get('test-exception/:id')
51+
async testException(@Param('id') id: string) {
52+
return this.appService.testException(id);
53+
}
54+
55+
@Get('test-expected-400-exception/:id')
56+
async testExpected400Exception(@Param('id') id: string) {
57+
return this.appService.testExpected400Exception(id);
58+
}
59+
60+
@Get('test-expected-500-exception/:id')
61+
async testExpected500Exception(@Param('id') id: string) {
62+
return this.appService.testExpected500Exception(id);
63+
}
64+
65+
@Get('test-expected-rpc-exception/:id')
66+
async testExpectedRpcException(@Param('id') id: string) {
67+
return this.appService.testExpectedRpcException(id);
68+
}
69+
70+
@Get('test-span-decorator-async')
71+
async testSpanDecoratorAsync() {
72+
return { result: await this.appService.testSpanDecoratorAsync() };
73+
}
74+
75+
@Get('test-span-decorator-sync')
76+
async testSpanDecoratorSync() {
77+
return { result: await this.appService.testSpanDecoratorSync() };
78+
}
79+
80+
@Get('kill-test-cron/:job')
81+
async killTestCron(@Param('job') job: string) {
82+
this.appService.killTestCron(job);
83+
}
84+
85+
@Get('flush')
86+
async flush() {
87+
await flush();
88+
}
89+
90+
@Get('example-exception-global-filter')
91+
async exampleExceptionGlobalFilter() {
92+
throw new ExampleExceptionGlobalFilter();
93+
}
94+
95+
@Get('example-exception-local-filter')
96+
async exampleExceptionLocalFilter() {
97+
throw new ExampleExceptionLocalFilter();
98+
}
99+
100+
@Get('test-service-use')
101+
testServiceWithUseMethod() {
102+
return this.appService.use();
103+
}
104+
105+
@Get('test-service-transform')
106+
testServiceWithTransform() {
107+
return this.appService.transform();
108+
}
109+
110+
@Get('test-service-intercept')
111+
testServiceWithIntercept() {
112+
return this.appService.intercept();
113+
}
114+
115+
@Get('test-service-canActivate')
116+
testServiceWithCanActivate() {
117+
return this.appService.canActivate();
118+
}
119+
120+
@Get('test-function-name')
121+
testFunctionName() {
122+
return this.appService.getFunctionName();
123+
}
124+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
2+
import { APP_FILTER } from '@nestjs/core';
3+
import { ScheduleModule } from '@nestjs/schedule';
4+
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
5+
import { AppController } from './app.controller';
6+
import { AppService } from './app.service';
7+
import { ExampleGlobalFilter } from './example-global.filter';
8+
import { ExampleMiddleware } from './example.middleware';
9+
10+
@Module({
11+
imports: [SentryModule.forRoot(), ScheduleModule.forRoot()],
12+
controllers: [AppController],
13+
providers: [
14+
AppService,
15+
{
16+
provide: APP_FILTER,
17+
useClass: SentryGlobalFilter,
18+
},
19+
{
20+
provide: APP_FILTER,
21+
useClass: ExampleGlobalFilter,
22+
},
23+
],
24+
})
25+
export class AppModule {
26+
configure(consumer: MiddlewareConsumer): void {
27+
consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
28+
}
29+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
import { RpcException } from '@nestjs/microservices';
3+
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
4+
import type { MonitorConfig } from '@sentry/core';
5+
import * as Sentry from '@sentry/nestjs';
6+
import { SentryCron, SentryTraced } from '@sentry/nestjs';
7+
8+
const monitorConfig: MonitorConfig = {
9+
schedule: {
10+
type: 'crontab',
11+
value: '* * * * *',
12+
},
13+
};
14+
15+
@Injectable()
16+
export class AppService {
17+
constructor(private schedulerRegistry: SchedulerRegistry) {}
18+
19+
testTransaction() {
20+
Sentry.startSpan({ name: 'test-span' }, () => {
21+
Sentry.startSpan({ name: 'child-span' }, () => {});
22+
});
23+
}
24+
25+
testSpan() {
26+
// span that should not be a child span of the middleware span
27+
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
28+
}
29+
30+
testException(id: string) {
31+
throw new Error(`This is an exception with id ${id}`);
32+
}
33+
34+
testExpected400Exception(id: string) {
35+
throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST);
36+
}
37+
38+
testExpected500Exception(id: string) {
39+
throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
40+
}
41+
42+
testExpectedRpcException(id: string) {
43+
throw new RpcException(`This is an expected RPC exception with id ${id}`);
44+
}
45+
46+
@SentryTraced('wait and return a string')
47+
async wait() {
48+
await new Promise(resolve => setTimeout(resolve, 500));
49+
return 'test';
50+
}
51+
52+
async testSpanDecoratorAsync() {
53+
return await this.wait();
54+
}
55+
56+
@SentryTraced('return a string')
57+
getString(): { result: string } {
58+
return { result: 'test' };
59+
}
60+
61+
@SentryTraced('return the function name')
62+
getFunctionName(): { result: string } {
63+
return { result: this.getFunctionName.name };
64+
}
65+
66+
async testSpanDecoratorSync() {
67+
const returned = this.getString();
68+
// Will fail if getString() is async, because returned will be a Promise<>
69+
return returned.result;
70+
}
71+
72+
/*
73+
Actual cron schedule differs from schedule defined in config because Sentry
74+
only supports minute granularity, but we don't want to wait (worst case) a
75+
full minute for the tests to finish.
76+
*/
77+
@Cron('*/5 * * * * *', { name: 'test-cron-job' })
78+
@SentryCron('test-cron-slug', monitorConfig)
79+
async testCron() {
80+
console.log('Test cron!');
81+
}
82+
83+
/*
84+
Actual cron schedule differs from schedule defined in config because Sentry
85+
only supports minute granularity, but we don't want to wait (worst case) a
86+
full minute for the tests to finish.
87+
*/
88+
@Cron('*/5 * * * * *', { name: 'test-cron-error' })
89+
@SentryCron('test-cron-error-slug', monitorConfig)
90+
async testCronError() {
91+
throw new Error('Test error from cron job');
92+
}
93+
94+
async killTestCron(job: string) {
95+
this.schedulerRegistry.deleteCronJob(job);
96+
}
97+
98+
use() {
99+
console.log('Test use!');
100+
}
101+
102+
transform() {
103+
console.log('Test transform!');
104+
}
105+
106+
intercept() {
107+
console.log('Test intercept!');
108+
}
109+
110+
canActivate() {
111+
console.log('Test canActivate!');
112+
}
113+
}

0 commit comments

Comments
 (0)