Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/nestjs/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat missing integration or E2E test

Medium Severity

This feat only adds unit tests for the new necord path in SentryGlobalFilter. The review rules require feat PRs to include at least one integration or E2E test, so the new context handling is not covered at that level. An integration test can drive a NestJS host with getType() returning necord and assert the captured event without adding necord or discord.js.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 00688ae. Configure here.


// HTTP exceptions
if (!isExpectedError(exception)) {
captureException(exception, {
Expand Down
47 changes: 47 additions & 0 deletions packages/nestjs/test/sentry-global-filter.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
});
});
});