Chore/improve error handling ux for insert before and remove child errors (#39779)

* Improve error handling UX for insertBefore and removeChild client exception errors

* nit

* nit refactor
This commit is contained in:
Joshen Lim authored and GitHub committed 2025-10-23 09:28:35 +08:00
1 parent 13c4b1584c
commit fe4d394fa8
8 files changed
+273 -203

No files matched your search

@@ -41,7 +41,7 @@ export function MessageField({ form, originalError }: MessageFieldProps) {
<Admonition
showIcon={false}
type="default"
className="mt-2"
className="mt-2 max-h-[150px] overflow-y-auto"
title="The error that you ran into will be included in your message for reference"
description={`Error: ${originalError}`}
/>
@@ -28,7 +28,7 @@ import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { Button, cn, KeyboardShortcut } from 'ui'
import { Admonition } from 'ui-patterns'
import { ButtonTooltip } from '../ButtonTooltip'
import { ErrorBoundary } from '../ErrorBoundary'
import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary'
import type { SqlSnippet } from './AIAssistant.types'
import { onErrorChat } from './AIAssistant.utils'
import { AIAssistantHeader } from './AIAssistantHeader'
@@ -0,0 +1,115 @@
import { ExternalLink } from 'lucide-react'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useRouter } from 'next/router'
import { Button, cn } from 'ui'
import { Admonition } from 'ui-patterns'
import CopyButton from '../CopyButton'
import { InlineLinkClassName } from '../InlineLink'
interface ClientSideExceptionHandlerProps {
message: string
sentryIssueId: string
urlMessage: string
resetErrorBoundary: () => void
}
export const ClientSideExceptionHandler = ({
message,
sentryIssueId,
urlMessage,
resetErrorBoundary,
}: ClientSideExceptionHandlerProps) => {
const router = useRouter()
const isProduction = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
const handleClearStorage = () => {
try {
localStorage.clear()
sessionStorage.clear()
} catch (e) {
// ignore
}
window.location.reload()
}
return (
<>
<div className="flex flex-col gap-y-1 text-left py-2 w-full">
<div className="flex items-center justify-between mb-3">
<p className="text-lg font-bold">Sorry! An unexpected error occurred.</p>
<CopyButton type="outline" text={message} copyLabel="Copy error" />
</div>
<p className="text-sm">
Application error: a client-side exception has occurred (see browser console for more
information)
</p>
<p className="text-foreground-light text-sm">{message}</p>
</div>
<Admonition type="warning" showIcon={false} title="We recommend trying the following:">
<ul className="list-disc pl-2 list-inside text-sm space-y-1 [&_b]:font-medium [&_b]:text-foreground">
<li>
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => window.location.reload()}
>
Refresh
</span>{' '}
the page
</li>
<li>
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => router.push('/logout')}
>
Sign out
</span>{' '}
and sign back in
</li>
<li>
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={handleClearStorage}
>
Clear your browser storage
</span>{' '}
to clean potentially outdated data
</li>
<li>
Disable browser extensions that might modify page content (e.g., Google Translate)
</li>
<li>If the problem persists, please contact support for assistance</li>
</ul>
</Admonition>
<div className={cn('w-full mx-auto grid gap-2', 'grid-cols-2 sm:w-1/2')}>
<Button asChild type="default" icon={<ExternalLink />}>
<SupportLink
queryParams={{
category: SupportCategories.DASHBOARD_BUG,
subject: 'Client side exception occurred on dashboard',
sid: sentryIssueId,
error: urlMessage,
}}
>
Contact support
</SupportLink>
</Button>
{/* [Joshen] For local and staging, allow us to escape the error boundary */}
{/* We could actually investigate how to make this available on prod, but without being able to reliably test this, I'm not keen to do it now */}
{isProduction ? (
<Button type="outline" onClick={() => router.reload()}>
Reload dashboard
</Button>
) : (
<Button type="outline" onClick={() => resetErrorBoundary()}>
Return to dashboard
</Button>
)}
</div>
</>
)
}
@@ -0,0 +1,65 @@
import { isError } from 'lodash'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { ClientSideExceptionHandler } from './ClientSideExceptionHandler'
import { InsertBeforeRemoveChildErrorHandler } from './InsertBeforeRemoveChildErrorHandler'
export type FallbackProps = {
error: unknown
resetErrorBoundary: (...args: unknown[]) => void
}
export const GlobalErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) => {
const router = useRouter()
const checkIsError = isError(error)
const largeLogo = useIsFeatureEnabled('branding:large_logo')
const errorMessage = checkIsError ? error.message : ''
const urlMessage = checkIsError ? `Path name: ${router.pathname}\n\n${error?.stack}` : ''
const isRemoveChildError = checkIsError
? errorMessage.includes("Failed to execute 'removeChild' on 'Node'")
: false
const isInsertBeforeError = checkIsError
? errorMessage.includes("Failed to execute 'insertBefore' on 'Node'")
: false
// Get Sentry issue ID from error if available
const sentryIssueId = (!!error && typeof error === 'object' && (error as any).sentryId) ?? ''
return (
<div className="w-screen mx-auto h-screen flex items-center justify-center">
<header className="h-12 absolute top-0 w-full border-b px-4 flex items-center">
<Link href="/" className="items-center justify-center">
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
className={largeLogo ? 'h-[20px]' : 'h-[18px]'}
/>
</Link>
</header>
<div className="flex flex-col gap-y-4 max-w-full sm:max-w-[660px] px-4 sm:px-0">
{isRemoveChildError || isInsertBeforeError ? (
<InsertBeforeRemoveChildErrorHandler
message={errorMessage}
sentryIssueId={sentryIssueId}
urlMessage={urlMessage}
isRemoveChildError={isRemoveChildError}
isInsertBeforeError={isInsertBeforeError}
/>
) : (
<ClientSideExceptionHandler
message={errorMessage}
sentryIssueId={sentryIssueId}
urlMessage={urlMessage}
resetErrorBoundary={resetErrorBoundary}
/>
)}
</div>
</div>
)
}
@@ -0,0 +1,89 @@
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { Blocks, ExternalLink } from 'lucide-react'
import { useRouter } from 'next/router'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { detectBrowser } from 'lib/helpers'
import { Button } from 'ui'
interface InsertBeforeRemoveChildErrorHandlerProps {
message: string
sentryIssueId: string
urlMessage: string
isRemoveChildError: boolean
isInsertBeforeError?: boolean
}
export const InsertBeforeRemoveChildErrorHandler = ({
message,
sentryIssueId,
urlMessage,
isRemoveChildError,
isInsertBeforeError,
}: InsertBeforeRemoveChildErrorHandlerProps) => {
const router = useRouter()
const browser = detectBrowser()
return (
<>
<div className="flex flex-col gap-y-4 text-left py-2 w-full">
<div className="flex items-center gap-x-3">
<p className="text-lg font-bold">Sorry! A browser extension may have caused an error.</p>
<Blocks className="text-foreground-lighter" />
</div>
<div className="flex flex-col gap-y-2">
<p className="text-sm text-foreground-light">
Browser translation tools (like Chrome's built-in Translate) or some third-party browser
extensions are known to cause errors when using the Supabase Dashboard.
</p>
<p className="text-sm text-foreground-light">
We highly recommend{' '}
<span className="text-foreground">
{browser === 'Chrome'
? 'disabling Chrome Translate or certain browser extensions'
: 'avoiding the use of browser translation tools or disabling certain extensions'}
</span>{' '}
while using the Supabase Dashboard to avoid running into this error. Try to refresh the
browser to see if it occurs again.
</p>
</div>
<p className="text-foreground-lighter text-sm">Error: {message}</p>
</div>
<div className="flex gap-x-2 justify-center items-center">
<Button asChild type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href={
isRemoveChildError
? 'https://github.com/facebook/react/issues/17256'
: isInsertBeforeError
? 'https://github.com/facebook/react/issues/24865'
: '/'
}
>
More information
</a>
</Button>
<Button type="outline" onClick={() => router.reload()}>
Refresh page
</Button>
</div>
<SupportLink
className="text-center text-xs text-foreground-lighter hover:text-foreground-light transition"
queryParams={{
category: SupportCategories.DASHBOARD_BUG,
subject: `Client error: Failed to execute '${isRemoveChildError ? 'removeChild' : 'insertBefore'}' on 'Node'`,
sid: sentryIssueId,
error: urlMessage,
}}
>
Still stuck?
</SupportLink>
</>
)
}
@@ -1,200 +0,0 @@
import { isError } from 'lodash'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { Button, cn } from 'ui'
import { Admonition } from 'ui-patterns'
import CopyButton from './CopyButton'
import { InlineLinkClassName } from './InlineLink'
export type FallbackProps = {
error: unknown
resetErrorBoundary: (...args: any[]) => void
}
export const GlobalErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) => {
const router = useRouter()
const checkIsError = isError(error)
const largeLogo = useIsFeatureEnabled('branding:large_logo')
const errorMessage = checkIsError ? error.message : ''
const urlMessage = checkIsError ? `Path name: ${router.pathname}\n\n${error?.stack}` : ''
const isRemoveChildError = checkIsError
? errorMessage.includes("Failed to execute 'removeChild' on 'Node'")
: false
const isInsertBeforeError = checkIsError
? errorMessage.includes("Failed to execute 'insertBefore' on 'Node'")
: false
// Get Sentry issue ID from error if available
const sentryIssueId = (!!error && typeof error === 'object' && (error as any).sentryId) ?? ''
const handleClearStorage = () => {
try {
localStorage.clear()
sessionStorage.clear()
} catch (e) {
// ignore
}
window.location.reload()
}
return (
<div className="w-screen mx-auto h-screen flex items-center justify-center">
<header className="h-12 absolute top-0 w-full border-b px-4 flex items-center">
<Link href="/" className="items-center justify-center">
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
className={largeLogo ? 'h-[20px]' : 'h-[18px]'}
/>
</Link>
</header>
<div className="flex flex-col gap-y-4 max-w-full sm:max-w-[660px] px-4 sm:px-0">
<div className="flex flex-col gap-y-1 text-left py-2 w-full">
<div className="flex items-center justify-between mb-3">
<p className="text-lg font-bold">Sorry! An unexpected error occurred.</p>
<CopyButton type="outline" text={errorMessage} copyLabel="Copy error" />
</div>
<p className="text-sm">
Application error: a client-side exception has occurred (see browser console for more
information)
</p>
<p className="text-foreground-light text-sm">{errorMessage}</p>
</div>
{isRemoveChildError || isInsertBeforeError ? (
<Admonition
type="warning"
title="This error might be caused by Google translate or third-party browser extensions"
>
<p className="prose max-w-full text-sm !leading-normal">
Try to avoid using Google translate or disable certain browser extensions to avoid
running into the{' '}
<code className="text-xs">
{isRemoveChildError
? `'removeChild' on 'Node'`
: isInsertBeforeError
? `'insertBefore' on 'Node'`
: ''}
</code>{' '}
error.{' '}
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => window.location.reload()}
>
Refresh
</span>{' '}
the browser to see if occurs again.
</p>
<Button asChild className="mt-2" type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href={
isRemoveChildError
? 'https://github.com/facebook/react/issues/17256'
: isInsertBeforeError
? 'https://github.com/facebook/react/issues/24865'
: '/'
}
>
More information
</a>
</Button>
</Admonition>
) : (
<Admonition type="warning" showIcon={false} title="We recommend trying the following:">
<ul className="list-disc pl-2 list-inside text-sm space-y-1 [&_b]:font-medium [&_b]:text-foreground">
<li>
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => window.location.reload()}
>
Refresh
</span>{' '}
the page
</li>
<li>
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => router.push('/logout')}
>
Sign out
</span>{' '}
and sign back in
</li>
<li>
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={handleClearStorage}
>
Clear your browser storage
</span>{' '}
to clean potentially outdated data
</li>
<li>
Disable browser extensions that might modify page content (e.g., Google Translate)
</li>
<li>If the problem persists, please contact support for assistance</li>
</ul>
</Admonition>
)}
<div
className={cn(
'w-full mx-auto grid gap-2',
!isRemoveChildError && !isInsertBeforeError
? 'grid-cols-2 sm:w-1/2'
: 'grid-cols-1 sm:w-1/4'
)}
>
{!isRemoveChildError && !isInsertBeforeError && (
<Button asChild type="default" icon={<ExternalLink />}>
<SupportLink
queryParams={{
category: SupportCategories.DASHBOARD_BUG,
subject: 'Client side exception occurred on dashboard',
sid: sentryIssueId,
message: urlMessage,
}}
>
Contact support
</SupportLink>
</Button>
)}
{/* [Joshen] For local and staging, allow us to escape the error boundary */}
{/* We could actually investigate how to make this available on prod, but without being able to reliably test this, I'm not keen to do it now */}
{process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? (
<Button type="outline" onClick={() => resetErrorBoundary()}>
Return to dashboard
</Button>
) : (
<Button type="outline" onClick={() => router.reload()}>
Reload dashboard
</Button>
)}
{(isRemoveChildError || isInsertBeforeError) && (
<SupportLink
className="text-center text-xs text-foreground-lighter hover:text-foreground-light transition"
queryParams={{
category: SupportCategories.DASHBOARD_BUG,
subject: 'Client side exception occurred on dashboard',
sid: sentryIssueId,
message: urlMessage,
}}
>
Still stuck?
</SupportLink>
)}
</div>
</div>
</div>
)
}
+2 -1
View File
@@ -48,7 +48,7 @@ import { StudioCommandMenu } from 'components/interfaces/App/CommandMenu'
import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal'
import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider'
import { GlobalErrorBoundaryState } from 'components/ui/GlobalErrorBoundaryState'
import { GlobalErrorBoundaryState } from 'components/ui/ErrorBoundary/GlobalErrorBoundaryState'
import { useRootQueryClient } from 'data/query-client'
import { customFont, sourceCodePro } from 'fonts'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
@@ -143,6 +143,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
<title>{appTitle ?? 'Supabase'}</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta property="og:image" content={`${BASE_PATH}/img/supabase-logo.png`} />
<meta name="googlebot" content="notranslate" />
{/* [Alaister]: This has to be an inline style tag here and not a separate component due to next/font */}
<style
dangerouslySetInnerHTML={{