Fix Flipper crashing to an empty screen

Summary:
Changelog: Fixed issue where a Flipper crash would result in an entirely blank screen, rather than a useful error message.

While debugging another issue, discovered that React errors that happen outside a Plugin aren't caught at all, resulting in the infamous gray screen of deaths. This was the case because no error boundary has been set up for our Chrome, and since React 16 the default error handling has becoming rendering blank, rather than freezing. See https://reactjs.org/docs/error-boundaries.html#new-behavior-for-uncaught-errors.

Thanks to ant.design styling this decently was trivial :). But sadly involved a component class since error boundaries are not yet available as hook.

With these changes the errors should also end up more readably in our monitoring.

Reviewed By: nikoant

Differential Revision: D26422666

fbshipit-source-id: 6c0f8611c80a4a5e0d7e61d58afcf5eabe410e57
This commit is contained in:
Michel Weststrate
2021-02-12 06:42:34 -08:00
committed by Facebook GitHub Bot
parent 11548c9cd6
commit 4964966b91
2 changed files with 102 additions and 17 deletions

View File

@@ -40,8 +40,15 @@ import {
_setGlobalInteractionReporter, _setGlobalInteractionReporter,
Logger, Logger,
_LoggerContext, _LoggerContext,
Layout,
theme,
} from 'flipper-plugin'; } from 'flipper-plugin';
import isProduction from './utils/isProduction'; import isProduction from './utils/isProduction';
import {Button, Input, Result, Typography} from 'antd';
import constants from './fb-stubs/constants';
import styled from '@emotion/styled';
import {CopyOutlined} from '@ant-design/icons';
import {clipboard} from 'electron/common';
if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') {
// By default Node.JS has its internal certificate storage and doesn't use // By default Node.JS has its internal certificate storage and doesn't use
@@ -58,7 +65,63 @@ enableMapSet();
GK.init(); GK.init();
const AppFrame = ({logger}: {logger: Logger}) => ( class AppFrame extends React.Component<
{logger: Logger},
{error: any; errorInfo: any}
> {
state = {error: undefined as any, errorInfo: undefined as any};
getError() {
return this.state.error
? `${this.state.error}\n\nComponent stack:\n${this.state.errorInfo?.componentStack}\n\nError stacktrace:\n${this.state.error?.stack}`
: '';
}
render() {
const {logger} = this.props;
return this.state.error ? (
<Layout.Container grow center pad={80} style={{height: '100%'}}>
<Layout.Top style={{maxWidth: 800, height: '100%'}}>
<Result
status="error"
title="Detected a Flipper crash"
subTitle={
<p>
A crash was detected in the Flipper chrome. Filing a{' '}
<Typography.Link
href={
constants.IS_PUBLIC_BUILD
? 'https://github.com/facebook/flipper/issues/new/choose'
: constants.FEEDBACK_GROUP_LINK
}>
bug report
</Typography.Link>{' '}
would be appreciated! Please include the details below.
</p>
}
extra={[
<Button
key="copy_error"
icon={<CopyOutlined />}
onClick={() => {
clipboard.writeText(this.getError());
}}>
Copy error
</Button>,
<Button
key="retry_error"
type="primary"
onClick={() => {
this.setState({error: undefined, errorInfo: undefined});
}}>
Retry
</Button>,
]}
/>
<CodeBlock value={this.getError()} readOnly />
</Layout.Top>
</Layout.Container>
) : (
<_LoggerContext.Provider value={logger}> <_LoggerContext.Provider value={logger}>
<Provider store={store}> <Provider store={store}>
<CacheProvider value={cache}> <CacheProvider value={cache}>
@@ -74,7 +137,21 @@ const AppFrame = ({logger}: {logger: Logger}) => (
</CacheProvider> </CacheProvider>
</Provider> </Provider>
</_LoggerContext.Provider> </_LoggerContext.Provider>
); );
}
componentDidCatch(error: any, errorInfo: any) {
console.error(
`Flipper chrome crash: ${error}`,
error,
'\nComponents: ' + errorInfo?.componentStack,
);
this.setState({
error,
errorInfo,
});
}
}
function setProcessState(store: Store) { function setProcessState(store: Store) {
const settings = store.getState().settingsState; const settings = store.getState().settingsState;
@@ -146,3 +223,10 @@ const persistor = persistStore(store, undefined, () => {
}); });
setPersistor(persistor); setPersistor(persistor);
const CodeBlock = styled(Input.TextArea)({
fontFamily:
'SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;',
fontSize: '0.8em',
color: theme.textColorSecondary,
});

View File

@@ -139,6 +139,7 @@ type SplitLayoutProps = {
*/ */
center?: boolean; center?: boolean;
children: [React.ReactNode, React.ReactNode]; children: [React.ReactNode, React.ReactNode];
style?: React.HTMLAttributes<HTMLDivElement>['style'];
}; };
function renderSplitLayout( function renderSplitLayout(