fix(sentry): drop browser-extension noise before sending events (#568)

## 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).
This commit is contained in:
Víctor Falcón 2026-06-20 12:47:05 +02:00 committed by GitHub
parent 934e16c0fa
commit 52708f940d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 135 additions and 0 deletions

View File

@ -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)

View File

@ -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);
});
});

View File

@ -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) => {