From 6585f6524bb2962bd34d43a3ba0a6173c5568e92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 3 Aug 2026 04:00:17 +0000 Subject: [PATCH] fix(sentry): filter injected ethereum/__firefox__ browser noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop wallet shim and Firefox-iOS bridge globals (window.ethereum, window.__firefox__) that throw from page global code on Brave iOS — not from Kody bundles (issues 7648833360, 7648833403). Co-authored-by: Kent C. Dodds --- .../sentry-browser-filters.node.test.ts | 104 ++++++++++++++++++ .../worker/client/sentry-browser-filters.ts | 61 ++++++++++ packages/worker/client/sentry-client.ts | 7 +- packages/worker/client/sentry-init.ts | 5 +- 4 files changed, 174 insertions(+), 3 deletions(-) diff --git a/packages/worker/client/sentry-browser-filters.node.test.ts b/packages/worker/client/sentry-browser-filters.node.test.ts index ec3108b78..dede5267d 100644 --- a/packages/worker/client/sentry-browser-filters.node.test.ts +++ b/packages/worker/client/sentry-browser-filters.node.test.ts @@ -1,6 +1,7 @@ import { expect, test } from 'vitest' import { filterBrowserAbortSentryEvent, + filterBrowserInjectedGlobalNoiseSentryEvent, filterBrowserSentryEvent, filterFirefoxDomPermissionDeniedSentryEvent, } from './sentry-browser-filters.ts' @@ -110,6 +111,96 @@ test('browser Sentry filters drop AbortError and Firefox Xray noise and keep rea }), ).not.toBeNull() + // Injected wallet / Firefox-iOS bridge globals only (issue 7648833360). + expect( + filterBrowserInjectedGlobalNoiseSentryEvent({ + exception: { + values: [ + { + type: 'TypeError', + value: + "undefined is not an object (evaluating 'window.ethereum.selectedAddress = undefined')", + }, + ], + }, + }), + ).toBeNull() + expect( + filterBrowserInjectedGlobalNoiseSentryEvent({ + exception: { + values: [ + { + type: 'TypeError', + value: + "undefined is not an object (evaluating 'window.__firefox__.reader')", + }, + ], + }, + }), + ).toBeNull() + expect( + filterBrowserInjectedGlobalNoiseSentryEvent({ + exception: { + values: [ + { + type: 'TypeError', + value: + "undefined is not an object (evaluating 'window.__firefox__.refresh_youtube_quality_2E41B6CA94114F3CB0CAF4E4DA93D5A8')", + }, + ], + }, + }), + ).toBeNull() + expect( + filterBrowserInjectedGlobalNoiseSentryEvent({ + exception: { + values: [ + { + type: 'ReferenceError', + value: "Can't find variable: __firefox__", + }, + ], + }, + }), + ).toBeNull() + expect( + filterBrowserInjectedGlobalNoiseSentryEvent( + { + exception: { + values: [{ type: 'Error', value: 'something else' }], + }, + }, + new TypeError( + "undefined is not an object (evaluating 'window.ethereum.selectedAddress = undefined')", + ), + ), + ).toBeNull() + expect( + filterBrowserInjectedGlobalNoiseSentryEvent({ + exception: { + values: [ + { + type: 'TypeError', + value: + "undefined is not an object (evaluating 'window.someAppApi.foo')", + }, + ], + }, + }), + ).not.toBeNull() + expect( + filterBrowserInjectedGlobalNoiseSentryEvent({ + exception: { + values: [ + { + type: 'ReferenceError', + value: "Can't find variable: ethereum", + }, + ], + }, + }), + ).not.toBeNull() + const realBug = { exception: { values: [{ type: 'TypeError', value: 'TypeError: Failed to fetch' }], @@ -140,4 +231,17 @@ test('browser Sentry filters drop AbortError and Firefox Xray noise and keep rea }, }), ).toBeNull() + expect( + filterBrowserSentryEvent({ + exception: { + values: [ + { + type: 'TypeError', + value: + "undefined is not an object (evaluating 'window.ethereum.selectedAddress = undefined')", + }, + ], + }, + }), + ).toBeNull() }) diff --git a/packages/worker/client/sentry-browser-filters.ts b/packages/worker/client/sentry-browser-filters.ts index 74d380c83..7e0f8440b 100644 --- a/packages/worker/client/sentry-browser-filters.ts +++ b/packages/worker/client/sentry-browser-filters.ts @@ -126,6 +126,61 @@ export function filterFirefoxDomPermissionDeniedSentryEvent< return event } +/** + * Wallet / browser-injected globals that throw from page `global code`, not + * from Kody bundles (no app code references these). Production signatures from + * Brave iOS issue 7648833360 and sibling 7648833403: + * - `undefined is not an object (evaluating 'window.ethereum…')` + * - `undefined is not an object (evaluating 'window.__firefox__…')` + * - `Can't find variable: __firefox__` + * + * Match is intentionally narrow: only these injected-global access patterns. + * Never blanket-drop TypeError / ReferenceError. + */ +export function isBrowserInjectedGlobalNoiseMessage(message: string) { + const withoutTypePrefix = message + .trim() + .replace(/^(?:TypeError|ReferenceError):\s*/i, '') + return ( + /^undefined is not an object \(evaluating ['"]window\.(?:ethereum|__firefox__)\./i.test( + withoutTypePrefix, + ) || + /^Can'?t find variable: __firefox__\.?$/i.test(withoutTypePrefix) || + /^Cannot find variable: __firefox__\.?$/i.test(withoutTypePrefix) || + /^__firefox__ is not defined\.?$/i.test(withoutTypePrefix) + ) +} + +export function isBrowserInjectedGlobalNoiseError(error: unknown) { + if (typeof error === 'string') { + return isBrowserInjectedGlobalNoiseMessage(error) + } + if (typeof error !== 'object' || error === null) return false + if (!('message' in error) || typeof error.message !== 'string') return false + return isBrowserInjectedGlobalNoiseMessage(error.message) +} + +export function isBrowserInjectedGlobalNoiseSentryEvent( + event: SentryErrorEventLike, + originalException?: unknown, +) { + if (isBrowserInjectedGlobalNoiseError(originalException)) return true + return sentryEventMessages(event).some( + (message) => + typeof message === 'string' && + isBrowserInjectedGlobalNoiseMessage(message), + ) +} + +export function filterBrowserInjectedGlobalNoiseSentryEvent< + T extends SentryErrorEventLike, +>(event: T, originalException?: unknown): T | null { + if (isBrowserInjectedGlobalNoiseSentryEvent(event, originalException)) { + return null + } + return event +} + /** Combined browser beforeSend / capture gate used by the client SDK. */ export function filterBrowserSentryEvent( event: T, @@ -140,5 +195,11 @@ export function filterBrowserSentryEvent( ) { return null } + if ( + filterBrowserInjectedGlobalNoiseSentryEvent(event, originalException) === + null + ) { + return null + } return event } diff --git a/packages/worker/client/sentry-client.ts b/packages/worker/client/sentry-client.ts index 310486c1d..1ff2e3caf 100644 --- a/packages/worker/client/sentry-client.ts +++ b/packages/worker/client/sentry-client.ts @@ -1,5 +1,6 @@ import { isBrowserAbortError, + isBrowserInjectedGlobalNoiseError, isFirefoxDomPermissionDeniedError, } from '#client/sentry-browser-filters.ts' import { @@ -44,7 +45,11 @@ let onWindowError: ((event: ErrorEvent) => void) | null = null let onUnhandledRejection: ((event: PromiseRejectionEvent) => void) | null = null function shouldIgnoreBufferedError(error: unknown) { - return isBrowserAbortError(error) || isFirefoxDomPermissionDeniedError(error) + return ( + isBrowserAbortError(error) || + isFirefoxDomPermissionDeniedError(error) || + isBrowserInjectedGlobalNoiseError(error) + ) } function enqueueBufferedError(error: unknown) { diff --git a/packages/worker/client/sentry-init.ts b/packages/worker/client/sentry-init.ts index 68220c371..b85033c88 100644 --- a/packages/worker/client/sentry-init.ts +++ b/packages/worker/client/sentry-init.ts @@ -29,8 +29,9 @@ export function initBrowserSentry(config: SentryClientConfig) { replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 1.0, // Drop expected browser noise (AbortError aborts, Firefox DOM - // permission-denied from Replay/hydration on restricted nodes) — - // see filterBrowserSentryEvent / KODY-CLOUDFLARE-23 / issue 7639685398. + // permission-denied from Replay/hydration on restricted nodes, and + // injected wallet/`__firefox__` globals) — see filterBrowserSentryEvent + // / KODY-CLOUDFLARE-23 / issues 7639685398, 7648833360, 7648833403. beforeSend(event, hint) { return filterBrowserSentryEvent(event, hint.originalException) },