Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NO QA]Feature: Critical Error Handling #3527

Merged
merged 8 commits into from
Jun 11, 2021
5 changes: 4 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React from 'react';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import CustomStatusBar from './components/CustomStatusBar';
import ErrorBoundary from './components/ErrorBoundary';
import Expensify from './Expensify';

const App = () => (
<SafeAreaProvider>
<CustomStatusBar />
<Expensify />
<ErrorBoundary errorMessage="E.cash crash caught by error boundary">
<Expensify />
</ErrorBoundary>
</SafeAreaProvider>
);

Expand Down
52 changes: 52 additions & 0 deletions src/components/ErrorBoundary/BaseErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
/* A message posted to `logError` (along with error data) when this component intercepts an error */
errorMessage: PropTypes.string.isRequired,

/* A function to perform the actual logging since different platforms support different tools */
logError: PropTypes.func,

/* Actual content wrapped by this error boundary */
children: PropTypes.node.isRequired,
};

const defaultProps = {
logError: () => {},
};

/**
* This component captures an error in the child component tree and logs it to the server
* It can be used to wrap the entire app as well as to wrap specific parts for more granularity
* @see {@link https://reactjs.org/docs/error-boundaries.html#where-to-place-error-boundaries}
*/
class BaseErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}

static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return {hasError: true};
}

componentDidCatch(error, errorInfo) {
this.props.logError(this.props.errorMessage, error, errorInfo);
}

render() {
if (this.state.hasError) {
// For the moment we've decided not to render any fallback UI
return null;
}

return this.props.children;
}
}

BaseErrorBoundary.propTypes = propTypes;
BaseErrorBoundary.defaultProps = defaultProps;

export default BaseErrorBoundary;
9 changes: 9 additions & 0 deletions src/components/ErrorBoundary/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import BaseErrorBoundary from './BaseErrorBoundary';
import Log from '../../libs/Log';

BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => {
// Log the error to the server
Log.alert(errorMessage, 0, {error: error.message, errorInfo}, false);
};

export default BaseErrorBoundary;
16 changes: 16 additions & 0 deletions src/components/ErrorBoundary/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import crashlytics from '@react-native-firebase/crashlytics';

import BaseErrorBoundary from './BaseErrorBoundary';
import Log from '../../libs/Log';

BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => {
// Log the error to the server
Log.alert(errorMessage, 0, {error: error.message, errorInfo}, false);

/* On native we also log the error to crashlytics
* Since the error was handled we need to manually tell crashlytics about it */
crashlytics().log(`errorInfo: ${JSON.stringify(errorInfo)}`);
crashlytics().recordError(error);
};

export default BaseErrorBoundary;
7 changes: 7 additions & 0 deletions tests/unit/loginTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import React from 'react';
import renderer from 'react-test-renderer';
import App from '../../src/App';

/* <App> uses <ErrorBoundary> and we need to mock the imported crashlytics module
* due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 */
jest.mock('@react-native-firebase/crashlytics', () => () => ({
log: jest.fn(),
recordError: jest.fn(),
}));

describe('AppComponent', () => {
it('renders correctly', () => {
renderer.create(<App />);
Expand Down