From 52708f940df2a86bc1bf47afe72af08abd4e345f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Sat, 20 Jun 2026 12:47:05 +0200 Subject: [PATCH] fix(sentry): drop browser-extension noise before sending events (#568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds an `isBrowserExtensionNoise` predicate to the frontend Sentry `beforeSend` chain that drops errors originating from injected browser-extension scripts. ## Why Triaging Sentry surfaced recurring, unactionable issues caused entirely by users' browser extensions, not our code: - **PHP-LARAVEL-21** — `i: Failed to connect to MetaMask` (`chrome-extension://…/inpage.js`) - **PHP-LARAVEL-3M** — `Cannot read properties of undefined (reading 'sendMessage')` (`chrome-extension://…/injectedScript.bundle.js`) These recur for every user who has the offending extension installed, so resolving-and-waiting just re-pages us forever. This follows the existing pattern of named noise predicates with vitest coverage (`isChunkLoadErrorEvent`, `isSafariCashbackExtensionNoise`, …). ## How The predicate inspects the **crashing (innermost) frame** of each exception and drops the event only when that frame's URL uses a browser-extension scheme (`chrome-extension://`, `moz-extension://`, `safari-web-extension://`, `safari-extension://`, `ms-browser-extension://`). Matching only the crashing frame — not *any* frame — avoids over-filtering legitimate app errors that merely pass through an extension-wrapped handler. ### Not filtered on purpose `AxiosError: Network Error` (PHP-LARAVEL-28 / -38) is left untouched: it has no extension frames and a full backend outage produces the same error, so we want it to keep alerting. ## Tests `resources/js/lib/sentry.test.ts` — 4 new cases (drops the two real extension shapes; keeps the network error and an app error where the extension frame is not the crash site). `vitest run` green (12/12). --- resources/js/app.tsx | 2 + resources/js/lib/sentry.test.ts | 110 ++++++++++++++++++++++++++++++++ resources/js/lib/sentry.ts | 23 +++++++ 3 files changed, 135 insertions(+) diff --git a/resources/js/app.tsx b/resources/js/app.tsx index b0c7ed2c..46c2daa1 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -22,6 +22,7 @@ import { initializeChartColorScheme } from './hooks/use-chart-color-scheme'; import { installChunkLoadRecovery } from './lib/chunk-load-recovery'; import { initializePostHog } from './lib/posthog'; import { + isBrowserExtensionNoise, isChunkLoadErrorEvent, isFacebookInAppBrowserJavaBridgeNoise, isPostMessageDataCloneNoise, @@ -42,6 +43,7 @@ Sentry.init({ beforeSend(event) { if ( isChunkLoadErrorEvent(event) || + isBrowserExtensionNoise(event) || isPostMessageDataCloneNoise(event) || isFacebookInAppBrowserJavaBridgeNoise(event) || isSafariCashbackExtensionNoise(event) diff --git a/resources/js/lib/sentry.test.ts b/resources/js/lib/sentry.test.ts index f2aca5e4..c340d7c3 100644 --- a/resources/js/lib/sentry.test.ts +++ b/resources/js/lib/sentry.test.ts @@ -1,6 +1,7 @@ import type { Event } from '@sentry/react'; import { describe, expect, it } from 'vitest'; import { + isBrowserExtensionNoise, isChunkLoadErrorEvent, isFacebookInAppBrowserJavaBridgeNoise, isPostMessageDataCloneNoise, @@ -181,3 +182,112 @@ describe('isPostMessageDataCloneNoise', () => { expect(isPostMessageDataCloneNoise(event)).toBe(false); }); }); + +describe('isBrowserExtensionNoise', () => { + it('drops errors thrown entirely inside a browser extension script', () => { + const event: Event = { + exception: { + values: [ + { + type: 'i', + value: 'Failed to connect to MetaMask', + stacktrace: { + frames: [ + { + filename: + 'chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/scripts/inpage.js', + function: 'Object.connect', + }, + ], + }, + }, + ], + }, + }; + + expect(isBrowserExtensionNoise(event)).toBe(true); + }); + + it('drops errors that crash in an injected extension script behind an app wrapper frame', () => { + const event: Event = { + exception: { + values: [ + { + type: 'Error', + value: "Cannot read properties of undefined (reading 'sendMessage')", + stacktrace: { + frames: [ + { + filename: '/build/assets/app-DTyIdEGx.js', + function: 'a', + }, + { + filename: + 'chrome-extension://dmkamcknogkgcdfhhbddcghachkejeap/injectedScript.bundle.js', + function: 'n', + }, + ], + }, + }, + ], + }, + }; + + expect(isBrowserExtensionNoise(event)).toBe(true); + }); + + it('keeps application errors with no extension frames', () => { + const event: Event = { + exception: { + values: [ + { + type: 'AxiosError', + value: 'Network Error', + stacktrace: { + frames: [ + { + filename: '/build/assets/app-DWXGp9uF.js', + function: 'Za.request', + }, + { + filename: '/build/assets/app-DWXGp9uF.js', + function: 'T.onerror', + }, + ], + }, + }, + ], + }, + }; + + expect(isBrowserExtensionNoise(event)).toBe(false); + }); + + it('keeps application errors when the extension frame is not where it crashed', () => { + const event: Event = { + exception: { + values: [ + { + type: 'TypeError', + value: 'Cannot read properties of null', + stacktrace: { + frames: [ + { + filename: + 'chrome-extension://abcdefghijklmnop/inject.js', + function: 'wrap', + }, + { + filename: '/build/assets/app.js', + function: 'handleClick', + }, + ], + }, + }, + ], + }, + }; + + expect(isBrowserExtensionNoise(event)).toBe(false); + }); +}); diff --git a/resources/js/lib/sentry.ts b/resources/js/lib/sentry.ts index b8bed60d..a3b0d254 100644 --- a/resources/js/lib/sentry.ts +++ b/resources/js/lib/sentry.ts @@ -6,6 +6,8 @@ const CLONE_ERROR_MESSAGE_PATTERN = const FACEBOOK_IAB_JAVA_OBJECT_GONE_PATTERN = /Error invoking .+: Java object is gone/i; const SAFARI_CASHBACK_EXTENSION_PATTERN = /response\.cashbackReminder/i; +const BROWSER_EXTENSION_URL_PATTERN = + /^(chrome-extension|moz-extension|safari-web-extension|safari-extension|ms-browser-extension):\/\//i; export function isChunkLoadErrorEvent(event: Event): boolean { return ( @@ -57,6 +59,27 @@ export function isFacebookInAppBrowserJavaBridgeNoise(event: Event): boolean { ); } +export function isBrowserExtensionNoise(event: Event): boolean { + return ( + event.exception?.values?.some((exception) => { + const frames = exception.stacktrace?.frames ?? []; + const crashingFrame = [...frames] + .reverse() + .find((frame) => frame.filename ?? frame.module); + + if (!crashingFrame) { + return false; + } + + return [crashingFrame.filename, crashingFrame.module].some( + (value) => + value !== undefined && + BROWSER_EXTENSION_URL_PATTERN.test(value), + ); + }) ?? false + ); +} + export function isSafariCashbackExtensionNoise(event: Event): boolean { return ( event.exception?.values?.some((exception) => {