diff --git a/src/layouts/shared/ErrorBoundary/ErrorBoundary.tsx b/src/layouts/shared/ErrorBoundary/ErrorBoundary.tsx
index c84df81842..32c866d114 100644
--- a/src/layouts/shared/ErrorBoundary/ErrorBoundary.tsx
+++ b/src/layouts/shared/ErrorBoundary/ErrorBoundary.tsx
@@ -5,6 +5,8 @@ import { Fragment, ReactElement, ReactNode } from 'react'
import A from 'ui/A'
+import { ErrorBoundaryWithRecovery } from './ErrorBoundaryWithRecovery'
+
function DefaultUI() {
return (
@@ -33,14 +35,16 @@ export default function ErrorBoundary({
children,
}: ErrorBoundaryProps) {
return (
-
- sentryScopes.forEach(([key, value]) => scope.setTag(key, value))
- }
- fallback={errorComponent}
- >
- {children}
-
+
+
+ sentryScopes.forEach(([key, value]) => scope.setTag(key, value))
+ }
+ fallback={errorComponent}
+ >
+ {children}
+
+
)
}
diff --git a/src/layouts/shared/ErrorBoundary/ErrorBoundaryWithRecovery.tsx b/src/layouts/shared/ErrorBoundary/ErrorBoundaryWithRecovery.tsx
new file mode 100644
index 0000000000..5411bd43ff
--- /dev/null
+++ b/src/layouts/shared/ErrorBoundary/ErrorBoundaryWithRecovery.tsx
@@ -0,0 +1,49 @@
+import { Component, Fragment, type ReactNode } from 'react'
+
+const MAX_RETRIES = 2
+
+/**
+ * Detects DOM NotFoundErrors caused by browser extensions directly mutating
+ * React-managed nodes (e.g. via insertBefore / removeChild). These errors are
+ * non-actionable from the application side.
+ */
+function isBrowserExtensionDOMError(error: unknown): boolean {
+ if (!(error instanceof Error)) return false
+ if (error.name !== 'NotFoundError') return false
+ const msg = error.message ?? ''
+ return msg.includes('insertBefore') || msg.includes('removeChild')
+}
+
+interface Props {
+ children: ReactNode
+}
+
+interface State {
+ retryKey: number
+}
+
+/**
+ * Wraps children so that when a browser-extension-caused DOM manipulation error
+ * is caught, the subtree is automatically remounted (up to MAX_RETRIES times)
+ * rather than surfacing an error fallback to the user.
+ */
+export class ErrorBoundaryWithRecovery extends Component
{
+ state: State = { retryKey: 0 }
+
+ componentDidCatch(error: Error) {
+ if (
+ isBrowserExtensionDOMError(error) &&
+ this.state.retryKey < MAX_RETRIES
+ ) {
+ // Incrementing the key forces React to unmount and remount the children,
+ // resetting the DOM to a clean state.
+ this.setState((prev) => ({ retryKey: prev.retryKey + 1 }))
+ }
+ }
+
+ render() {
+ return (
+ {this.props.children}
+ )
+ }
+}
diff --git a/src/sentry.ts b/src/sentry.ts
index 2c53256335..e54d5d50b6 100644
--- a/src/sentry.ts
+++ b/src/sentry.ts
@@ -161,11 +161,22 @@ export const setupSentry = ({
replaysOnErrorSampleRate: config?.SENTRY_ERROR_SAMPLE_RATE,
// profiling sample rate
profilesSampleRate: config?.SENTRY_PROFILING_SAMPLE_RATE,
- beforeSend: (event, _hint) => {
+ beforeSend: (event, hint) => {
if (checkForBlockedUserAgents()) {
return null
}
+ // Drop DOM NotFoundErrors caused by browser extensions directly
+ // manipulating React-managed DOM nodes (e.g. insertBefore/removeChild).
+ // These are non-actionable third-party interference errors.
+ const error = hint?.originalException
+ if (error instanceof Error && error.name === 'NotFoundError') {
+ const msg = error.message ?? ''
+ if (msg.includes('insertBefore') || msg.includes('removeChild')) {
+ return null
+ }
+ }
+
return event
},
beforeSendTransaction: (event, _hint) => {