Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/layouts/shared/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Fragment, ReactElement, ReactNode } from 'react'

import A from 'ui/A'

import { ErrorBoundaryWithRecovery } from './ErrorBoundaryWithRecovery'

function DefaultUI() {
return (
<div className="flex flex-1 items-center justify-center">
Expand Down Expand Up @@ -33,14 +35,16 @@ export default function ErrorBoundary({
children,
}: ErrorBoundaryProps) {
return (
<Sentry.ErrorBoundary
beforeCapture={(scope) =>
sentryScopes.forEach(([key, value]) => scope.setTag(key, value))
}
fallback={errorComponent}
>
{children}
</Sentry.ErrorBoundary>
<ErrorBoundaryWithRecovery>
<Sentry.ErrorBoundary
beforeCapture={(scope) =>
sentryScopes.forEach(([key, value]) => scope.setTag(key, value))
}
fallback={errorComponent}
>
{children}
</Sentry.ErrorBoundary>
</ErrorBoundaryWithRecovery>
)
}

Expand Down
49 changes: 49 additions & 0 deletions src/layouts/shared/ErrorBoundary/ErrorBoundaryWithRecovery.tsx
Original file line number Diff line number Diff line change
@@ -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<Props, State> {
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 (
<Fragment key={this.state.retryKey}>{this.props.children}</Fragment>
)
}
}
13 changes: 12 additions & 1 deletion src/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading