Skip to content

Commit

Permalink
Custom error boundary (#25946)
Browse files Browse the repository at this point in the history
* Test custom error boundary

* Test

* Test

* Add error boundary to catch full page crashes

* Update apps/studio/components/ui/ErrorBoundaryState.tsx

Co-authored-by: Alaister Young <[email protected]>

---------

Co-authored-by: Alaister Young <[email protected]>
  • Loading branch information
joshenlim and alaister committed May 13, 2024
1 parent 3e4e73f commit a72220e
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 75 deletions.
16 changes: 5 additions & 11 deletions apps/studio/components/interfaces/Home/ServiceStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { useParams } from 'common'
import { AlertTriangle, CheckCircle2 } from 'lucide-react'
import { AlertTriangle, CheckCircle2, Loader2 } from 'lucide-react'
import { useState } from 'react'
import {
Button,
IconLoader,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_ } from 'ui'

import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'
import { usePostgresServiceStatusQuery } from 'data/service-status/postgres-service-status-query'
import { useProjectServiceStatusQuery } from 'data/service-status/service-status-query'
import { useIsFeatureEnabled, useSelectedProject } from 'hooks'
import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'

const ServiceStatus = () => {
const { ref } = useParams()
Expand Down Expand Up @@ -125,7 +119,7 @@ const ServiceStatus = () => {
type="default"
icon={
isLoadingChecks ? (
<IconLoader className="animate-spin" />
<Loader2 className="animate-spin" />
) : (
<div
className={`w-2 h-2 rounded-full ${
Expand Down Expand Up @@ -155,7 +149,7 @@ const ServiceStatus = () => {
</p>
</div>
{service.isLoading ? (
<IconLoader className="animate-spin" size="tiny" />
<Loader2 className="animate-spin text-foreground-light" size={18} />
) : service.isSuccess ? (
<CheckCircle2 className="text-brand" size={18} strokeWidth={1.5} />
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/nextjs'
import { BASE_PATH } from 'lib/constants'
import { auth, buildPathWithParams } from 'lib/gotrue'
import { useState } from 'react'
Expand Down
32 changes: 13 additions & 19 deletions apps/studio/components/interfaces/Support/SupportForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CLIENT_LIBRARIES } from 'common/constants'
import { HelpCircle } from 'lucide-react'
import { AlertCircle, ExternalLink, HelpCircle, Loader2, Mail, Plus, X } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ChangeEvent, useEffect, useRef, useState } from 'react'
Expand All @@ -25,12 +25,6 @@ import {
Button,
Checkbox,
Form,
IconAlertCircle,
IconExternalLink,
IconLoader,
IconMail,
IconPlus,
IconX,
Input,
Listbox,
Separator,
Expand Down Expand Up @@ -357,7 +351,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
<div className="space-y-2">
<p className="text-sm prose">Which project is affected?</p>
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
<IconAlertCircle strokeWidth={2} className="text-foreground-light" />
<AlertCircle size={16} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm prose">Failed to retrieve projects</p>
</div>
</div>
Expand Down Expand Up @@ -413,7 +407,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
</p>
) : isLoadingSubscription && selectedProjectRef !== 'no-project' ? (
<div className="flex items-center space-x-2 mt-2">
<IconLoader size={14} className="animate-spin" />
<Loader2 size={14} className="animate-spin" />
<p className="text-sm text-foreground-light">Checking project's plan</p>
</div>
) : (
Expand Down Expand Up @@ -443,7 +437,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
<div className="space-y-2">
<p className="text-sm prose">Which organization is affected?</p>
<div className="border rounded-md px-4 py-2 flex items-center space-x-2">
<IconAlertCircle strokeWidth={2} className="text-foreground-light" />
<AlertCircle size={16} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm prose">Failed to retrieve organizations</p>
</div>
</div>
Expand Down Expand Up @@ -473,7 +467,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
{subscription?.plan.id !== 'enterprise' && values.category !== 'Login_issues' && (
<div className="px-6">
<InformationBox
icon={<IconAlertCircle strokeWidth={2} />}
icon={<AlertCircle size={18} strokeWidth={2} />}
defaultVisibility={true}
hideCollapse={true}
title="Expected response times are based on your project's plan"
Expand Down Expand Up @@ -516,7 +510,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
Upgrade project
</Link>
</Button>
<Button asChild type="default" icon={<IconExternalLink size={14} />}>
<Button asChild type="default" icon={<ExternalLink />}>
<Link
href="https://supabase.com/contact/enterprise"
target="_blank"
Expand Down Expand Up @@ -561,7 +555,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
className="flex items-center space-x-2 text-foreground-light underline hover:text-foreground transition"
>
Github discussions
<IconExternalLink size={14} strokeWidth={2} className="ml-1" />
<ExternalLink size={14} strokeWidth={2} className="ml-1" />
</Link>
<span> for a quick answer</span>
</p>
Expand Down Expand Up @@ -629,7 +623,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
<Button
asChild
type="default"
icon={<IconExternalLink size={14} strokeWidth={1.5} />}
icon={<ExternalLink size={14} strokeWidth={1.5} />}
>
<Link href={library.url} target="_blank" rel="noreferrer">
View Github issues
Expand All @@ -655,7 +649,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
<Button
asChild
type="default"
icon={<IconExternalLink size={14} strokeWidth={1.5} />}
icon={<ExternalLink size={14} strokeWidth={1.5} />}
>
<Link
href="https://github.com/supabase/supabase"
Expand Down Expand Up @@ -712,7 +706,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
<Button
asChild
type="default"
icon={<IconExternalLink strokeWidth={1.5} />}
icon={<ExternalLink strokeWidth={1.5} />}
>
<Link
href="https://github.com/orgs/supabase/discussions/17817"
Expand Down Expand Up @@ -771,7 +765,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
].join(' ')}
onClick={() => removeUploadedFile(idx)}
>
<IconX size={12} strokeWidth={2} />
<X size={12} strokeWidth={2} />
</div>
</div>
))}
Expand All @@ -785,7 +779,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
if (uploadButtonRef.current) (uploadButtonRef.current as any).click()
}}
>
<IconPlus strokeWidth={2} size={20} />
<Plus strokeWidth={2} size={20} />
</div>
)}
</div>
Expand All @@ -804,7 +798,7 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
<Button
htmlType="submit"
size="small"
icon={<IconMail />}
icon={<Mail />}
disabled={isSubmitting}
loading={isSubmitting}
>
Expand Down
41 changes: 41 additions & 0 deletions apps/studio/components/ui/ErrorBoundaryState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { FallbackProps } from 'react-error-boundary'

import { Button } from 'ui'

export const ErrorBoundaryState = ({ error, resetErrorBoundary }: FallbackProps) => {
const router = useRouter()
const message = `Path name: ${router.pathname}\n\n${error.stack}`

return (
<div className="w-screen h-screen flex items-center justify-center flex-col gap-y-3">
<div className="flex items-center flex-col gap-y-1">
<p className="text-sm">
Application error: a client-side exception has occurred (see browser console for more
information)
</p>
<p className="text-sm text-foreground-light">Error: {error.message}</p>
</div>

<div className="flex items-center justify-center gap-x-2">
<Button asChild type="default" icon={<ExternalLink />}>
<Link
href={`/support/new?category=dashboard_bug&subject=Client%20side%20exception%20occured%20on%20dashboard&message=${encodeURI(message)}`}
target="_blank"
>
Report to support
</Link>
</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>
)}
</div>
</div>
)
}
5 changes: 3 additions & 2 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.13",
"react-grid-layout": "^1.4.2",
"react-hot-toast": "^2.4.1",
"react-inlinesvg": "^4.0.4",
Expand Down Expand Up @@ -136,6 +137,7 @@
"@types/sqlstring": "^2.3.0",
"@types/uuid": "^8.3.4",
"@types/zxcvbn": "^4.4.1",
"api-types": "*",
"autoprefixer": "^10.4.14",
"common": "*",
"config": "*",
Expand All @@ -147,7 +149,6 @@
"prettier": "^4.0.0-alpha.8",
"storybook-dark-mode": "^3.0.1",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.3",
"api-types": "*"
"typescript": "^5.4.3"
}
}
88 changes: 49 additions & 39 deletions apps/studio/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'styles/ui.scss'
import 'ui/build/css/themes/dark.css'
import 'ui/build/css/themes/light.css'

import * as Sentry from '@sentry/nextjs'
import { loader } from '@monaco-editor/react'
import { TooltipProvider } from '@radix-ui/react-tooltip'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
Expand All @@ -29,7 +30,8 @@ import relativeTime from 'dayjs/plugin/relativeTime'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import Head from 'next/head'
import { useEffect, useMemo, useRef, useState } from 'react'
import { ErrorInfo, useEffect, useMemo, useRef, useState } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import toast from 'react-hot-toast'
import { PortalToast, Toaster } from 'ui'
import { ConsentToast } from 'ui-patterns/ConsentToast'
Expand All @@ -43,6 +45,7 @@ import {
import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext'
import { FeaturePreviewContextProvider } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/FeaturePreviewModal'
import { ErrorBoundaryState } from 'components/ui/ErrorBoundaryState'
import FlagProvider from 'components/ui/Flag/FlagProvider'
import PageTelemetry from 'components/ui/PageTelemetry'
import { useRootQueryClient } from 'data/query-client'
Expand Down Expand Up @@ -107,6 +110,11 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
[supabase]
)

const errorBoundaryHandler = (error: Error, info: ErrorInfo) => {
console.error(error.stack)
Sentry.captureMessage('Full page crash caught by error boundary')
}

useEffect(() => {
// Check for telemetry consent
if (typeof window !== 'undefined') {
Expand Down Expand Up @@ -140,44 +148,46 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
const isTestEnv = process.env.NEXT_PUBLIC_NODE_ENV === 'test'

return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<AuthContainer>
<ProfileProvider>
<FlagProvider>
<Head>
<title>Supabase</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<MetaFaviconsPagesRouter applicationName="Supabase Studio" />
<PageTelemetry>
<TooltipProvider>
<RouteValidationWrapper>
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
<AppBannerContextProvider>
<CommandMenuWrapper>
<AppBannerWrapper>
<FeaturePreviewContextProvider>
{getLayout(<Component {...pageProps} />)}
<FeaturePreviewModal />
</FeaturePreviewContextProvider>
</AppBannerWrapper>
</CommandMenuWrapper>
</AppBannerContextProvider>
</ThemeProvider>
</RouteValidationWrapper>
</TooltipProvider>
</PageTelemetry>

<HCaptchaLoadedStore />
<Toaster />
<PortalToast />
{!isTestEnv && <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />}
</FlagProvider>
</ProfileProvider>
</AuthContainer>
</Hydrate>
</QueryClientProvider>
<ErrorBoundary FallbackComponent={ErrorBoundaryState} onError={errorBoundaryHandler}>
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<AuthContainer>
<ProfileProvider>
<FlagProvider>
<Head>
<title>Supabase</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<MetaFaviconsPagesRouter applicationName="Supabase Studio" />
<PageTelemetry>
<TooltipProvider>
<RouteValidationWrapper>
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
<AppBannerContextProvider>
<CommandMenuWrapper>
<AppBannerWrapper>
<FeaturePreviewContextProvider>
{getLayout(<Component {...pageProps} />)}
<FeaturePreviewModal />
</FeaturePreviewContextProvider>
</AppBannerWrapper>
</CommandMenuWrapper>
</AppBannerContextProvider>
</ThemeProvider>
</RouteValidationWrapper>
</TooltipProvider>
</PageTelemetry>

<HCaptchaLoadedStore />
<Toaster />
<PortalToast />
{!isTestEnv && <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />}
</FlagProvider>
</ProfileProvider>
</AuthContainer>
</Hydrate>
</QueryClientProvider>
</ErrorBoundary>
)
}

Expand Down
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a72220e

Please sign in to comment.