From 0a5e99fcbd95ed10942c606bf1718c8b64ad2ec1 Mon Sep 17 00:00:00 2001 From: Tiago Vilas Boas Date: Thu, 10 Sep 2026 22:50:38 +0000 Subject: [PATCH] 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(); + }); + }); });