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