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:
parent
934e16c0fa
commit
52708f940d
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue