diff --git a/sentry.utils.ts b/sentry.utils.ts index 2a72d7990c..7b2c0646b1 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -58,12 +58,14 @@ export function shouldIgnoreError(event: ErrorEvent): boolean { const exceptionType = event.exception?.values?.[0]?.type || '' const culprit = (event as any).culprit || '' - const searchText = `${message} ${exceptionValue} ${exceptionType} ${culprit}`.toLowerCase() + // Match each field independently — concatenating them would let a pattern + // match across unrelated fields and suppress a legitimate event. + const searchTexts = [message, exceptionValue, exceptionType, culprit] // Check all ignore patterns for (const patterns of Object.values(IGNORED_ERRORS)) { for (const pattern of patterns) { - if (searchText.includes(pattern.toLowerCase())) { + if (searchTexts.some((text) => text.toLowerCase().includes(pattern.toLowerCase()))) { return true } } diff --git a/src/utils/__tests__/sentry-ignore.test.ts b/src/utils/__tests__/sentry-ignore.test.ts new file mode 100644 index 0000000000..73ee0cd87d --- /dev/null +++ b/src/utils/__tests__/sentry-ignore.test.ts @@ -0,0 +1,62 @@ +import type { ErrorEvent } from '@sentry/nextjs' + +import { shouldIgnoreError } from '../../../sentry.utils' + +function eventWithMessage(message: string): ErrorEvent { + return { type: undefined, message } as ErrorEvent +} + +function eventWithException(type: string, value: string): ErrorEvent { + return { type: undefined, exception: { values: [{ type, value }] } } as ErrorEvent +} + +describe('shouldIgnoreError — per-field matching', () => { + it('matches a pattern contained in event.message', () => { + expect(shouldIgnoreError(eventWithMessage('User rejected the request'))).toBe(true) + }) + + it('matches a pattern contained in an exception value', () => { + expect(shouldIgnoreError(eventWithException('Error', 'MetaMask: user rejected signature'))).toBe(true) + }) + + it('matches case-insensitively', () => { + expect(shouldIgnoreError(eventWithMessage('USER REJECTED the request'))).toBe(true) + }) + + it('does not match across field boundaries (message + exception value concatenation)', () => { + // 'User rejected' split across two fields: the old concatenated-blob + // matching would have suppressed this unrelated event. + const event = { + type: undefined, + message: 'Signing failed for User', + exception: { values: [{ type: 'Error', value: 'rejected promise left unhandled' }] }, + } as unknown as ErrorEvent + expect(shouldIgnoreError(event)).toBe(false) + }) + + it('still reports unrelated errors', () => { + expect(shouldIgnoreError(eventWithMessage('Cannot read properties of undefined'))).toBe(false) + }) +}) + +describe('shouldIgnoreError — existing patterns intact', () => { + it('ignores network noise', () => { + expect(shouldIgnoreError(eventWithException('TypeError', 'Failed to fetch'))).toBe(true) + }) + + it('ignores extension noise via stack frames', () => { + const event = { + type: undefined, + exception: { + values: [ + { + type: 'TypeError', + value: 'boom', + stacktrace: { frames: [{ filename: 'chrome-extension://abc/content.js' }] }, + }, + ], + }, + } as unknown as ErrorEvent + expect(shouldIgnoreError(event)).toBe(true) + }) +})