Skip to content

Commit 0c06d16

Browse files
nicohrubecZen-cronic
authored andcommitted
fix(nestjs): Exception filters in main app module are not being executed (getsentry#13278)
[ref](getsentry#12351) This PR moves the `SentryGlobalFilter` out of the root module, which led to the filter overriding user exception filters in certain scenarios. Now there is two ways to setup sentry error monitoring: - If users have no catch-all exception filter in their application they add the `SentryGlobalFilter` as a provider in their main module. - If users have a catch-all exception filter (annotated with `@Catch()` they can use the newly introduced `SentryCaptureException()` decorator to capture alle exceptions caught by this filter. Testing: Added a new sample application to test the second setup option and expanded the test suite in general. Side note: - Also removed the `@sentry/microservices` dependency again, since it does not come out of the box with every nest application so we cannot rely on it.
1 parent 69f52db commit 0c06d16

File tree

60 files changed

+1479
-77
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1479
-77
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common';
1+
import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
22
import { flush } from '@sentry/nestjs';
33
import { AppService } from './app.service';
4+
import { ExampleExceptionGlobalFilter } from './example-global-filter.exception';
5+
import { ExampleExceptionLocalFilter } from './example-local-filter.exception';
6+
import { ExampleLocalFilter } from './example-local.filter';
47
import { ExampleGuard } from './example.guard';
58
import { ExampleInterceptor } from './example.interceptor';
69

710
@Controller()
11+
@UseFilters(ExampleLocalFilter)
812
export class AppController {
913
constructor(private readonly appService: AppService) {}
1014

@@ -74,4 +78,14 @@ export class AppController {
7478
async flush() {
7579
await flush();
7680
}
81+
82+
@Get('example-exception-global-filter')
83+
async exampleExceptionGlobalFilter() {
84+
throw new ExampleExceptionGlobalFilter();
85+
}
86+
87+
@Get('example-exception-local-filter')
88+
async exampleExceptionLocalFilter() {
89+
throw new ExampleExceptionLocalFilter();
90+
}
7791
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import { MiddlewareConsumer, Module } from '@nestjs/common';
2+
import { APP_FILTER } from '@nestjs/core';
23
import { ScheduleModule } from '@nestjs/schedule';
3-
import { SentryModule } from '@sentry/nestjs/setup';
4+
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
45
import { AppController } from './app.controller';
56
import { AppService } from './app.service';
7+
import { ExampleGlobalFilter } from './example-global.filter';
68
import { ExampleMiddleware } from './example.middleware';
79

810
@Module({
911
imports: [SentryModule.forRoot(), ScheduleModule.forRoot()],
1012
controllers: [AppController],
11-
providers: [AppService],
13+
providers: [
14+
AppService,
15+
{
16+
provide: APP_FILTER,
17+
useClass: SentryGlobalFilter,
18+
},
19+
{
20+
provide: APP_FILTER,
21+
useClass: ExampleGlobalFilter,
22+
},
23+
],
1224
})
1325
export class AppModule {
1426
configure(consumer: MiddlewareConsumer): void {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class ExampleExceptionGlobalFilter extends Error {
2+
constructor() {
3+
super('Original global example exception!');
4+
}
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
2+
import { Request, Response } from 'express';
3+
import { ExampleExceptionGlobalFilter } from './example-global-filter.exception';
4+
5+
@Catch(ExampleExceptionGlobalFilter)
6+
export class ExampleGlobalFilter implements ExceptionFilter {
7+
catch(exception: BadRequestException, host: ArgumentsHost): void {
8+
const ctx = host.switchToHttp();
9+
const response = ctx.getResponse<Response>();
10+
const request = ctx.getRequest<Request>();
11+
12+
response.status(400).json({
13+
statusCode: 400,
14+
timestamp: new Date().toISOString(),
15+
path: request.url,
16+
message: 'Example exception was handled by global filter!',
17+
});
18+
}
19+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class ExampleExceptionLocalFilter extends Error {
2+
constructor() {
3+
super('Original local example exception!');
4+
}
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
2+
import { Request, Response } from 'express';
3+
import { ExampleExceptionLocalFilter } from './example-local-filter.exception';
4+
5+
@Catch(ExampleExceptionLocalFilter)
6+
export class ExampleLocalFilter implements ExceptionFilter {
7+
catch(exception: BadRequestException, host: ArgumentsHost): void {
8+
const ctx = host.switchToHttp();
9+
const response = ctx.getResponse<Response>();
10+
const request = ctx.getRequest<Request>();
11+
12+
response.status(400).json({
13+
statusCode: 400,
14+
timestamp: new Date().toISOString(),
15+
path: request.url,
16+
message: 'Example exception was handled by local filter!',
17+
});
18+
}
19+
}

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,73 @@ test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => {
9494

9595
expect(errorEventOccurred).toBe(false);
9696
});
97+
98+
test('Global exception filter registered in main module is applied and exception is not sent to Sentry', async ({
99+
baseURL,
100+
}) => {
101+
let errorEventOccurred = false;
102+
103+
waitForError('nestjs-basic', event => {
104+
if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') {
105+
errorEventOccurred = true;
106+
}
107+
108+
return event?.transaction === 'GET /example-exception-global-filter';
109+
});
110+
111+
const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => {
112+
return transactionEvent?.transaction === 'GET /example-exception-global-filter';
113+
});
114+
115+
const response = await fetch(`${baseURL}/example-exception-global-filter`);
116+
const responseBody = await response.json();
117+
118+
expect(response.status).toBe(400);
119+
expect(responseBody).toEqual({
120+
statusCode: 400,
121+
timestamp: expect.any(String),
122+
path: '/example-exception-global-filter',
123+
message: 'Example exception was handled by global filter!',
124+
});
125+
126+
await transactionEventPromise;
127+
128+
(await fetch(`${baseURL}/flush`)).text();
129+
130+
expect(errorEventOccurred).toBe(false);
131+
});
132+
133+
test('Local exception filter registered in main module is applied and exception is not sent to Sentry', async ({
134+
baseURL,
135+
}) => {
136+
let errorEventOccurred = false;
137+
138+
waitForError('nestjs-basic', event => {
139+
if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') {
140+
errorEventOccurred = true;
141+
}
142+
143+
return event?.transaction === 'GET /example-exception-local-filter';
144+
});
145+
146+
const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => {
147+
return transactionEvent?.transaction === 'GET /example-exception-local-filter';
148+
});
149+
150+
const response = await fetch(`${baseURL}/example-exception-local-filter`);
151+
const responseBody = await response.json();
152+
153+
expect(response.status).toBe(400);
154+
expect(responseBody).toEqual({
155+
statusCode: 400,
156+
timestamp: expect.any(String),
157+
path: '/example-exception-local-filter',
158+
message: 'Example exception was handled by local filter!',
159+
});
160+
161+
await transactionEventPromise;
162+
163+
(await fetch(`${baseURL}/flush`)).text();
164+
165+
expect(errorEventOccurred).toBe(false);
166+
});
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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "nestjs-with-submodules-decorator",
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": "^10.0.0",
19+
"@nestjs/core": "^10.0.0",
20+
"@nestjs/platform-express": "^10.0.0",
21+
"@sentry/nestjs": "latest || *",
22+
"@sentry/types": "latest || *",
23+
"reflect-metadata": "^0.2.0",
24+
"rxjs": "^7.8.1"
25+
},
26+
"devDependencies": {
27+
"@playwright/test": "^1.44.1",
28+
"@sentry-internal/test-utils": "link:../../../test-utils",
29+
"@nestjs/cli": "^10.0.0",
30+
"@nestjs/schematics": "^10.0.0",
31+
"@nestjs/testing": "^10.0.0",
32+
"@types/express": "^4.17.17",
33+
"@types/node": "18.15.1",
34+
"@types/supertest": "^6.0.0",
35+
"@typescript-eslint/eslint-plugin": "^6.0.0",
36+
"@typescript-eslint/parser": "^6.0.0",
37+
"eslint": "^8.42.0",
38+
"eslint-config-prettier": "^9.0.0",
39+
"eslint-plugin-prettier": "^5.0.0",
40+
"prettier": "^3.0.0",
41+
"source-map-support": "^0.5.21",
42+
"supertest": "^6.3.3",
43+
"ts-loader": "^9.4.3",
44+
"tsconfig-paths": "^4.2.0",
45+
"typescript": "^4.9.5"
46+
}
47+
}
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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Controller, Get, Param, UseFilters } from '@nestjs/common';
2+
import { flush } from '@sentry/nestjs';
3+
import { AppService } from './app.service';
4+
import { ExampleExceptionLocalFilter } from './example-local.exception';
5+
import { ExampleLocalFilter } from './example-local.filter';
6+
import { ExampleExceptionSpecificFilter } from './example-specific.exception';
7+
8+
@Controller()
9+
@UseFilters(ExampleLocalFilter)
10+
export class AppController {
11+
constructor(private readonly appService: AppService) {}
12+
13+
@Get('test-exception/:id')
14+
async testException(@Param('id') id: string) {
15+
return this.appService.testException(id);
16+
}
17+
18+
@Get('test-expected-exception/:id')
19+
async testExpectedException(@Param('id') id: string) {
20+
return this.appService.testExpectedException(id);
21+
}
22+
23+
@Get('flush')
24+
async flush() {
25+
await flush();
26+
}
27+
28+
@Get('example-exception-specific-filter')
29+
async exampleExceptionGlobalFilter() {
30+
throw new ExampleExceptionSpecificFilter();
31+
}
32+
33+
@Get('example-exception-local-filter')
34+
async exampleExceptionLocalFilter() {
35+
throw new ExampleExceptionLocalFilter();
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Module } from '@nestjs/common';
2+
import { APP_FILTER } from '@nestjs/core';
3+
import { SentryModule } from '@sentry/nestjs/setup';
4+
import { AppController } from './app.controller';
5+
import { AppService } from './app.service';
6+
import { ExampleWrappedGlobalFilter } from './example-global.filter';
7+
import { ExampleModuleGlobalFilterRegisteredFirst } from './example-module-global-filter-registered-first/example.module';
8+
import { ExampleModuleGlobalFilter } from './example-module-global-filter/example.module';
9+
import { ExampleModuleLocalFilter } from './example-module-local-filter/example.module';
10+
import { ExampleSpecificFilter } from './example-specific.filter';
11+
12+
@Module({
13+
imports: [
14+
ExampleModuleGlobalFilterRegisteredFirst,
15+
SentryModule.forRoot(),
16+
ExampleModuleGlobalFilter,
17+
ExampleModuleLocalFilter,
18+
],
19+
controllers: [AppController],
20+
providers: [
21+
AppService,
22+
{
23+
provide: APP_FILTER,
24+
useClass: ExampleWrappedGlobalFilter,
25+
},
26+
{
27+
provide: APP_FILTER,
28+
useClass: ExampleSpecificFilter,
29+
},
30+
],
31+
})
32+
export class AppModule {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
3+
@Injectable()
4+
export class AppService {
5+
constructor() {}
6+
7+
testException(id: string) {
8+
throw new Error(`This is an exception with id ${id}`);
9+
}
10+
11+
testExpectedException(id: string) {
12+
throw new HttpException(`This is an expected exception with id ${id}`, HttpStatus.FORBIDDEN);
13+
}
14+
}

0 commit comments

Comments
 (0)