mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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={{
|
||||
|
||||
Reference in new issue
Block a user