diff --git a/.env b/.env index 9cf725202..cc1811743 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ # ViteJS based VITE_VERSION=latest +VITE_MUTE_ERROR_BOUNDARY_LOG=false VITE_BASE_API_URL=http://localhost:8000/api/v2/ VITE_FORM_ID=93c09209-5fb9-4105-b6bb-9d9f0aa6782c VITE_USE_HASH_ROUTING=false diff --git a/src/components/Errors/ErrorBoundary.jsx b/src/components/Errors/ErrorBoundary.jsx index d7786c038..8eb62f127 100644 --- a/src/components/Errors/ErrorBoundary.jsx +++ b/src/components/Errors/ErrorBoundary.jsx @@ -1,9 +1,12 @@ +import * as Sentry from '@sentry/react'; +import {getEnv} from 'env.mjs'; import PropTypes from 'prop-types'; import React from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; import Body from 'components/Body'; import Card from 'components/Card'; +import FormUnavailable from 'components/Errors/FormUnavailable'; import FormMaximumSubmissions from 'components/FormMaximumSubmissions'; import Link from 'components/Link'; import MaintenanceMode from 'components/MaintenanceMode'; @@ -13,7 +16,12 @@ import {DEBUG} from 'utils'; import ErrorMessage from './ErrorMessage'; const logError = (error, errorInfo) => { - if (DEBUG) console.error(error, errorInfo); + if (DEBUG) { + const muteConsole = getEnv('MUTE_ERROR_BOUNDARY_LOG'); + if (!muteConsole) console.error(error, errorInfo); + } else { + Sentry.captureException(error); + } }; class ErrorBoundary extends React.Component { @@ -30,7 +38,6 @@ class ErrorBoundary extends React.Component { } componentDidCatch(error, errorInfo) { - // TODO: depending on the error type, send to sentry? logError(error, errorInfo); } @@ -139,13 +146,9 @@ const UnprocessableEntityError = ({wrapper: Wrapper, error}) => { UnprocessableEntityError.propTypes = GenericError.propTypes; const ServiceUnavailableError = ({wrapper: Wrapper, error}) => { - if (!['form-maintenance', 'form-maximum-submissions'].includes(error.code)) { - return ; - } - - // handle maintenance mode forms - if (error.code === 'form-maintenance') { - return ( + const defaultComponent = ; + const componentMapping = { + 'form-maintenance': ( { /> } /> - ); - } + ), + 'form-maximum-submissions': , + service_unavailable: , + }; - // handle submission limit forms - if (error.code === 'form-maximum-submissions') { - return ; - } + return componentMapping[error.code] || defaultComponent; }; // map the error class to the component to render it diff --git a/src/components/Errors/ErrorBoundary.stories.jsx b/src/components/Errors/ErrorBoundary.stories.jsx new file mode 100644 index 000000000..e9d1a24ea --- /dev/null +++ b/src/components/Errors/ErrorBoundary.stories.jsx @@ -0,0 +1,103 @@ +import {MemoryRouter} from 'react-router'; + +import {PermissionDenied, ServiceUnavailable, UnprocessableEntity} from 'errors'; + +import ErrorBoundary from './ErrorBoundary'; + +const Nested = ({error}) => { + throw error; +}; + +const render = ({useCard, errorType, errorCode}) => { + const error = new errorType('some error', 500, 'some error', errorCode); + return ( + + + + ); +}; + +export default { + title: 'Private API / ErrorBoundary', + component: ErrorBoundary, + render, + argTypes: { + useCard: {control: {type: 'boolean'}}, + errorType: { + table: { + options: [PermissionDenied, ServiceUnavailable, UnprocessableEntity], + control: {type: 'radio'}, + }, + }, + }, +}; + +export const GenericError = { + args: { + useCard: true, + errorType: Error, + errorCode: 'generic', + }, +}; + +export const PermissionDeniedError = { + decorators: [ + Story => ( + + + + ), + ], + args: { + useCard: true, + errorType: PermissionDenied, + }, +}; + +export const UnprocessableEntityErrorInactive = { + args: { + useCard: true, + errorType: UnprocessableEntity, + errorCode: 'form-inactive', + }, +}; + +export const UnprocessableEntityErrorGeneric = { + args: { + useCard: true, + errorType: UnprocessableEntity, + errorCode: 'generic', + }, +}; + +export const ServiceUnavailableErrorMaintenance = { + args: { + useCard: true, + errorType: ServiceUnavailable, + errorCode: 'form-maintenance', + }, +}; + +export const ServiceUnavailableErrorMaxSubmissions = { + args: { + useCard: true, + errorType: ServiceUnavailable, + errorCode: 'form-maximum-submissions', + }, +}; + +export const ServiceUnavailableError = { + args: { + useCard: true, + errorType: ServiceUnavailable, + errorCode: 'service_unavailable', + }, +}; + +export const ServiceUnavailableErrorGeneric = { + args: { + useCard: true, + errorType: ServiceUnavailable, + errorCode: 'generic', + }, +}; diff --git a/src/components/Errors/FormUnavailable.jsx b/src/components/Errors/FormUnavailable.jsx new file mode 100644 index 000000000..17c9e6a18 --- /dev/null +++ b/src/components/Errors/FormUnavailable.jsx @@ -0,0 +1,24 @@ +import {FormattedMessage, useIntl} from 'react-intl'; + +import ErrorMessage from 'components/Errors/ErrorMessage'; + +const FormUnavailable = ({wrapper: Wrapper}) => { + const intl = useIntl(); + // Wrapper may be a DOM element, which can't handle + const title = intl.formatMessage({ + description: 'Open Forms service unavailable error title', + defaultMessage: 'Form unavailable', + }); + return ( + + + + + + ); +}; + +export default FormUnavailable;