Skip to content

Commit c1052ab

Browse files
authored
feat(nestjs): Filter RPC exceptions (#13227)
`RpcExceptions` are always explicitly thrown in nest. Therefore, they are expected for users and should not be sent to Sentry. This PR filters these exceptions. In `@sentry/nestjs` we can simply use `instanceof RpcExceptions` to achieve this. In `@sentry/node` we do not have access to this class, so we need to check based on a property. [ref](#13190)
1 parent d6b7279 commit c1052ab

File tree

12 files changed

+96
-7
lines changed

12 files changed

+96
-7
lines changed

dev-packages/e2e-tests/test-applications/nestjs-basic/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"@nestjs/common": "^10.0.0",
1919
"@nestjs/core": "^10.0.0",
20+
"@nestjs/microservices": "^10.0.0",
2021
"@nestjs/schedule": "^4.1.0",
2122
"@nestjs/platform-express": "^10.0.0",
2223
"@sentry/nestjs": "latest || *",

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

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export class AppController {
4949
return this.appService.testExpected500Exception(id);
5050
}
5151

52+
@Get('test-expected-rpc-exception/:id')
53+
async testExpectedRpcException(@Param('id') id: string) {
54+
return this.appService.testExpectedRpcException(id);
55+
}
56+
5257
@Get('test-span-decorator-async')
5358
async testSpanDecoratorAsync() {
5459
return { result: await this.appService.testSpanDecoratorAsync() };

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

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
import { RpcException } from '@nestjs/microservices';
23
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
34
import * as Sentry from '@sentry/nestjs';
45
import { SentryCron, SentryTraced } from '@sentry/nestjs';
@@ -38,6 +39,10 @@ export class AppService {
3839
throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
3940
}
4041

42+
testExpectedRpcException(id: string) {
43+
throw new RpcException(`This is an expected RPC exception with id ${id}`);
44+
}
45+
4146
@SentryTraced('wait and return a string')
4247
async wait() {
4348
await new Promise(resolve => setTimeout(resolve, 500));

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

+25
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,28 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
6969

7070
expect(errorEventOccurred).toBe(false);
7171
});
72+
73+
test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => {
74+
let errorEventOccurred = false;
75+
76+
waitForError('nestjs-basic', event => {
77+
if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') {
78+
errorEventOccurred = true;
79+
}
80+
81+
return event?.transaction === 'GET /test-expected-rpc-exception/:id';
82+
});
83+
84+
const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => {
85+
return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id';
86+
});
87+
88+
const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`);
89+
expect(response.status).toBe(500);
90+
91+
await transactionEventPromise;
92+
93+
await new Promise(resolve => setTimeout(resolve, 10000));
94+
95+
expect(errorEventOccurred).toBe(false);
96+
});

dev-packages/e2e-tests/test-applications/node-nestjs-basic/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"@nestjs/common": "^10.0.0",
1919
"@nestjs/core": "^10.0.0",
20+
"@nestjs/microservices": "^10.0.0",
2021
"@nestjs/schedule": "^4.1.0",
2122
"@nestjs/platform-express": "^10.0.0",
2223
"@sentry/nestjs": "latest || *",

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

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ export class AppController {
4949
return this.appService.testExpected500Exception(id);
5050
}
5151

52+
@Get('test-expected-rpc-exception/:id')
53+
async testExpectedRpcException(@Param('id') id: string) {
54+
return this.appService.testExpectedRpcException(id);
55+
}
56+
5257
@Get('test-span-decorator-async')
5358
async testSpanDecoratorAsync() {
5459
return { result: await this.appService.testSpanDecoratorAsync() };

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

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
import { RpcException } from '@nestjs/microservices';
23
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
34
import * as Sentry from '@sentry/nestjs';
45
import { SentryCron, SentryTraced } from '@sentry/nestjs';
@@ -38,6 +39,10 @@ export class AppService {
3839
throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
3940
}
4041

42+
testExpectedRpcException(id: string) {
43+
throw new RpcException(`This is an expected RPC exception with id ${id}`);
44+
}
45+
4146
@SentryTraced('wait and return a string')
4247
async wait() {
4348
await new Promise(resolve => setTimeout(resolve, 500));

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

+25
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,28 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
6969

7070
expect(errorEventOccurred).toBe(false);
7171
});
72+
73+
test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => {
74+
let errorEventOccurred = false;
75+
76+
waitForError('node-nestjs-basic', event => {
77+
if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') {
78+
errorEventOccurred = true;
79+
}
80+
81+
return event?.transaction === 'GET /test-expected-rpc-exception/:id';
82+
});
83+
84+
const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => {
85+
return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id';
86+
});
87+
88+
const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`);
89+
expect(response.status).toBe(500);
90+
91+
await transactionEventPromise;
92+
93+
await new Promise(resolve => setTimeout(resolve, 10000));
94+
95+
expect(errorEventOccurred).toBe(false);
96+
});

