Merge branch 'error-refactor' into 'main'
Rewrite ErrorBoundary as a functional component using `react-error-boundary` Closes #1555 See merge request soapbox-pub/soapbox!2836
This commit is contained in:
commit
166a7f27d3
11 changed files with 367 additions and 307 deletions
|
@ -63,9 +63,8 @@
|
|||
"@reach/rect": "^0.18.0",
|
||||
"@reach/tabs": "^0.18.0",
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"@sentry/browser": "^7.37.2",
|
||||
"@sentry/react": "^7.37.2",
|
||||
"@sentry/tracing": "^7.37.2",
|
||||
"@sentry/browser": "^7.74.1",
|
||||
"@sentry/react": "^7.74.1",
|
||||
"@tabler/icons": "^2.0.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
|
@ -134,6 +133,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",
|
||||
|
|
|
@ -1,222 +0,0 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
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 KVStore from 'soapbox/storage/kv-store';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { unregisterSW } from 'soapbox/utils/sw';
|
||||
|
||||
import SiteLogo from './site-logo';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
interface Props extends ReturnType<typeof mapStateToProps> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean;
|
||||
error: any;
|
||||
componentStack: any;
|
||||
browser?: Bowser.Parser.Parser;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
|
||||
state: State = {
|
||||
hasError: false,
|
||||
error: undefined,
|
||||
componentStack: undefined,
|
||||
browser: undefined,
|
||||
};
|
||||
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
componentDidCatch(error: any, info: any): void {
|
||||
captureException(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<HTMLTextAreaElement> = 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) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
KVStore.clear();
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
e.preventDefault();
|
||||
unregisterSW().then(goHome).catch(goHome);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { browser, hasError } = this.state;
|
||||
const { children, links } = this.props;
|
||||
|
||||
if (!hasError) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const isProduction = BuildConfig.NODE_ENV === 'production';
|
||||
|
||||
const errorText = this.getErrorText();
|
||||
|
||||
return (
|
||||
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'>
|
||||
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex shrink-0 justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='py-8'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.body'
|
||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Text theme='muted'>
|
||||
<Text weight='medium' tag='span' theme='muted'>{sourceCode.displayName}:</Text>
|
||||
|
||||
{' '}{sourceCode.version}
|
||||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
{' '}
|
||||
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isProduction && (
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
{errorText && (
|
||||
<textarea
|
||||
ref={this.setTextareaRef}
|
||||
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
|
||||
value={errorText}
|
||||
onClick={this.handleCopy}
|
||||
dir='ltr'
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
|
||||
{browser && (
|
||||
<Stack>
|
||||
<Text weight='semibold'><FormattedMessage id='alert.unexpected.browser' defaultMessage='Browser' /></Text>
|
||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{links.get('help') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{links.get('support') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
function mapStateToProps(state: RootState) {
|
||||
const { links, logo } = getSoapboxConfig(state);
|
||||
|
||||
return {
|
||||
siteTitle: state.instance.title,
|
||||
logo,
|
||||
links,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ErrorBoundary);
|
66
src/components/sentry-feedback-form.tsx
Normal file
66
src/components/sentry-feedback-form.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Textarea, Form, Button, FormGroup, FormActions, Text } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import { captureSentryFeedback } from 'soapbox/sentry';
|
||||
|
||||
interface ISentryFeedbackForm {
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
/** Accept feedback for the given Sentry event. */
|
||||
const SentryFeedbackForm: React.FC<ISentryFeedbackForm> = ({ eventId }) => {
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const [feedback, setFeedback] = useState<string>();
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState<boolean>(false);
|
||||
|
||||
const handleFeedbackChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
setFeedback(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmitFeedback: React.FormEventHandler = async (_e) => {
|
||||
if (!feedback || !eventId) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
await captureSentryFeedback({
|
||||
name: account?.acct,
|
||||
event_id: eventId,
|
||||
comments: feedback,
|
||||
}).catch(console.error);
|
||||
|
||||
setIsSubmitted(true);
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='alert.unexpected.thanks' defaultMessage='Thanks for your feedback!' />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmitFeedback}>
|
||||
<FormGroup>
|
||||
<Textarea
|
||||
value={feedback}
|
||||
onChange={handleFeedbackChange}
|
||||
placeholder='Anything you can tell us about what happened?'
|
||||
disabled={isSubmitting}
|
||||
autoGrow
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit' className='mx-auto' disabled={!feedback || isSubmitting}>
|
||||
<FormattedMessage id='alert.unexpected.submit_feedback' defaultMessage='Submit Feedback' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SentryFeedbackForm;
|
199
src/components/site-error-boundary.tsx
Normal file
199
src/components/site-error-boundary.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
import React, { type ErrorInfo, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { NODE_ENV } from 'soapbox/build-config';
|
||||
import { HStack, Text, Stack, Textarea } 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';
|
||||
import { unregisterSW } from 'soapbox/utils/sw';
|
||||
|
||||
import SentryFeedbackForm from './sentry-feedback-form';
|
||||
import SiteLogo from './site-logo';
|
||||
|
||||
interface ISiteErrorBoundary {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Application-level error boundary. Fills the whole screen. */
|
||||
const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||
const { links, sentryDsn } = useSoapboxConfig();
|
||||
const textarea = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [error, setError] = useState<unknown>();
|
||||
const [componentStack, setComponentStack] = useState<string>();
|
||||
const [browser, setBrowser] = useState<Bowser.Parser.Parser>();
|
||||
const [sentryEventId, setSentryEventId] = useState<string>();
|
||||
|
||||
const sentryEnabled = Boolean(sentryDsn);
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
const errorText = String(error) + componentStack;
|
||||
|
||||
const clearCookies: React.MouseEventHandler = (e) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
KVStore.clear();
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
e.preventDefault();
|
||||
unregisterSW().then(goHome).catch(goHome);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy: React.MouseEventHandler = () => {
|
||||
if (!textarea.current) return;
|
||||
|
||||
textarea.current.select();
|
||||
textarea.current.setSelectionRange(0, 99999);
|
||||
|
||||
document.execCommand('copy');
|
||||
};
|
||||
|
||||
function handleError(error: Error, info: ErrorInfo) {
|
||||
setError(error);
|
||||
setComponentStack(info.componentStack);
|
||||
|
||||
captureSentryException(error, {
|
||||
tags: {
|
||||
// Allow page crashes to be easily searched in Sentry.
|
||||
ErrorBoundary: 'yes',
|
||||
},
|
||||
})
|
||||
.then((eventId) => setSentryEventId(eventId))
|
||||
.catch(console.error);
|
||||
|
||||
import('bowser')
|
||||
.then(({ default: Bowser }) => setBrowser(Bowser.getParser(window.navigator.userAgent)))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
const fallback = (
|
||||
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'>
|
||||
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex shrink-0 justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='py-8'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.body'
|
||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Text theme='muted'>
|
||||
<Text weight='medium' tag='span' theme='muted'>{sourceCode.displayName}:</Text>
|
||||
|
||||
{' '}{sourceCode.version}
|
||||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
{' '}
|
||||
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
{(isProduction) ? (
|
||||
(sentryEnabled && sentryEventId) && (
|
||||
<SentryFeedbackForm eventId={sentryEventId} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{errorText && (
|
||||
<Textarea
|
||||
ref={textarea}
|
||||
value={errorText}
|
||||
onClick={handleCopy}
|
||||
isCodeEditor
|
||||
rows={12}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
|
||||
{browser && (
|
||||
<Stack>
|
||||
<Text weight='semibold'><FormattedMessage id='alert.unexpected.browser' defaultMessage='Browser' /></Text>
|
||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<SiteErrorBoundaryLink href={links.get('status')!}>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
||||
{links.get('help') && (
|
||||
<SiteErrorBoundaryLink href={links.get('help')!}>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
||||
{links.get('support') && (
|
||||
<SiteErrorBoundaryLink href={links.get('support')!}>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={fallback} onError={handleError}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISiteErrorBoundaryLink {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SiteErrorBoundaryLink({ href, children }: ISiteErrorBoundaryLink) {
|
||||
return (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={href} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
{children}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SiteErrorBoundary;
|
|
@ -8,7 +8,7 @@ import { getTextDirection } from 'soapbox/utils/rtl';
|
|||
import Stack from '../stack/stack';
|
||||
import Text from '../text/text';
|
||||
|
||||
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'id' | 'maxLength' | 'onChange' | 'onKeyDown' | 'onPaste' | 'required' | 'disabled' | 'rows' | 'readOnly'> {
|
||||
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'id' | 'maxLength' | 'onChange' | 'onClick' | 'onKeyDown' | 'onPaste' | 'required' | 'disabled' | 'rows' | 'readOnly'> {
|
||||
/** Put the cursor into the input on mount. */
|
||||
autoFocus?: boolean;
|
||||
/** Allows the textarea height to grow while typing */
|
||||
|
@ -48,13 +48,14 @@ const Textarea = React.forwardRef(({
|
|||
autoGrow = false,
|
||||
maxRows = 10,
|
||||
minRows = 1,
|
||||
rows: initialRows = 4,
|
||||
theme = 'default',
|
||||
maxLength,
|
||||
value,
|
||||
...props
|
||||
}: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => {
|
||||
const length = value?.length || 0;
|
||||
const [rows, setRows] = useState<number>(autoGrow ? 1 : 4);
|
||||
const [rows, setRows] = useState<number>(autoGrow ? minRows : initialRows);
|
||||
const locale = useLocale();
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import React from 'react';
|
|||
* For testing logging/monitoring & previewing ErrorBoundary design.
|
||||
*/
|
||||
const IntentionalError: React.FC = () => {
|
||||
throw 'This error is intentional.';
|
||||
throw new Error('This error is intentional.');
|
||||
};
|
||||
|
||||
export default IntentionalError;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ScrollContext } from 'react-router-scroll-4';
|
|||
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||
import SiteErrorBoundary from 'soapbox/components/site-error-boundary';
|
||||
import {
|
||||
ModalContainer,
|
||||
OnboardingWizard,
|
||||
|
@ -18,8 +19,6 @@ import {
|
|||
} from 'soapbox/hooks';
|
||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||
|
||||
import ErrorBoundary from '../components/error-boundary';
|
||||
|
||||
const GdprBanner = React.lazy(() => import('soapbox/components/gdpr-banner'));
|
||||
const EmbeddedStatus = React.lazy(() => import('soapbox/features/embedded-status'));
|
||||
const UI = React.lazy(() => import('soapbox/features/ui'));
|
||||
|
@ -42,7 +41,7 @@ const SoapboxMount = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<SiteErrorBoundary>
|
||||
<BrowserRouter basename={BuildConfig.FE_SUBDIRECTORY}>
|
||||
<CompatRouter>
|
||||
<ScrollContext shouldUpdateScroll={shouldUpdateScroll}>
|
||||
|
@ -90,7 +89,7 @@ const SoapboxMount = () => {
|
|||
</ScrollContext>
|
||||
</CompatRouter>
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
</SiteErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -164,6 +164,8 @@
|
|||
"alert.unexpected.links.support": "Support",
|
||||
"alert.unexpected.message": "Something went wrong.",
|
||||
"alert.unexpected.return_home": "Return Home",
|
||||
"alert.unexpected.submit_feedback": "Submit Feedback",
|
||||
"alert.unexpected.thanks": "Thanks for your feedback!",
|
||||
"aliases.account.add": "Create alias",
|
||||
"aliases.account_label": "Old account:",
|
||||
"aliases.aliases_list_delete": "Unlink alias",
|
||||
|
|
|
@ -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<void> {
|
||||
try {
|
||||
const Sentry = await import('@sentry/react');
|
||||
Sentry.captureException(exception, captureContext);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export { captureException };
|
|
@ -2,19 +2,18 @@ import { NODE_ENV } from 'soapbox/build-config';
|
|||
import sourceCode from 'soapbox/utils/code';
|
||||
|
||||
import type { Account } from './schemas';
|
||||
import type { CaptureContext, UserFeedback } from '@sentry/types';
|
||||
import type { SetOptional } from 'type-fest';
|
||||
|
||||
/** Start Sentry. */
|
||||
async function startSentry(dsn: string): Promise<void> {
|
||||
const [Sentry, { Integrations: Integrations }] = await Promise.all([
|
||||
import('@sentry/react'),
|
||||
import('@sentry/tracing'),
|
||||
]);
|
||||
const Sentry = await import('@sentry/react');
|
||||
|
||||
Sentry.init({
|
||||
dsn,
|
||||
debug: false,
|
||||
enabled: NODE_ENV === 'production',
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
integrations: [new Sentry.BrowserTracing()],
|
||||
|
||||
// Filter events.
|
||||
// https://docs.sentry.io/platforms/javascript/configuration/filtering/
|
||||
|
@ -47,7 +46,7 @@ async function startSentry(dsn: string): Promise<void> {
|
|||
}
|
||||
|
||||
/** Associate the account with Sentry events. */
|
||||
async function setSentryAccount(account: Account) {
|
||||
async function setSentryAccount(account: Account): Promise<void> {
|
||||
const Sentry = await import('@sentry/react');
|
||||
|
||||
Sentry.setUser({
|
||||
|
@ -58,9 +57,30 @@ async function setSentryAccount(account: Account) {
|
|||
}
|
||||
|
||||
/** Remove the account from Sentry events. */
|
||||
async function unsetSentryAccount() {
|
||||
async function unsetSentryAccount(): Promise<void> {
|
||||
const Sentry = await import('@sentry/react');
|
||||
Sentry.setUser(null);
|
||||
}
|
||||
|
||||
export { startSentry, setSentryAccount, unsetSentryAccount };
|
||||
/** Capture the exception and report it to Sentry. */
|
||||
async function captureSentryException (
|
||||
exception: any,
|
||||
captureContext?: CaptureContext | undefined,
|
||||
): Promise<string> {
|
||||
const Sentry = await import('@sentry/react');
|
||||
return Sentry.captureException(exception, captureContext);
|
||||
}
|
||||
|
||||
/** Capture user feedback and report it to Sentry. */
|
||||
async function captureSentryFeedback(feedback: SetOptional<UserFeedback, 'name' | 'email'>): Promise<void> {
|
||||
const Sentry = await import('@sentry/react');
|
||||
Sentry.captureUserFeedback(feedback as UserFeedback);
|
||||
}
|
||||
|
||||
export {
|
||||
startSentry,
|
||||
setSentryAccount,
|
||||
unsetSentryAccount,
|
||||
captureSentryException,
|
||||
captureSentryFeedback,
|
||||
};
|
116
yarn.lock
116
yarn.lock
|
@ -2026,68 +2026,69 @@
|
|||
"@noble/hashes" "~1.3.0"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@sentry/browser@7.37.2", "@sentry/browser@^7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.37.2.tgz#355dd28ad12677d63e0b12c5209d12b3f98ac3a4"
|
||||
integrity sha512-UvKfpx6+BUdV+rGAXqDBznagfz44Ut+x2h/i0OZPNCEpXaH9KAQOlv06I66861aXiucWFRb1lAMrN4+cE9aJIg==
|
||||
"@sentry-internal/tracing@7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.74.1.tgz#55ff387e61d2c9533a9a0d099d376332426c8e08"
|
||||
integrity sha512-nNaiZreQxCitG2PzYPaC7XtyA9OMsETGYMKAtiK4p62/uTmeYbsBva9BoNx1XeiHRwbrVQYRMKQ9nV5e2jS4/A==
|
||||
dependencies:
|
||||
"@sentry/core" "7.37.2"
|
||||
"@sentry/replay" "7.37.2"
|
||||
"@sentry/types" "7.37.2"
|
||||
"@sentry/utils" "7.37.2"
|
||||
tslib "^1.9.3"
|
||||
"@sentry/core" "7.74.1"
|
||||
"@sentry/types" "7.74.1"
|
||||
"@sentry/utils" "7.74.1"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/core@7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.37.2.tgz#959b2bf953f442b07f8377d90f4f7735cf260ae4"
|
||||
integrity sha512-LjofMDSTyVeBErl9N7TTqlyEVuW1g6U4iuJtdZ75JohnvVxzWdpZfWfddwQ6h7nGWfe9dNg0fGs1wxKtMhY+MA==
|
||||
"@sentry/browser@7.74.1", "@sentry/browser@^7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.74.1.tgz#9302d440bbdcb018abd5fee5959dab4b2fe97383"
|
||||
integrity sha512-OYWNne/KO60lOvkIpIlJUyiJt/9j8DGI57thSDFEYSmmbNqMitczUTBOaEStouvHKyfchqLZm1CZfWKt+z0VOA==
|
||||
dependencies:
|
||||
"@sentry/types" "7.37.2"
|
||||
"@sentry/utils" "7.37.2"
|
||||
tslib "^1.9.3"
|
||||
"@sentry-internal/tracing" "7.74.1"
|
||||
"@sentry/core" "7.74.1"
|
||||
"@sentry/replay" "7.74.1"
|
||||
"@sentry/types" "7.74.1"
|
||||
"@sentry/utils" "7.74.1"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/react@^7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.37.2.tgz#f5ecc4071c5dd0e8446103c24e94edda520a1217"
|
||||
integrity sha512-e5NFQAwHSGVyMUGYjvYXLI/QECkXkZ2BNUo+OHr5mAYqcIyGSA38tX7RJetrhonVjjpJp/ZVzlOyxQkpnBfBLw==
|
||||
"@sentry/core@7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.74.1.tgz#9e33cf59b754a994e4054c47c74df1d3fbd30d3c"
|
||||
integrity sha512-LvEhOSfdIvwkr+PdlrT/aA/iOLhkXrSkvjqAQyogE4ddCWeYfS0NoirxNt1EaxMBAWKhYZRqzkA7WA4LDLbzlA==
|
||||
dependencies:
|
||||
"@sentry/browser" "7.37.2"
|
||||
"@sentry/types" "7.37.2"
|
||||
"@sentry/utils" "7.37.2"
|
||||
"@sentry/types" "7.74.1"
|
||||
"@sentry/utils" "7.74.1"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/react@^7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.74.1.tgz#43517f8e42cfab917ed909d2fce76b265febb9c7"
|
||||
integrity sha512-16oTsNi2hl/S5AL/e5bo9DQZDwXPkX0nC8ajrpU0z2pH4cwjQZUZt/9Xq1+MKqDIEZkqDcMwpTmBptOvy1Pvkw==
|
||||
dependencies:
|
||||
"@sentry/browser" "7.74.1"
|
||||
"@sentry/types" "7.74.1"
|
||||
"@sentry/utils" "7.74.1"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
tslib "^1.9.3"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/replay@7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.37.2.tgz#eb49b7a1286335c1a4de49b386d0258e5c789682"
|
||||
integrity sha512-y8Gfc7EGfGU4eVae5HAtch2YgkiTzNPi16dcqPX9jtIHDwiurGqWcaOgs5HoGJm45eMfl6LvcE7MPbwqcDkPIA==
|
||||
"@sentry/replay@7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.74.1.tgz#dcb5040a3b0a9bda160b70cde5368ecbb4f0e782"
|
||||
integrity sha512-qmbOl+jYdyhoHFbPp9WemKx8UojID5hVmuVLxNIP0ANqAwmE9OQEK9YFg2cf7L/TpKb1tqz0qLgi5MYIdcdpgQ==
|
||||
dependencies:
|
||||
"@sentry/core" "7.37.2"
|
||||
"@sentry/types" "7.37.2"
|
||||
"@sentry/utils" "7.37.2"
|
||||
"@sentry/core" "7.74.1"
|
||||
"@sentry/types" "7.74.1"
|
||||
"@sentry/utils" "7.74.1"
|
||||
|
||||
"@sentry/tracing@^7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.37.2.tgz#88f149aea6a4d5a3cfb9145868d885fac9fffb52"
|
||||
integrity sha512-XBVvxbV5TADq2rHg/kJqGqDfOP8n2myMUxMMpfHMb38NrxkxQwXy+gDe41bA8FJKA2k7Y3Wkn8ZC/PelQ8c+ag==
|
||||
"@sentry/types@7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.74.1.tgz#b6f9b1bd266254f1f8b55fbcc92fa649ba2100ed"
|
||||
integrity sha512-2jIuPc+YKvXqZETwr2E8VYnsH1zsSUR/wkIvg1uTVeVNyoowJv+YsOtCdeGyL2AwiotUBSPKu7O1Lz0kq5rMOQ==
|
||||
|
||||
"@sentry/utils@7.74.1":
|
||||
version "7.74.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.74.1.tgz#e9a8453c954d02ebed2fd3dbe7588483d8f6d3cb"
|
||||
integrity sha512-qUsqufuHYcy5gFhLZslLxA5kcEOkkODITXW3c7D+x+8iP/AJqa8v8CeUCVNS7RetHCuIeWAbbTClC4c411EwQg==
|
||||
dependencies:
|
||||
"@sentry/core" "7.37.2"
|
||||
"@sentry/types" "7.37.2"
|
||||
"@sentry/utils" "7.37.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.37.2.tgz#99fd76230d7c1d3c6901ed4c0bea35be7d6fe26d"
|
||||
integrity sha512-SxKQOCX94ZaQM4C2ysNjHdJsjYapu/NYZCz1cnPyCdDvYfhwiVge1uq6ZHiQ/ARfxAAOmc3R4Mh3VvEz7WUOdw==
|
||||
|
||||
"@sentry/utils@7.37.2":
|
||||
version "7.37.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.37.2.tgz#14dea644454e3df247fb113fc834f509c1f0e48c"
|
||||
integrity sha512-5irN1nN/mtdOoWwsJiwBK0gPgNMkciUubEMbCaaXqJaGyGz8+yfDvXj7L+xGYiU57z+7+QkkSKxKEZ/IcBpjVQ==
|
||||
dependencies:
|
||||
"@sentry/types" "7.37.2"
|
||||
tslib "^1.9.3"
|
||||
"@sentry/types" "7.74.1"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sinclair/typebox@^0.27.8":
|
||||
version "0.27.8"
|
||||
|
@ -7186,6 +7187,13 @@ react-error-boundary@^3.1.0, react-error-boundary@^3.1.4:
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-error-boundary@^4.0.11:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
|
||||
integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-event-listener@^0.6.0:
|
||||
version "0.6.6"
|
||||
resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a"
|
||||
|
@ -8555,12 +8563,12 @@ tsconfig-paths@^3.14.2:
|
|||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^1.9.0, tslib@^1.9.3:
|
||||
tslib@^1.9.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.3, tslib@^2.3.1, tslib@^2.4.0:
|
||||
tslib@^2.0.3, tslib@^2.3.1, tslib@^2.4.0, "tslib@^2.4.1 || ^1.9.3":
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
|
Loading…
Reference in a new issue