From c77c7387706fe6b83cac27a9bb313c4916c57f2a Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Sat, 18 Jul 2026 11:23:43 +0100 Subject: [PATCH 1/3] fix(sentry): suppress Capacitor plugin-not-implemented iframe noise Capacitor plugin calls executing inside the /crisp-proxy iframe on native throw '"" plugin is not implemented on ios' because the bridge is not injected into iframes. Pure noise, not a broken binary (PEANUT-UI-QV3: 2,211 events in 14 days from 6 users). Filter the whole message family (StatusBar, Keyboard, App, ...) in shouldIgnoreError via a regex pattern, and add unit coverage for the ignore rules. --- sentry.utils.ts | 10 +++- src/utils/__tests__/sentry-ignore.test.ts | 59 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/utils/__tests__/sentry-ignore.test.ts diff --git a/sentry.utils.ts b/sentry.utils.ts index 2a72d7990c..056fd0aa45 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -7,7 +7,7 @@ import type { ErrorEvent } from '@sentry/nextjs' * Patterns to filter out from Sentry reporting. * These are generally noise that doesn't require action. */ -const IGNORED_ERRORS = { +const IGNORED_ERRORS: Record> = { // User-initiated cancellations (not bugs) userRejected: [ 'User rejected', @@ -46,6 +46,10 @@ const IGNORED_ERRORS = { // which is always true on capacitor://localhost — it then proceeds and the // camera works. Pure noise on native (PEANUT-UI-R1M). 'The camera stream is only accessible if the page is transferred via https', + // Capacitor plugin calls (StatusBar, Keyboard, App, …) running inside the + // /crisp-proxy iframe, where the native bridge isn't injected — pure noise, + // not a broken binary (PEANUT-UI-QV3). + /"[a-z]+" plugin is not implemented on/i, ], } @@ -63,7 +67,9 @@ export function shouldIgnoreError(event: ErrorEvent): boolean { // Check all ignore patterns for (const patterns of Object.values(IGNORED_ERRORS)) { for (const pattern of patterns) { - if (searchText.includes(pattern.toLowerCase())) { + const matches = + typeof pattern === 'string' ? searchText.includes(pattern.toLowerCase()) : pattern.test(searchText) + if (matches) { 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..4728529707 --- /dev/null +++ b/src/utils/__tests__/sentry-ignore.test.ts @@ -0,0 +1,59 @@ +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 — Capacitor plugin-not-implemented iframe noise (PEANUT-UI-QV3)', () => { + it('ignores "StatusBar" plugin not implemented on ios', () => { + expect(shouldIgnoreError(eventWithMessage('"StatusBar" plugin is not implemented on ios'))).toBe(true) + }) + + it('ignores other plugin variants from the same iframe (Keyboard, App)', () => { + expect(shouldIgnoreError(eventWithMessage('"Keyboard" plugin is not implemented on ios'))).toBe(true) + expect(shouldIgnoreError(eventWithMessage('"App" plugin is not implemented on android'))).toBe(true) + expect(shouldIgnoreError(eventWithMessage('"StatusBar" plugin is not implemented on web'))).toBe(true) + }) + + it('matches when the message lives in exception values instead of event.message', () => { + expect(shouldIgnoreError(eventWithException('Error', '"StatusBar" plugin is not implemented on ios'))).toBe( + true + ) + }) + + it('does not match a plugin error that IS implemented-related but differently worded', () => { + expect(shouldIgnoreError(eventWithMessage('StatusBar plugin failed to initialize'))).toBe(false) + }) + + it('still reports unrelated errors', () => { + expect(shouldIgnoreError(eventWithMessage('Cannot read properties of undefined'))).toBe(false) + }) +}) + +describe('shouldIgnoreError — existing string patterns still work', () => { + it('ignores user rejections', () => { + expect(shouldIgnoreError(eventWithMessage('User rejected the request'))).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) + }) +}) From b89a285f1f1e1e24f4654ad13ac30dc7c6dc2f2f Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Sat, 18 Jul 2026 11:43:07 +0100 Subject: [PATCH 2/3] fix(sentry): match ignore patterns per field, not across concatenated fields Matching against one concatenated blob of message + exception value/type + culprit let a pattern match across field boundaries and suppress an unrelated event. Test each field independently; regexes now also run against the original (non-lowercased) text with their own flags. --- sentry.utils.ts | 9 ++++++--- src/utils/__tests__/sentry-ignore.test.ts | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sentry.utils.ts b/sentry.utils.ts index 056fd0aa45..1f2a49ecc9 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -62,13 +62,16 @@ 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) { - const matches = - typeof pattern === 'string' ? searchText.includes(pattern.toLowerCase()) : pattern.test(searchText) + const matches = searchTexts.some((text) => + typeof pattern === 'string' ? text.toLowerCase().includes(pattern.toLowerCase()) : pattern.test(text) + ) if (matches) { return true } diff --git a/src/utils/__tests__/sentry-ignore.test.ts b/src/utils/__tests__/sentry-ignore.test.ts index 4728529707..99ba104577 100644 --- a/src/utils/__tests__/sentry-ignore.test.ts +++ b/src/utils/__tests__/sentry-ignore.test.ts @@ -27,6 +27,15 @@ describe('shouldIgnoreError — Capacitor plugin-not-implemented iframe noise (P ) }) + it('does not match across field boundaries (message + exception value concatenation)', () => { + const event = { + type: undefined, + message: '"StatusBar"', + exception: { values: [{ type: 'Error', value: 'plugin is not implemented on ios' }] }, + } as unknown as ErrorEvent + expect(shouldIgnoreError(event)).toBe(false) + }) + it('does not match a plugin error that IS implemented-related but differently worded', () => { expect(shouldIgnoreError(eventWithMessage('StatusBar plugin failed to initialize'))).toBe(false) }) From 8362551340cdd93371954327fde04b85f478dc1a Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Sat, 18 Jul 2026 12:01:59 +0100 Subject: [PATCH 3/3] fix(sentry): drop plugin-not-implemented filter, keep per-field matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /crisp-proxy iframe source of the plugin-not-implemented events was already fixed (972f35458); residual events come from stale binaries, and a message filter would permanently mask a top-frame plugin failure on a fresh binary — a genuinely broken build we want reported. The Sentry issue is resolved in the dashboard instead (reopens as regressed if it recurs). Keep the per-field matching in shouldIgnoreError: the old concatenated blob let a pattern match across unrelated fields and suppress legitimate events. Tests now cover per-field matching, the cross-boundary case, and the previously untested ignore list. --- sentry.utils.ts | 11 ++------ src/utils/__tests__/sentry-ignore.test.ts | 34 ++++++++++------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/sentry.utils.ts b/sentry.utils.ts index 1f2a49ecc9..7b2c0646b1 100644 --- a/sentry.utils.ts +++ b/sentry.utils.ts @@ -7,7 +7,7 @@ import type { ErrorEvent } from '@sentry/nextjs' * Patterns to filter out from Sentry reporting. * These are generally noise that doesn't require action. */ -const IGNORED_ERRORS: Record> = { +const IGNORED_ERRORS = { // User-initiated cancellations (not bugs) userRejected: [ 'User rejected', @@ -46,10 +46,6 @@ const IGNORED_ERRORS: Record> = { // which is always true on capacitor://localhost — it then proceeds and the // camera works. Pure noise on native (PEANUT-UI-R1M). 'The camera stream is only accessible if the page is transferred via https', - // Capacitor plugin calls (StatusBar, Keyboard, App, …) running inside the - // /crisp-proxy iframe, where the native bridge isn't injected — pure noise, - // not a broken binary (PEANUT-UI-QV3). - /"[a-z]+" plugin is not implemented on/i, ], } @@ -69,10 +65,7 @@ export function shouldIgnoreError(event: ErrorEvent): boolean { // Check all ignore patterns for (const patterns of Object.values(IGNORED_ERRORS)) { for (const pattern of patterns) { - const matches = searchTexts.some((text) => - typeof pattern === 'string' ? text.toLowerCase().includes(pattern.toLowerCase()) : pattern.test(text) - ) - if (matches) { + 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 index 99ba104577..73ee0cd87d 100644 --- a/src/utils/__tests__/sentry-ignore.test.ts +++ b/src/utils/__tests__/sentry-ignore.test.ts @@ -10,44 +10,38 @@ function eventWithException(type: string, value: string): ErrorEvent { return { type: undefined, exception: { values: [{ type, value }] } } as ErrorEvent } -describe('shouldIgnoreError — Capacitor plugin-not-implemented iframe noise (PEANUT-UI-QV3)', () => { - it('ignores "StatusBar" plugin not implemented on ios', () => { - expect(shouldIgnoreError(eventWithMessage('"StatusBar" plugin is not implemented on ios'))).toBe(true) +describe('shouldIgnoreError — per-field matching', () => { + it('matches a pattern contained in event.message', () => { + expect(shouldIgnoreError(eventWithMessage('User rejected the request'))).toBe(true) }) - it('ignores other plugin variants from the same iframe (Keyboard, App)', () => { - expect(shouldIgnoreError(eventWithMessage('"Keyboard" plugin is not implemented on ios'))).toBe(true) - expect(shouldIgnoreError(eventWithMessage('"App" plugin is not implemented on android'))).toBe(true) - expect(shouldIgnoreError(eventWithMessage('"StatusBar" plugin is not implemented on web'))).toBe(true) + it('matches a pattern contained in an exception value', () => { + expect(shouldIgnoreError(eventWithException('Error', 'MetaMask: user rejected signature'))).toBe(true) }) - it('matches when the message lives in exception values instead of event.message', () => { - expect(shouldIgnoreError(eventWithException('Error', '"StatusBar" plugin is not implemented on ios'))).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: '"StatusBar"', - exception: { values: [{ type: 'Error', value: 'plugin is not implemented on ios' }] }, + message: 'Signing failed for User', + exception: { values: [{ type: 'Error', value: 'rejected promise left unhandled' }] }, } as unknown as ErrorEvent expect(shouldIgnoreError(event)).toBe(false) }) - it('does not match a plugin error that IS implemented-related but differently worded', () => { - expect(shouldIgnoreError(eventWithMessage('StatusBar plugin failed to initialize'))).toBe(false) - }) - it('still reports unrelated errors', () => { expect(shouldIgnoreError(eventWithMessage('Cannot read properties of undefined'))).toBe(false) }) }) -describe('shouldIgnoreError — existing string patterns still work', () => { - it('ignores user rejections', () => { - expect(shouldIgnoreError(eventWithMessage('User rejected the request'))).toBe(true) +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', () => {