64a16373c5
* * add error boundary immediately above page component * send error logs to server via LoggerContext * display error stack and component stack in copyable text area * wrap every context provider in an error boundary * memoize context values to fix error boundary propagation issue * Wrap page component in an error boundary so we can use our context utilities for a better experience, like toast and logger Still have a top-level error boundary that catches things from the context providers, just in case. Don't display the whole error stack, just because it's ugly, but make it copyable
81 lines
2.4 KiB
JavaScript
81 lines
2.4 KiB
JavaScript
import { Component } from 'react'
|
|
import { StaticLayout } from './layout'
|
|
import styles from '../styles/error.module.css'
|
|
import Image from 'react-bootstrap/Image'
|
|
import copy from 'clipboard-copy'
|
|
import { LoggerContext } from './logger'
|
|
import Button from 'react-bootstrap/Button'
|
|
import { useToast } from './toast'
|
|
|
|
class ErrorBoundary extends Component {
|
|
constructor (props) {
|
|
super(props)
|
|
|
|
// Define a state variable to track whether is an error or not
|
|
this.state = {
|
|
hasError: false,
|
|
error: undefined,
|
|
errorInfo: undefined
|
|
}
|
|
}
|
|
|
|
static getDerivedStateFromError (error) {
|
|
// Update state so the next render will show the fallback UI
|
|
return { hasError: true, error }
|
|
}
|
|
|
|
getErrorDetails () {
|
|
let details = this.state.error.stack
|
|
if (this.state.errorInfo?.componentStack) {
|
|
details += `\n\nComponent stack:${this.state.errorInfo.componentStack}`
|
|
}
|
|
return details
|
|
}
|
|
|
|
componentDidCatch (error, errorInfo) {
|
|
// You can use your own error logging service here
|
|
console.log({ error, errorInfo })
|
|
this.setState({ errorInfo })
|
|
const logger = this.context
|
|
logger?.error(this.getErrorDetails())
|
|
}
|
|
|
|
render () {
|
|
// Check if the error is thrown
|
|
if (this.state.hasError) {
|
|
// You can render any custom fallback UI
|
|
const errorDetails = this.getErrorDetails()
|
|
return (
|
|
<StaticLayout footer={false}>
|
|
<Image width='500' height='375' className='rounded-1 shadow-sm' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/floating.gif`} fluid />
|
|
<h1 className={styles.status} style={{ fontSize: '48px' }}>something went wrong</h1>
|
|
{this.state.error && <CopyErrorButton errorDetails={errorDetails} />}
|
|
</StaticLayout>
|
|
)
|
|
}
|
|
|
|
// Return children components in case of no error
|
|
return this.props.children
|
|
}
|
|
}
|
|
|
|
ErrorBoundary.contextType = LoggerContext
|
|
|
|
export default ErrorBoundary
|
|
|
|
// This button is a functional component so we can use `useToast` hook, which
|
|
// can't be easily done in a class component that already consumes a context
|
|
const CopyErrorButton = ({ errorDetails }) => {
|
|
const toaster = useToast()
|
|
const onClick = async () => {
|
|
try {
|
|
await copy(errorDetails)
|
|
toaster?.success?.('copied')
|
|
} catch (err) {
|
|
console.error(err)
|
|
toaster?.danger?.('failed to copy')
|
|
}
|
|
}
|
|
return <Button className='mt-3' onClick={onClick}>copy error information</Button>
|
|
}
|