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:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user