From 00688aedd151c41f30d05811f29dda8f445fa8af Mon Sep 17 00:00:00 2001 From: Tiago Vilas Boas Date: Thu, 10 Sep 2026 22:50:38 +0000 Subject: [PATCH 1/2] feat(nestjs): Handle Necord contexts in SentryGlobalFilter Fixes #18472 Co-authored-by: Tiago Vilas Boas --- packages/nestjs/src/setup.ts | 19 ++++++++ .../nestjs/test/sentry-global-filter.test.ts | 47 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index b646806532b4..93a9e7b9728b 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -179,6 +179,25 @@ class SentryGlobalFilter extends BaseExceptionFilter { return; } + // Necord sets ExecutionContext type to 'necord' (see NecordContextCreator). + // BaseExceptionFilter expects an HTTP adapter and cannot reply to Discord interactions. + if (contextType === 'necord') { + if (!isExpectedError(exception)) { + captureException(exception, { + mechanism: { + handled: false, + type: 'auto.necord.nestjs.global_filter', + }, + }); + } + + if (exception instanceof Error) { + this._logger.error(exception.message, exception.stack); + } + + return; + } + // HTTP exceptions if (!isExpectedError(exception)) { captureException(exception, { diff --git a/packages/nestjs/test/sentry-global-filter.test.ts b/packages/nestjs/test/sentry-global-filter.test.ts index d5f772992854..33689c1264c6 100644 --- a/packages/nestjs/test/sentry-global-filter.test.ts +++ b/packages/nestjs/test/sentry-global-filter.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { ArgumentsHost } from '@nestjs/common'; import { HttpException, HttpStatus, Logger } from '@nestjs/common'; +import { BaseExceptionFilter } from '@nestjs/core'; import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as Helpers from '../src/helpers'; @@ -322,4 +323,50 @@ describe('SentryGlobalFilter', () => { expect(mockLoggerError).toHaveBeenCalledWith(error.message, error.stack); }); }); + + describe('Necord context', () => { + beforeEach(() => { + vi.mocked(mockArgumentsHost.getType).mockReturnValue('necord'); + }); + + it('captures unexpected errors without delegating to the HTTP exception filter', () => { + const superCatchSpy = vi.spyOn(BaseExceptionFilter.prototype, 'catch').mockImplementation(() => undefined); + const error = new Error('Slash command failed'); + + filter.catch(error, mockArgumentsHost); + + expect(mockCaptureException).toHaveBeenCalledWith(error, { + mechanism: { + handled: false, + type: 'auto.necord.nestjs.global_filter', + }, + }); + expect(mockLoggerError).toHaveBeenCalledWith(error.message, error.stack); + expect(superCatchSpy).not.toHaveBeenCalled(); + }); + + it('does not capture expected Necord exceptions', () => { + isExpectedErrorMock.mockReturnValueOnce(true); + const exception = new HttpException('Unknown interaction', HttpStatus.BAD_REQUEST); + + filter.catch(exception, mockArgumentsHost); + + expect(mockCaptureException).not.toHaveBeenCalled(); + expect(mockLoggerError).toHaveBeenCalledWith(exception.message, exception.stack); + }); + + it('captures unexpected non-Error values', () => { + const nonErrorObject = { message: 'interaction failed' }; + + filter.catch(nonErrorObject, mockArgumentsHost); + + expect(mockCaptureException).toHaveBeenCalledWith(nonErrorObject, { + mechanism: { + handled: false, + type: 'auto.necord.nestjs.global_filter', + }, + }); + expect(mockLoggerError).not.toHaveBeenCalled(); + }); + }); }); From d069192a522a161657f39085d798030c8499c4cc Mon Sep 17 00:00:00 2001 From: Tiago Vilas Boas Date: Tue, 22 Sep 2026 12:56:10 -0300 Subject: [PATCH 2/2] fix(nestjs): treat non-HTTP contexts in SentryGlobalFilter Replace the Necord-only branch with a generic contextType !== http path, as requested in review. Mechanism type stays dynamic per context. --- packages/nestjs/src/setup.ts | 8 ++-- .../nestjs/test/sentry-global-filter.test.ts | 46 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 93a9e7b9728b..784f33c3dcb8 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -179,14 +179,14 @@ class SentryGlobalFilter extends BaseExceptionFilter { return; } - // Necord sets ExecutionContext type to 'necord' (see NecordContextCreator). - // BaseExceptionFilter expects an HTTP adapter and cannot reply to Discord interactions. - if (contextType === 'necord') { + // Custom context types (necord, ...) run through ExternalContextCreator and have no HTTP adapter. + // BaseExceptionFilter expects an HTTP adapter and cannot reply on those hosts. + if (contextType !== 'http') { if (!isExpectedError(exception)) { captureException(exception, { mechanism: { handled: false, - type: 'auto.necord.nestjs.global_filter', + type: `auto.${contextType}.nestjs.global_filter`, }, }); } diff --git a/packages/nestjs/test/sentry-global-filter.test.ts b/packages/nestjs/test/sentry-global-filter.test.ts index 33689c1264c6..ba539531626f 100644 --- a/packages/nestjs/test/sentry-global-filter.test.ts +++ b/packages/nestjs/test/sentry-global-filter.test.ts @@ -324,28 +324,29 @@ describe('SentryGlobalFilter', () => { }); }); - describe('Necord context', () => { - beforeEach(() => { - vi.mocked(mockArgumentsHost.getType).mockReturnValue('necord'); - }); - - it('captures unexpected errors without delegating to the HTTP exception filter', () => { - const superCatchSpy = vi.spyOn(BaseExceptionFilter.prototype, 'catch').mockImplementation(() => undefined); - const error = new Error('Slash command failed'); + describe('non-HTTP custom context', () => { + it.each(['necord', 'custom'])( + 'captures unexpected errors for context type %s without delegating to HTTP', + contextType => { + vi.mocked(mockArgumentsHost.getType).mockReturnValue(contextType); + const superCatchSpy = vi.spyOn(BaseExceptionFilter.prototype, 'catch').mockImplementation(() => undefined); + const error = new Error('Custom context failed'); - filter.catch(error, mockArgumentsHost); - - expect(mockCaptureException).toHaveBeenCalledWith(error, { - mechanism: { - handled: false, - type: 'auto.necord.nestjs.global_filter', - }, - }); - expect(mockLoggerError).toHaveBeenCalledWith(error.message, error.stack); - expect(superCatchSpy).not.toHaveBeenCalled(); - }); + filter.catch(error, mockArgumentsHost); - it('does not capture expected Necord exceptions', () => { + expect(mockCaptureException).toHaveBeenCalledWith(error, { + mechanism: { + handled: false, + type: `auto.${contextType}.nestjs.global_filter`, + }, + }); + expect(mockLoggerError).toHaveBeenCalledWith(error.message, error.stack); + expect(superCatchSpy).not.toHaveBeenCalled(); + }, + ); + + it('does not capture expected exceptions for non-HTTP contexts', () => { + vi.mocked(mockArgumentsHost.getType).mockReturnValue('necord'); isExpectedErrorMock.mockReturnValueOnce(true); const exception = new HttpException('Unknown interaction', HttpStatus.BAD_REQUEST); @@ -355,7 +356,8 @@ describe('SentryGlobalFilter', () => { expect(mockLoggerError).toHaveBeenCalledWith(exception.message, exception.stack); }); - it('captures unexpected non-Error values', () => { + it('captures unexpected non-Error values for non-HTTP contexts', () => { + vi.mocked(mockArgumentsHost.getType).mockReturnValue('custom'); const nonErrorObject = { message: 'interaction failed' }; filter.catch(nonErrorObject, mockArgumentsHost); @@ -363,7 +365,7 @@ describe('SentryGlobalFilter', () => { expect(mockCaptureException).toHaveBeenCalledWith(nonErrorObject, { mechanism: { handled: false, - type: 'auto.necord.nestjs.global_filter', + type: 'auto.custom.nestjs.global_filter', }, }); expect(mockLoggerError).not.toHaveBeenCalled();