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:
Alex Gleason 2023-10-21 23:48:45 +00:00
commit 166a7f27d3
11 changed files with 367 additions and 307 deletions

View file

@ -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",

View file

@ -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'>&rarr;</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);

View 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;

View 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'>&rarr;</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;

View file

@ -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>) => {

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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",

View file

@ -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 };

View file

@ -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
View file

@ -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==