From 560be4b4e31931c2a69459ec6b87dd8690645d7b Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 24 Jan 2020 17:11:02 -0400 Subject: [PATCH] Add top-level error page (#7889) Any error caught during a React component render or lifecycle method will now be caught by the top-level error boundary, which shows the user this new error page. The error page will display a simple error message, and will show the details of the error in a collapsible section. The caught error is also reported to Sentry. In development the error will be re-thrown to make it easier to see on the console, but it is not re-thrown in production. --- app/_locales/en/messages.json | 32 ++++++++++++ ui/app/pages/error/error.component.js | 74 +++++++++++++++++++++++++++ ui/app/pages/error/index.js | 1 + ui/app/pages/error/index.scss | 41 +++++++++++++++ ui/app/pages/index.js | 52 ++++++++++++++----- ui/app/pages/index.scss | 2 + 6 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 ui/app/pages/error/error.component.js create mode 100644 ui/app/pages/error/index.js create mode 100644 ui/app/pages/error/index.scss diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index f904ffa50..55b423894 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -531,6 +531,38 @@ "enterPasswordContinue": { "message": "Enter password to continue" }, + "errorCode": { + "message": "Code: $1", + "description": "Displayed error code for debugging purposes. $1 is the error code" + }, + "errorDetails": { + "message": "Error Details", + "description": "Title for collapsible section that displays error details for debugging purposes" + }, + "errorMessage": { + "message": "Message: $1", + "description": "Displayed error message for debugging purposes. $1 is the error message" + }, + "errorName": { + "message": "Code: $1", + "description": "Displayed error name for debugging purposes. $1 is the error name" + }, + "errorPageTitle": { + "message": "MetaMask encountered an error", + "description": "Title of generic error page" + }, + "errorPageMessage": { + "message": "Try again by reloading the page, or contact support at support@metamask.io", + "description": "Message displayed on generic error page in the fullscreen or notification UI" + }, + "errorPagePopupMessage": { + "message": "Try again by closing and reopening the popup, or contact support at support@metamask.io", + "description": "Message displayed on generic error page in the popup UI" + }, + "errorStack": { + "message": "Stack:", + "description": "Title for error stack, which is displayed for debugging purposes" + }, "ethereumPublicAddress": { "message": "Ethereum Public Address" }, diff --git a/ui/app/pages/error/error.component.js b/ui/app/pages/error/error.component.js new file mode 100644 index 000000000..1da1ac609 --- /dev/null +++ b/ui/app/pages/error/error.component.js @@ -0,0 +1,74 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { getEnvironmentType } from '../../../../app/scripts/lib/util' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' + +class ErrorPage extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static propTypes = { + error: PropTypes.object.isRequired, + } + + renderErrorDetail (content) { + return ( +
  • +

    + {content} +

    +
  • + ) + } + + renderErrorStack (title, stack) { + return ( +
  • + + {title} + +
    +          {stack}
    +        
    +
  • + ) + } + + render () { + const { error } = this.props + const { t } = this.context + + const isPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP + + return ( +
    +

    + {t('errorPageTitle')} +

    +

    + { + isPopup + ? t('errorPagePopupMessage') + : t('errorPageMessage') + } +

    +
    +
    + + {t('errorDetails')} + +
      + { error.message ? this.renderErrorDetail(t('errorMessage', [error.message])) : null } + { error.code ? this.renderErrorDetail(t('errorCode', [error.code])) : null } + { error.name ? this.renderErrorDetail(t('errorName', [error.name])) : null } + { error.stack ? this.renderErrorStack(t('errorStack'), error.stack) : null } +
    +
    +
    +
    + ) + } +} + +export default ErrorPage diff --git a/ui/app/pages/error/index.js b/ui/app/pages/error/index.js new file mode 100644 index 000000000..c2a9f62d9 --- /dev/null +++ b/ui/app/pages/error/index.js @@ -0,0 +1 @@ +export { default } from './error.component' diff --git a/ui/app/pages/error/index.scss b/ui/app/pages/error/index.scss new file mode 100644 index 000000000..6784b374f --- /dev/null +++ b/ui/app/pages/error/index.scss @@ -0,0 +1,41 @@ +.error-page { + display: flex; + flex-flow: column nowrap; + align-items: center; + + font-family: Roboto; + font-style: normal; + font-weight: normal; + + padding: 35px 10px 10px 10px; + height: 100%; + + &__header { + display: flex; + justify-content: center; + font-size: 42px; + padding: 10px 0; + text-align: center; + } + + &__subheader { + font-size: 19px; + padding: 10px 0; + width: 100%; + max-width: 720px; + text-align: center; + } + + &__details { + font-size: 18px; + overflow-y: auto; + width: 100%; + max-width: 720px; + padding-top: 10px; + } + + &__stack { + overflow-x: auto; + background-color: #eee; + } +} diff --git a/ui/app/pages/index.js b/ui/app/pages/index.js index 40b2781a8..7de4fb408 100644 --- a/ui/app/pages/index.js +++ b/ui/app/pages/index.js @@ -1,25 +1,53 @@ -import React from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import { Provider } from 'react-redux' import { HashRouter } from 'react-router-dom' +import * as Sentry from '@sentry/browser' +import ErrorPage from './error' import Routes from './routes' import I18nProvider from '../helpers/higher-order-components/i18n-provider' import MetaMetricsProvider from '../helpers/higher-order-components/metametrics/metametrics.provider' -const Index = props => { - const { store } = props +class Index extends PureComponent { + state = {} - return ( - - - + static getDerivedStateFromError (error) { + return { error } + } + + componentDidCatch (error) { + Sentry.captureException(error) + } + + render () { + const { error, errorId } = this.state + const { store } = this.props + + if (error) { + return ( + - + - - - - ) + + ) + } + + return ( + + + + + + + + + + ) + } } Index.propTypes = { diff --git a/ui/app/pages/index.scss b/ui/app/pages/index.scss index deb6bce45..6325e0b0c 100644 --- a/ui/app/pages/index.scss +++ b/ui/app/pages/index.scss @@ -2,6 +2,8 @@ @import 'add-token/index'; +@import 'error/index'; + @import 'send/send'; @import 'confirm-add-token/index';