Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: OPTIC-1746: Improve global error message handling by showing toast messages in favour of modals with increased Sentry reporting #7167

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion web/apps/labelstudio/src/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ const App = ({ content }) => {
<MultiProvider
providers={[
<AppStoreProvider key="app-store" />,
<ToastProvider key="toast" />,
<ApiProvider key="api" />,
<ConfigProvider key="config" />,
<RoutesProvider key="rotes" />,
<ProjectProvider key="project" />,
<ToastProvider key="toast" />,
<CurrentUserProvider key="current-user" />,
isFF(FF_PRODUCT_TOUR) && <TourProvider useAPI={useAPI} />,
].filter(Boolean)}
Expand Down
19 changes: 17 additions & 2 deletions web/apps/labelstudio/src/app/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React, { Component } from "react";
import { ErrorWrapper } from "../components/Error/Error";
import { Modal } from "../components/Modal/ModalPopup";
import { captureException } from "../config/Sentry";
import { isFF } from "../utils/feature-flags";
import { IMPROVE_GLOBAL_ERROR_MESSAGES } from "../providers/ApiProvider";

export const ErrorContext = React.createContext();

Expand All @@ -16,7 +19,13 @@ export default class ErrorBoundary extends Component {
}

componentDidCatch(error, { componentStack }) {
// You can also log the error to an error reporting service
// Capture the error in Sentry, so we can fix it directly
// Don't make the users copy and paste the stacktrace, it's not actionable
captureException(error, {
extra: {
component_stacktrace: componentStack,
},
});
this.setState({
error,
hasError: true,
Expand All @@ -35,13 +44,19 @@ export default class ErrorBoundary extends Component {
setTimeout(() => location.reload(), 32);
};

// We will capture the stacktrace in Sentry, so we don't need to show it in the modal
// It is not actionable to the user, let's not show it
const stacktrace = isFF(IMPROVE_GLOBAL_ERROR_MESSAGES)
? undefined
: `${errorInfo ? `Component Stack: ${errorInfo}` : ""}\n\n${this.state.error?.stack ?? ""}`;

return (
<Modal onHide={() => location.reload()} style={{ width: "60vw" }} visible bare>
<div style={{ padding: 40 }}>
<ErrorWrapper
title="Runtime error"
message={error}
stacktrace={`${errorInfo ? `Component Stack: ${errorInfo}` : ""}\n\n${this.state.error?.stack ?? ""}`}
stacktrace={stacktrace}
onGoBack={goBack}
onReload={() => location.reload()}
/>
Expand Down
2 changes: 1 addition & 1 deletion web/apps/labelstudio/src/components/Error/InlineError.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const InlineError = ({ children, includeValidation, className, style }) =
const context = React.useContext(ApiContext);

React.useEffect(() => {
context.showModal = false;
context.showGlobalError = false;
}, [context]);

return context.error ? (
Expand Down
3 changes: 3 additions & 0 deletions web/apps/labelstudio/src/components/Form/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
FormValidationContext,
} from "./FormContext";
import * as Validators from "./Validation/Validators";
import { ToastProvider, ToastViewport } from "@humansignal/ui";

const PASSWORD_PROTECTED_VALUE = "got ya, suspicious hacker!";

Expand Down Expand Up @@ -65,6 +66,7 @@ export default class Form extends React.Component {
<FormSubmissionContext.Provider key="form-submission-ctx" value={this.state.submitting} />,
<FormStateContext.Provider key="form-state-ctx" value={this.state.state} />,
<FormResponseContext.Provider key="form-response" value={this.state.lastResponse} />,
<ToastProvider key="toast" />,
<ApiProvider key="form-api" ref={this.apiRef} />,
];

Expand All @@ -86,6 +88,7 @@ export default class Form extends React.Component {
<ValidationRenderer validation={this.state.validation} />
)}
</form>
<ToastViewport />
</MultiProvider>
);
}
Expand Down
9 changes: 8 additions & 1 deletion web/apps/labelstudio/src/components/Modal/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cn } from "../../utils/bem";
import { Button } from "../Button/Button";
import { Space } from "../Space/Space";
import { Modal } from "./ModalPopup";
import { ToastProvider, ToastViewport } from "@humansignal/ui";

const standaloneModal = (props) => {
const modalRef = createRef();
Expand All @@ -27,7 +28,12 @@ const standaloneModal = (props) => {
providers={
props.simple
? []
: [<ConfigProvider key="config" />, <ApiProvider key="api" />, <CurrentUserProvider key="current-user" />]
: [
<ConfigProvider key="config" />,
<ToastProvider key="toast" />,
<ApiProvider key="api" />,
<CurrentUserProvider key="current-user" />,
]
}
>
<Modal
Expand All @@ -40,6 +46,7 @@ const standaloneModal = (props) => {
}}
animateAppearance={animate}
/>
{!props.simple && <ToastViewport />}
</MultiProvider>,
rootDiv,
);
Expand Down
32 changes: 29 additions & 3 deletions web/apps/labelstudio/src/config/Sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,52 @@ import * as Sentry from "@sentry/browser";
import * as ReactSentry from "@sentry/react";
import type { RouterHistory } from "@sentry/react/build/types/reactrouter";
import { Route } from "react-router-dom";
import { isDefined } from "../utils/helpers";

const SENTRY_DSN = APP_SETTINGS.sentry_dsn;
const SENTRY_ENV = APP_SETTINGS.sentry_environment ?? process.env.NODE_ENV;
const SENTRY_RATE = APP_SETTINGS.sentry_rate ? Number.parseFloat(APP_SETTINGS.sentry_rate) : 0.25;
const SENTRY_ENABLED = APP_SETTINGS.debug === false && isDefined(SENTRY_DSN);

export const initSentry = (history: RouterHistory) => {
if (APP_SETTINGS.debug === false && APP_SETTINGS.sentry_dsn) {
if (SENTRY_ENABLED) {
setTags();
Sentry.init({
dsn: APP_SETTINGS.sentry_dsn,
integrations: [
Sentry.browserTracingIntegration(),
ReactSentry.reactRouterV5BrowserTracingIntegration({ history }),
],
environment: process.env.NODE_ENV,
environment: SENTRY_ENV,
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 0.25,
tracesSampleRate: SENTRY_RATE,
release: getVersion(),
});
}
};

export const captureMessage: typeof Sentry.captureMessage = (message, type) => {
if (!SENTRY_ENABLED) {
if (typeof type === "string" && type in console) {
(console as any)[type](message);
} else {
console.log(message);
}
return "";
}
return Sentry.captureMessage(message, type);
};

export const captureException: typeof Sentry.captureException = (exception, captureContext) => {
if (!SENTRY_ENABLED) {
console.error(exception, captureContext);
return "";
}
return Sentry.captureException(exception, captureContext);
};

const setTags = () => {
const tags: Record<string, any> = {};

Expand Down
Loading
Loading