From a7d78d0935de2f6acad01ab581b3df01894bef35 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 21 Oct 2023 14:18:56 -0500 Subject: [PATCH 01/15] Delete monitoring.ts, add `captureSentryException` to sentry.ts --- src/components/error-boundary.tsx | 4 ++-- src/monitoring.ts | 13 ------------- src/sentry.ts | 9 ++++++++- 3 files changed, 10 insertions(+), 16 deletions(-) delete mode 100644 src/monitoring.ts diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx index b25ca2aae..225b64023 100644 --- a/src/components/error-boundary.tsx +++ b/src/components/error-boundary.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import * as BuildConfig from 'soapbox/build-config'; import { HStack, Text, Stack } from 'soapbox/components/ui'; -import { captureException } from 'soapbox/monitoring'; +import { captureSentryException } from 'soapbox/sentry'; import KVStore from 'soapbox/storage/kv-store'; import sourceCode from 'soapbox/utils/code'; import { unregisterSW } from 'soapbox/utils/sw'; @@ -37,7 +37,7 @@ class ErrorBoundary extends React.PureComponent { textarea: HTMLTextAreaElement | null = null; componentDidCatch(error: any, info: any): void { - captureException(error, { + captureSentryException(error, { tags: { // Allow page crashes to be easily searched in Sentry. ErrorBoundary: 'yes', diff --git a/src/monitoring.ts b/src/monitoring.ts deleted file mode 100644 index 128b3a5ba..000000000 --- a/src/monitoring.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { CaptureContext } from '@sentry/types'; - -/** Capture the exception and report it to Sentry. */ -async function captureException (exception: any, captureContext?: CaptureContext | undefined): Promise { - try { - const Sentry = await import('@sentry/react'); - Sentry.captureException(exception, captureContext); - } catch (e) { - console.error(e); - } -} - -export { captureException }; diff --git a/src/sentry.ts b/src/sentry.ts index b5617c324..31cec656b 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -2,6 +2,7 @@ import { NODE_ENV } from 'soapbox/build-config'; import sourceCode from 'soapbox/utils/code'; import type { Account } from './schemas'; +import type { CaptureContext } from '@sentry/types'; /** Start Sentry. */ async function startSentry(dsn: string): Promise { @@ -63,4 +64,10 @@ async function unsetSentryAccount() { Sentry.setUser(null); } -export { startSentry, setSentryAccount, unsetSentryAccount }; \ No newline at end of file +/** Capture the exception and report it to Sentry. */ +async function captureSentryException (exception: any, captureContext?: CaptureContext | undefined): Promise { + const Sentry = await import('@sentry/react'); + Sentry.captureException(exception, captureContext); +} + +export { startSentry, setSentryAccount, unsetSentryAccount, captureSentryException }; \ No newline at end of file From c5d527a667c92449ca6710191b97ae054d527093 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 21 Oct 2023 14:21:21 -0500 Subject: [PATCH 02/15] Sentry: add return types, return eventId from `captureSentryException` --- src/sentry.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sentry.ts b/src/sentry.ts index 31cec656b..ae74e05ab 100644 --- a/src/sentry.ts +++ b/src/sentry.ts @@ -48,7 +48,7 @@ async function startSentry(dsn: string): Promise { } /** Associate the account with Sentry events. */ -async function setSentryAccount(account: Account) { +async function setSentryAccount(account: Account): Promise { const Sentry = await import('@sentry/react'); Sentry.setUser({ @@ -59,15 +59,18 @@ async function setSentryAccount(account: Account) { } /** Remove the account from Sentry events. */ -async function unsetSentryAccount() { +async function unsetSentryAccount(): Promise { const Sentry = await import('@sentry/react'); Sentry.setUser(null); } /** Capture the exception and report it to Sentry. */ -async function captureSentryException (exception: any, captureContext?: CaptureContext | undefined): Promise { +async function captureSentryException ( + exception: any, + captureContext?: CaptureContext | undefined, +): Promise { const Sentry = await import('@sentry/react'); - Sentry.captureException(exception, captureContext); + return Sentry.captureException(exception, captureContext); } export { startSentry, setSentryAccount, unsetSentryAccount, captureSentryException }; \ No newline at end of file From d7ea38cf225a967be87b0d3a7d31b37780b225cd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 21 Oct 2023 15:32:37 -0500 Subject: [PATCH 03/15] Rewrite ErrorBoundary as a functional component using `react-error-boundary` --- package.json | 1 + src/components/error-boundary.tsx | 328 +++++++++++++----------------- src/init/soapbox-mount.tsx | 7 +- yarn.lock | 7 + 4 files changed, 154 insertions(+), 189 deletions(-) diff --git a/package.json b/package.json index 184153f0e..c0161ccb6 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "react-color": "^2.19.3", "react-datepicker": "^4.8.0", "react-dom": "^18.0.0", + "react-error-boundary": "^4.0.11", "react-helmet": "^6.1.0", "react-hot-toast": "^2.4.0", "react-hotkeys": "^1.1.4", diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx index 225b64023..94ed65d3d 100644 --- a/src/components/error-boundary.tsx +++ b/src/components/error-boundary.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { ErrorInfo, useRef, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -import * as BuildConfig from 'soapbox/build-config'; +import { NODE_ENV } from 'soapbox/build-config'; import { HStack, Text, Stack } from 'soapbox/components/ui'; +import { useSoapboxConfig } from 'soapbox/hooks'; import { captureSentryException } from 'soapbox/sentry'; import KVStore from 'soapbox/storage/kv-store'; import sourceCode from 'soapbox/utils/code'; @@ -12,72 +12,23 @@ import { unregisterSW } from 'soapbox/utils/sw'; import SiteLogo from './site-logo'; -import type { RootState } from 'soapbox/store'; - -interface Props extends ReturnType { +interface ISiteErrorBoundary { children: React.ReactNode; } -type State = { - hasError: boolean; - error: any; - componentStack: any; - browser?: Bowser.Parser.Parser; -} +/** Application-level error boundary. Fills the whole screen. */ +const SiteErrorBoundary: React.FC = ({ children }) => { + const { links } = useSoapboxConfig(); + const textarea = useRef(null); -class ErrorBoundary extends React.PureComponent { + const [error, setError] = useState(); + const [componentStack, setComponentStack] = useState(); + const [browser, setBrowser] = useState(); - state: State = { - hasError: false, - error: undefined, - componentStack: undefined, - browser: undefined, - }; + const isProduction = NODE_ENV === 'production'; + const errorText = error + componentStack; - textarea: HTMLTextAreaElement | null = null; - - componentDidCatch(error: any, info: any): void { - captureSentryException(error, { - tags: { - // Allow page crashes to be easily searched in Sentry. - ErrorBoundary: 'yes', - }, - }); - - this.setState({ - hasError: true, - error, - componentStack: info && info.componentStack, - }); - - import('bowser') - .then(({ default: Bowser }) => { - this.setState({ - browser: Bowser.getParser(window.navigator.userAgent), - }); - }) - .catch(() => {}); - } - - setTextareaRef: React.RefCallback = c => { - this.textarea = c; - }; - - handleCopy: React.MouseEventHandler = () => { - if (!this.textarea) return; - - this.textarea.select(); - this.textarea.setSelectionRange(0, 99999); - - document.execCommand('copy'); - }; - - getErrorText = (): string => { - const { error, componentStack } = this.state; - return error + componentStack; - }; - - clearCookies: React.MouseEventHandler = (e) => { + const clearCookies: React.MouseEventHandler = (e) => { localStorage.clear(); sessionStorage.clear(); KVStore.clear(); @@ -88,135 +39,142 @@ class ErrorBoundary extends React.PureComponent { } }; - render() { - const { browser, hasError } = this.state; - const { children, links } = this.props; + const handleCopy: React.MouseEventHandler = () => { + if (!textarea.current) return; - if (!hasError) { - return children; - } + textarea.current.select(); + textarea.current.setSelectionRange(0, 99999); - const isProduction = BuildConfig.NODE_ENV === 'production'; + document.execCommand('copy'); + }; - const errorText = this.getErrorText(); + function handleError(error: Error, info: ErrorInfo) { + setError(error); + setComponentStack(info.componentStack); - return ( -
-
-
- - - -
+ captureSentryException(error, { + tags: { + // Allow page crashes to be easily searched in Sentry. + ErrorBoundary: 'yes', + }, + }); -
-
-

- -

-

- - - - ), - }} - /> -

- - - {sourceCode.displayName}: - - {' '}{sourceCode.version} - - - -
- - {!isProduction && ( -
- {errorText && ( -