Skip to content

Commit f870a50

Browse files
feat: OPTIC-1746: Improve global error message handling by showing toast messages in favour of modals with increased Sentry reporting (#7167)
Co-authored-by: robot-ci-heartex <[email protected]>
1 parent 79c979d commit f870a50

File tree

9 files changed

+234
-49
lines changed

9 files changed

+234
-49
lines changed

web/apps/labelstudio/src/app/App.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ const App = ({ content }) => {
7272
<QueryClientProvider client={LSQueryClient} key="query" />,
7373
<JotaiProvider key="jotai" store={JotaiStore} />,
7474
<AppStoreProvider key="app-store" />,
75+
<ToastProvider key="toast" />,
7576
<ApiProvider key="api" />,
7677
<ConfigProvider key="config" />,
7778
<RoutesProvider key="rotes" />,
7879
<ProjectProvider key="project" />,
79-
<ToastProvider key="toast" />,
8080
<CurrentUserProvider key="current-user" />,
8181
isFF(FF_PRODUCT_TOUR) && <TourProvider useAPI={useAPI} />,
8282
].filter(Boolean)}

web/apps/labelstudio/src/app/ErrorBoundary.jsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { Component } from "react";
22
import { ErrorWrapper } from "../components/Error/Error";
33
import { Modal } from "../components/Modal/ModalPopup";
4+
import { captureException } from "../config/Sentry";
5+
import { isFF } from "../utils/feature-flags";
6+
import { IMPROVE_GLOBAL_ERROR_MESSAGES } from "../providers/ApiProvider";
47

58
export const ErrorContext = React.createContext();
69

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

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

47+
// We will capture the stacktrace in Sentry, so we don't need to show it in the modal
48+
// It is not actionable to the user, let's not show it
49+
const stacktrace = isFF(IMPROVE_GLOBAL_ERROR_MESSAGES)
50+
? undefined
51+
: `${errorInfo ? `Component Stack: ${errorInfo}` : ""}\n\n${this.state.error?.stack ?? ""}`;
52+
3853
return (
3954
<Modal onHide={() => location.reload()} style={{ width: "60vw" }} visible bare>
4055
<div style={{ padding: 40 }}>
4156
<ErrorWrapper
4257
title="Runtime error"
4358
message={error}
44-
stacktrace={`${errorInfo ? `Component Stack: ${errorInfo}` : ""}\n\n${this.state.error?.stack ?? ""}`}
59+
stacktrace={stacktrace}
4560
onGoBack={goBack}
4661
onReload={() => location.reload()}
4762
/>

web/apps/labelstudio/src/components/Error/InlineError.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const InlineError = ({ children, includeValidation, className, style }) =
77
const context = React.useContext(ApiContext);
88

99
React.useEffect(() => {
10-
context.showModal = false;
10+
context.showGlobalError = false;
1111
}, [context]);
1212

1313
return context.error ? (

web/apps/labelstudio/src/components/Form/Form.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
FormValidationContext,
1919
} from "./FormContext";
2020
import * as Validators from "./Validation/Validators";
21+
import { ToastProvider, ToastViewport } from "@humansignal/ui";
2122

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

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

@@ -86,6 +88,7 @@ export default class Form extends React.Component {
8688
<ValidationRenderer validation={this.state.validation} />
8789
)}
8890
</form>
91+
<ToastViewport />
8992
</MultiProvider>
9093
);
9194
}

web/apps/labelstudio/src/components/Modal/Modal.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { cn } from "../../utils/bem";
88
import { Button } from "../Button/Button";
99
import { Space } from "../Space/Space";
1010
import { Modal } from "./ModalPopup";
11+
import { ToastProvider, ToastViewport } from "@humansignal/ui";
1112

1213
const standaloneModal = (props) => {
1314
const modalRef = createRef();
@@ -27,7 +28,12 @@ const standaloneModal = (props) => {
2728
providers={
2829
props.simple
2930
? []
30-
: [<ConfigProvider key="config" />, <ApiProvider key="api" />, <CurrentUserProvider key="current-user" />]
31+
: [
32+
<ConfigProvider key="config" />,
33+
<ToastProvider key="toast" />,
34+
<ApiProvider key="api" />,
35+
<CurrentUserProvider key="current-user" />,
36+
]
3137
}
3238
>
3339
<Modal
@@ -40,6 +46,7 @@ const standaloneModal = (props) => {
4046
}}
4147
animateAppearance={animate}
4248
/>
49+
{!props.simple && <ToastViewport />}
4350
</MultiProvider>,
4451
rootDiv,
4552
);

web/apps/labelstudio/src/config/Sentry.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,52 @@ import * as Sentry from "@sentry/browser";
22
import * as ReactSentry from "@sentry/react";
33
import type { RouterHistory } from "@sentry/react/build/types/reactrouter";
44
import { Route } from "react-router-dom";
5+
import { isDefined } from "../utils/helpers";
6+
7+
const SENTRY_DSN = APP_SETTINGS.sentry_dsn;
8+
const SENTRY_ENV = APP_SETTINGS.sentry_environment ?? process.env.NODE_ENV;
9+
const SENTRY_RATE = APP_SETTINGS.sentry_rate ? Number.parseFloat(APP_SETTINGS.sentry_rate) : 0.25;
10+
const SENTRY_ENABLED = APP_SETTINGS.debug === false && isDefined(SENTRY_DSN);
511

612
export const initSentry = (history: RouterHistory) => {
7-
if (APP_SETTINGS.debug === false && APP_SETTINGS.sentry_dsn) {
13+
if (SENTRY_ENABLED) {
814
setTags();
915
Sentry.init({
1016
dsn: APP_SETTINGS.sentry_dsn,
1117
integrations: [
1218
Sentry.browserTracingIntegration(),
1319
ReactSentry.reactRouterV5BrowserTracingIntegration({ history }),
1420
],
15-
environment: process.env.NODE_ENV,
21+
environment: SENTRY_ENV,
1622
// Set tracesSampleRate to 1.0 to capture 100%
1723
// of transactions for performance monitoring.
1824
// We recommend adjusting this value in production
19-
tracesSampleRate: 0.25,
25+
tracesSampleRate: SENTRY_RATE,
2026
release: getVersion(),
2127
});
2228
}
2329
};
2430

31+
export const captureMessage: typeof Sentry.captureMessage = (message, type) => {
32+
if (!SENTRY_ENABLED) {
33+
if (typeof type === "string" && type in console) {
34+
(console as any)[type](message);
35+
} else {
36+
console.log(message);
37+
}
38+
return "";
39+
}
40+
return Sentry.captureMessage(message, type);
41+
};
42+
43+
export const captureException: typeof Sentry.captureException = (exception, captureContext) => {
44+
if (!SENTRY_ENABLED) {
45+
console.error(exception, captureContext);
46+
return "";
47+
}
48+
return Sentry.captureException(exception, captureContext);
49+
};
50+
2551
const setTags = () => {
2652
const tags: Record<string, any> = {};
2753

0 commit comments

Comments
 (0)