packages/nestjs/package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@
5151
},
5252
"devDependencies": {
5353
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
54-
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
54+
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
55+
"@nestjs/microservices": "^8.0.0 || ^9.0.0 || ^10.0.0"
5556
},
5657
"peerDependencies": {
5758
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
58-
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
59+
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
60+
"@nestjs/microservices": "^8.0.0 || ^9.0.0 || ^10.0.0"
5961
},
6062
"scripts": {
6163
"build": "run-p build:transpile build:types",

packages/nestjs/src/setup.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Catch } from '@nestjs/common';
1111
import { Injectable } from '@nestjs/common';
1212
import { Global, Module } from '@nestjs/common';
1313
import { APP_FILTER, APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core';
14+
import { RpcException } from '@nestjs/microservices';
1415
import {
1516
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1617
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -68,7 +69,7 @@ class SentryGlobalFilter extends BaseExceptionFilter {
6869
*/
6970
public catch(exception: unknown, host: ArgumentsHost): void {
7071
// don't report expected errors
71-
if (exception instanceof HttpException) {
72+
if (exception instanceof HttpException || exception instanceof RpcException) {
7273
return super.catch(exception, host);
7374
}
7475

packages/node/src/integrations/tracing/nest/nest.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,16 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE
8787
const originalCatch = Reflect.get(target, prop, receiver);
8888

8989
return (exception: unknown, host: unknown) => {
90-
const status_code = (exception as { status?: number }).status;
91-
92-
// don't report expected errors
93-
if (status_code !== undefined) {
90+
const exceptionIsObject = typeof exception === 'object' && exception !== null;
91+
const exceptionStatusCode = exceptionIsObject && 'status' in exception ? exception.status : null;
92+
const exceptionErrorProperty = exceptionIsObject && 'error' in exception ? exception.error : null;
93+
94+
/*
95+
Don't report expected NestJS control flow errors
96+
- `HttpException` errors will have a `status` property
97+
- `RpcException` errors will have an `error` property
98+
*/
99+
if (exceptionStatusCode !== null || exceptionErrorProperty !== null) {
94100
return originalCatch.apply(target, [exception, host]);
95101
}
96102

yarn.lock

+8
Original file line numberDiff line numberDiff line change
@@ -6135,6 +6135,14 @@
61356135
path-to-regexp "3.2.0"
61366136
tslib "2.6.3"
61376137

6138+
"@nestjs/microservices@^8.0.0 || ^9.0.0 || ^10.0.0":
6139+
version "10.3.10"
6140+
resolved "https://registry.yarnpkg.com/@nestjs/microservices/-/microservices-10.3.10.tgz#e00957e0c22b0cc8b041242a40538e2d862255fb"
6141+
integrity sha512-zZrilhZmXU2Ik5Usrcy4qEX262Uhvz0/9XlIdX6SRn8I39ns1EE9tAhEBmmkMwh7lsEikRFa4aaa05loi8Gsow==
6142+
dependencies:
6143+
iterare "1.2.1"
6144+
tslib "2.6.3"
6145+
61386146
"@nestjs/platform-express@^10.3.3":
61396147
version "10.3.3"
61406148
resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.3.3.tgz#c1484d30d1e7666c4c8d0d7cde31cfc0b9d166d7"

0 commit comments

Comments
 (0)