- Notifications
You must be signed in to change notification settings - Fork928
feat: added error boundary#1602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
2d6531b
59be45a
dfa43fb
d12d8fc
b9fce75
0cf36a4
a5f1aa9
bea96b4
dbb7913
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -52,6 +52,7 @@ | ||
"rpty", | ||
"sdkproto", | ||
"Signup", | ||
"sourcemapped", | ||
"stretchr", | ||
"TCGETS", | ||
"tcpip", | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -42,6 +42,7 @@ | ||
"react": "17.0.2", | ||
"react-dom": "17.0.2", | ||
"react-router-dom": "6.3.0", | ||
"sourcemapped-stacktrace": "1.1.11", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. praise: Thanks for pinning this! | ||
"swr": "1.2.2", | ||
"uuid": "8.3.2", | ||
"xstate": "4.32.1", | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -4,6 +4,7 @@ import React from "react" | ||
import { BrowserRouter as Router } from "react-router-dom" | ||
import { SWRConfig } from "swr" | ||
import { AppRouter } from "./AppRouter" | ||
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary" | ||
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar" | ||
import { dark } from "./theme" | ||
import "./theme/globalFonts" | ||
@@ -30,13 +31,15 @@ export const App: React.FC = () => { | ||
}, | ||
}} | ||
> | ||
<ThemeProvider theme={dark}> | ||
<CssBaseline /> | ||
<ErrorBoundary> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Praise: I love that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Praise: I love that the | ||
<XServiceProvider> | ||
<AppRouter /> | ||
<GlobalSnackbar /> | ||
</XServiceProvider> | ||
</ErrorBoundary> | ||
</ThemeProvider> | ||
</SWRConfig> | ||
</Router> | ||
) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from "react" | ||
import { RuntimeErrorState } from "../RuntimeErrorState/RuntimeErrorState" | ||
type ErrorBoundaryProps = Record<string, unknown> | ||
interface ErrorBoundaryState { | ||
error: Error | null | ||
} | ||
/** | ||
* Our app's Error Boundary | ||
* Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html | ||
*/ | ||
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { | ||
constructor(props: ErrorBoundaryProps) { | ||
super(props) | ||
this.state = { error: null } | ||
} | ||
static getDerivedStateFromError(error: Error): { error: Error } { | ||
return { error } | ||
} | ||
render(): React.ReactNode { | ||
if (this.state.error) { | ||
return <RuntimeErrorState error={this.state.error} /> | ||
} | ||
return this.props.children | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { makeStyles } from "@material-ui/core/styles" | ||
import React from "react" | ||
import { CodeBlock } from "../CodeBlock/CodeBlock" | ||
import { createCtas } from "./createCtas" | ||
const Language = { | ||
reportLoading: "Generating crash report...", | ||
} | ||
interface ReportState { | ||
error: Error | ||
mappedStack: string[] | null | ||
} | ||
interface StackTraceAvailableMsg { | ||
type: "stackTraceAvailable" | ||
stackTrace: string[] | ||
} | ||
/** | ||
* stackTraceUnavailable is a Msg describing a stack trace not being available | ||
*/ | ||
export const stackTraceUnavailable = { | ||
type: "stackTraceUnavailable", | ||
} as const | ||
type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable | ||
export const stackTraceAvailable = (stackTrace: string[]): StackTraceAvailableMsg => { | ||
return { | ||
type: "stackTraceAvailable", | ||
stackTrace, | ||
} | ||
} | ||
const setStackTrace = (model: ReportState, mappedStack: string[]): ReportState => { | ||
return { | ||
...model, | ||
mappedStack, | ||
} | ||
} | ||
export const reducer = (model: ReportState, msg: ReportMessage): ReportState => { | ||
switch (msg.type) { | ||
case "stackTraceAvailable": | ||
return setStackTrace(model, msg.stackTrace) | ||
case "stackTraceUnavailable": | ||
return setStackTrace(model, ["Unable to get stack trace"]) | ||
} | ||
} | ||
export const createFormattedStackTrace = (error: Error, mappedStack: string[] | null): string[] => { | ||
return [ | ||
"======================= STACK TRACE ========================", | ||
"", | ||
error.message, | ||
...(mappedStack ? mappedStack : []), | ||
"", | ||
"============================================================", | ||
] | ||
} | ||
/** | ||
* A code block component that contains the error stack resulting from an error boundary trigger | ||
*/ | ||
export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): React.ReactElement => { | ||
const styles = useStyles() | ||
if (!mappedStack) { | ||
return <CodeBlock lines={[Language.reportLoading]} className={styles.codeBlock} /> | ||
} | ||
const formattedStackTrace = createFormattedStackTrace(error, mappedStack) | ||
return <CodeBlock lines={formattedStackTrace} className={styles.codeBlock} ctas={createCtas(formattedStackTrace)} /> | ||
} | ||
const useStyles = makeStyles(() => ({ | ||
codeBlock: { | ||
minHeight: "auto", | ||
userSelect: "all", | ||
width: "100%", | ||
}, | ||
})) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { ComponentMeta, Story } from "@storybook/react" | ||
import React from "react" | ||
import { RuntimeErrorState, RuntimeErrorStateProps } from "./RuntimeErrorState" | ||
const error = new Error("An error occurred") | ||
export default { | ||
title: "components/RuntimeErrorState", | ||
component: RuntimeErrorState, | ||
argTypes: { | ||
error: { | ||
defaultValue: error, | ||
}, | ||
}, | ||
} as ComponentMeta<typeof RuntimeErrorState> | ||
const Template: Story<RuntimeErrorStateProps> = (args) => <RuntimeErrorState {...args} /> | ||
export const Errored = Template.bind({}) | ||
Errored.parameters = { | ||
// The RuntimeErrorState is noisy for chromatic, because it renders an actual error | ||
// along with the stacktrace - and the stacktrace includes the full URL of | ||
// scripts in the stack. This is problematic, because every deployment uses | ||
// a different URL, causing the validation to fail. | ||
chromatic: { disableSnapshot: true }, | ||
} | ||
Errored.args = { | ||
error, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { screen } from "@testing-library/react" | ||
import React from "react" | ||
import { render } from "../../testHelpers/renderHelpers" | ||
import { Language as ButtonLanguage } from "./createCtas" | ||
import { Language as RuntimeErrorStateLanguage, RuntimeErrorState } from "./RuntimeErrorState" | ||
describe("RuntimeErrorState", () => { | ||
beforeEach(() => { | ||
// Given | ||
const errorText = "broken!" | ||
const errorStateProps = { | ||
error: new Error(errorText), | ||
} | ||
// When | ||
render(<RuntimeErrorState {...errorStateProps} />) | ||
}) | ||
it("should show stack when encountering runtime error", () => { | ||
// Then | ||
const reportError = screen.getByText("broken!") | ||
expect(reportError).toBeDefined() | ||
// Despite appearances, this is the stack trace | ||
const stackTrace = screen.getByText("Unable to get stack trace") | ||
expect(stackTrace).toBeDefined() | ||
}) | ||
it("should have a button bar", () => { | ||
// Then | ||
const copyCta = screen.getByText(ButtonLanguage.copyReport) | ||
expect(copyCta).toBeDefined() | ||
const reloadCta = screen.getByText(ButtonLanguage.reloadApp) | ||
expect(reloadCta).toBeDefined() | ||
}) | ||
it("should have an email link", () => { | ||
// Then | ||
const emailLink = screen.getByText(RuntimeErrorStateLanguage.link) | ||
expect(emailLink.closest("a")).toHaveAttribute("href", expect.stringContaining("mailto:support@coder.com")) | ||
}) | ||
}) |
Uh oh!
There was an error while loading.Please reload this page.