From 939a971779aab1742c9d88b297b4885861a46dea Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 10 Jul 2024 12:47:05 +0200 Subject: [PATCH 1/2] Add error handling --- .../Main/RegistrationCard/index.tsx | 4 ++-- .../RegisterStep/index.tsx | 6 +++-- .../CreateEnvironmentWizard/index.tsx | 4 +++- src/components/Tests/TestCard/index.tsx | 3 +-- src/components/common/App/index.tsx | 18 ++++++++++++-- .../ErrorScreen/ErrorScreen.stories.tsx | 18 ++++++++++++++ src/components/common/ErrorScreen/index.tsx | 20 ++++++++++++++++ src/components/common/ErrorScreen/styles.ts | 11 +++++++++ src/containers/Dashboard/index.tsx | 5 ++++ src/containers/Documentation/index.tsx | 5 ++++ src/containers/InstallationWizard/index.tsx | 12 ++++++++++ src/containers/Main/index.tsx | 5 ++++ src/containers/Main/router.tsx | 10 +++++++- src/containers/Notifications/index.tsx | 5 ++++ src/containers/RecentActivity/index.tsx | 5 ++++ src/containers/Troubleshooting/index.tsx | 5 ++++ src/trackingEvents.ts | 2 ++ src/utils/actions/sendErrorTrackingEvent.ts | 24 +++++++++++++++++++ src/utils/handleUncaughtError.ts | 22 +++++++++++++++++ 19 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 src/components/common/ErrorScreen/ErrorScreen.stories.tsx create mode 100644 src/components/common/ErrorScreen/index.tsx create mode 100644 src/components/common/ErrorScreen/styles.ts create mode 100644 src/utils/actions/sendErrorTrackingEvent.ts create mode 100644 src/utils/handleUncaughtError.ts diff --git a/src/components/Main/RegistrationCard/index.tsx b/src/components/Main/RegistrationCard/index.tsx index be8ff9520..1a952f2d0 100644 --- a/src/components/Main/RegistrationCard/index.tsx +++ b/src/components/Main/RegistrationCard/index.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { SLACK_WORKSPACE_URL } from "../../../constants"; import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser"; -import { sendTrackingEvent } from "../../../utils/actions/sendTrackingEvent"; +import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { SlackLogoIcon } from "../../common/icons/16px/SlackLogoIcon"; import { CrossIcon } from "../../common/icons/CrossIcon"; import { trackingEvents } from "../tracking"; @@ -25,7 +25,7 @@ export const RegistrationCard = ({ const registrationCardRef = useRef(null); const handleSlackLinkClick = () => { - sendTrackingEvent(trackingEvents.PROMOTION_SLACK_LINK_CLICKED); + sendUserActionTrackingEvent(trackingEvents.PROMOTION_SLACK_LINK_CLICKED); openURLInDefaultBrowser(SLACK_WORKSPACE_URL); }; diff --git a/src/components/RecentActivity/CreateEnvironmentWizard/RegisterStep/index.tsx b/src/components/RecentActivity/CreateEnvironmentWizard/RegisterStep/index.tsx index 271473add..434654eda 100644 --- a/src/components/RecentActivity/CreateEnvironmentWizard/RegisterStep/index.tsx +++ b/src/components/RecentActivity/CreateEnvironmentWizard/RegisterStep/index.tsx @@ -2,7 +2,7 @@ import { KeyboardEvent, useContext, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { actions as globalActions } from "../../../../actions"; import { usePrevious } from "../../../../hooks/usePrevious"; -import { sendTrackingEvent } from "../../../../utils/actions/sendTrackingEvent"; +import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; import { isValidEmailFormat } from "../../../../utils/isValidEmailFormat"; import { ConfigContext } from "../../../common/App/ConfigContext"; import { EnvelopeIcon } from "../../../common/icons/16px/EnvelopeIcon"; @@ -72,7 +72,9 @@ export const RegisterStep = ({ onNext }: RegisterStepProps) => { }, [setFocus]); const onSubmit = (data: RegistrationFormValues) => { - sendTrackingEvent(trackingEvents.LOCAL_REGISTRATION_FORM_SUBMITTED); + sendUserActionTrackingEvent( + trackingEvents.LOCAL_REGISTRATION_FORM_SUBMITTED + ); window.sendMessageToDigma({ action: globalActions.PERSONALIZE_REGISTER, payload: { diff --git a/src/components/RecentActivity/CreateEnvironmentWizard/index.tsx b/src/components/RecentActivity/CreateEnvironmentWizard/index.tsx index 73d71d21d..ea6460092 100644 --- a/src/components/RecentActivity/CreateEnvironmentWizard/index.tsx +++ b/src/components/RecentActivity/CreateEnvironmentWizard/index.tsx @@ -155,7 +155,9 @@ export const CreateEnvironmentWizard = ({ } }); - sendTrackingEvent(trackingEvents.CREATE_NEW_ENVIRONMENT_FORM_SUBMITTED); + sendUserActionTrackingEvent( + trackingEvents.CREATE_NEW_ENVIRONMENT_FORM_SUBMITTED + ); return; } diff --git a/src/components/Tests/TestCard/index.tsx b/src/components/Tests/TestCard/index.tsx index ce57e613a..66d4ce427 100644 --- a/src/components/Tests/TestCard/index.tsx +++ b/src/components/Tests/TestCard/index.tsx @@ -1,6 +1,5 @@ import { isString } from "../../../typeGuards/isString"; import { changeScope } from "../../../utils/actions/changeScope"; -import { sendTrackingEvent } from "../../../utils/actions/sendTrackingEvent"; import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; import { formatEnvironmentName } from "../../../utils/formatEnvironmentName"; import { formatTimeDistance } from "../../../utils/formatTimeDistance"; @@ -105,7 +104,7 @@ export const TestCard = ({ const handleRunButtonClick = () => { if (test.spanInfo.methodCodeObjectId) { - sendTrackingEvent(trackingEvents.RUN_TEST_BUTTON_CLICKED); + sendUserActionTrackingEvent(trackingEvents.RUN_TEST_BUTTON_CLICKED); window.sendMessageToDigma({ action: actions.RUN_TEST, payload: { diff --git a/src/components/common/App/index.tsx b/src/components/common/App/index.tsx index 2193bee8c..064448614 100644 --- a/src/components/common/App/index.tsx +++ b/src/components/common/App/index.tsx @@ -1,12 +1,16 @@ -import { useContext, useEffect, useState } from "react"; +import { ErrorInfo, useContext, useEffect, useState } from "react"; +import { ErrorBoundary } from "react-error-boundary"; import { ThemeProvider } from "styled-components"; import { actions } from "../../../actions"; import { dispatcher } from "../../../dispatcher"; import { Theme } from "../../../globals"; +import { logger } from "../../../logging"; import { isBoolean } from "../../../typeGuards/isBoolean"; import { isNull } from "../../../typeGuards/isNull"; import { isObject } from "../../../typeGuards/isObject"; import { isString } from "../../../typeGuards/isString"; +import { sendErrorTrackingEvent } from "../../../utils/actions/sendErrorTrackingEvent"; +import { ErrorScreen } from "../ErrorScreen"; import { ConfigContext } from "./ConfigContext"; import { getStyledComponentsTheme } from "./getTheme"; import { GlobalStyle } from "./styles"; @@ -58,6 +62,14 @@ export const App = ({ theme, children }: AppProps) => { const [codeFont, setCodeFont] = useState(defaultCodeFont); const [config, setConfig] = useState(useContext(ConfigContext)); + const handleError = (error: Error, info: ErrorInfo) => { + logger.error(error, info); + sendErrorTrackingEvent(error, { + severity: "high", + level: "App react component" + }); + }; + useEffect(() => { if (theme) { setCurrentTheme(theme); @@ -443,7 +455,9 @@ export const App = ({ theme, children }: AppProps) => { - {children} + } onError={handleError}> + {children} + ); diff --git a/src/components/common/ErrorScreen/ErrorScreen.stories.tsx b/src/components/common/ErrorScreen/ErrorScreen.stories.tsx new file mode 100644 index 000000000..68760490d --- /dev/null +++ b/src/components/common/ErrorScreen/ErrorScreen.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { ErrorScreen } from "."; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: "common/ErrorScreen", + component: ErrorScreen, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout + layout: "fullscreen" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/common/ErrorScreen/index.tsx b/src/components/common/ErrorScreen/index.tsx new file mode 100644 index 000000000..0d8b2ce44 --- /dev/null +++ b/src/components/common/ErrorScreen/index.tsx @@ -0,0 +1,20 @@ +import { trackingEvents } from "../../../trackingEvents"; +import { sendUserActionTrackingEvent } from "../../../utils/actions/sendUserActionTrackingEvent"; +import { NewButton } from "../v3/NewButton"; +import * as s from "./styles"; + +export const ErrorScreen = () => { + const onRefreshButtonClick = () => { + sendUserActionTrackingEvent( + trackingEvents.ERROR_SCREEN_REFRESH_BUTTON_CLICKED + ); + window.location.href = "/"; + }; + + return ( + + Oops, something went wrong + + + ); +}; diff --git a/src/components/common/ErrorScreen/styles.ts b/src/components/common/ErrorScreen/styles.ts new file mode 100644 index 000000000..cd9da7720 --- /dev/null +++ b/src/components/common/ErrorScreen/styles.ts @@ -0,0 +1,11 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + height: 100%; + text-align: center; +`; diff --git a/src/containers/Dashboard/index.tsx b/src/containers/Dashboard/index.tsx index 19b62561f..d522a46e1 100644 --- a/src/containers/Dashboard/index.tsx +++ b/src/containers/Dashboard/index.tsx @@ -7,6 +7,7 @@ import { import { Dashboard } from "../../components/Dashboard"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { GlobalStyle } from "./styles"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,10 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError("dashboard", message, source, lineno, colno, error); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/containers/Documentation/index.tsx b/src/containers/Documentation/index.tsx index c587eb930..815e5dc6e 100644 --- a/src/containers/Documentation/index.tsx +++ b/src/containers/Documentation/index.tsx @@ -7,6 +7,7 @@ import { import { Documentation } from "../../components/Documentation"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { GlobalStyle } from "./styles"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,10 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError("documentation", message, source, lineno, colno, error); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/containers/InstallationWizard/index.tsx b/src/containers/InstallationWizard/index.tsx index 4aa7c64fb..e989b9e75 100644 --- a/src/containers/InstallationWizard/index.tsx +++ b/src/containers/InstallationWizard/index.tsx @@ -7,6 +7,7 @@ import { import { InstallationWizard } from "../../components/InstallationWizard"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { GlobalStyle } from "./styles"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,17 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError( + "installationWizard", + message, + source, + lineno, + colno, + error + ); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/containers/Main/index.tsx b/src/containers/Main/index.tsx index 2349e977a..472e69013 100644 --- a/src/containers/Main/index.tsx +++ b/src/containers/Main/index.tsx @@ -7,6 +7,7 @@ import { } from "../../api"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { router } from "./router"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,10 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError("main", message, source, lineno, colno, error); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/containers/Main/router.tsx b/src/containers/Main/router.tsx index 344c31f38..3951a5d6b 100644 --- a/src/containers/Main/router.tsx +++ b/src/containers/Main/router.tsx @@ -1,4 +1,9 @@ -import { Navigate, RouteObject, createHashRouter } from "react-router-dom"; +import { + Navigate, + RouteObject, + createHashRouter, + useRouteError +} from "react-router-dom"; import { Assets } from "../../components/Assets"; import { Errors } from "../../components/Errors"; import { Highlights } from "../../components/Highlights"; @@ -11,6 +16,9 @@ export const routes: RouteObject[] = [ { path: "/", element:
, + ErrorBoundary: () => { + throw useRouteError(); + }, children: [ { index: true, diff --git a/src/containers/Notifications/index.tsx b/src/containers/Notifications/index.tsx index 3088d76c4..3915b3c10 100644 --- a/src/containers/Notifications/index.tsx +++ b/src/containers/Notifications/index.tsx @@ -7,6 +7,7 @@ import { import { Notifications } from "../../components/Notifications"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { GlobalStyle } from "./styles"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,10 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError("notifications", message, source, lineno, colno, error); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/containers/RecentActivity/index.tsx b/src/containers/RecentActivity/index.tsx index bdd998cc3..80ca8e2c0 100644 --- a/src/containers/RecentActivity/index.tsx +++ b/src/containers/RecentActivity/index.tsx @@ -7,6 +7,7 @@ import { import { RecentActivity } from "../../components/RecentActivity"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { GlobalStyle } from "./styles"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,10 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError("recentActivity", message, source, lineno, colno, error); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/containers/Troubleshooting/index.tsx b/src/containers/Troubleshooting/index.tsx index a0e5c6cb0..89a1ae428 100644 --- a/src/containers/Troubleshooting/index.tsx +++ b/src/containers/Troubleshooting/index.tsx @@ -7,6 +7,7 @@ import { import { Troubleshooting } from "../../components/Troubleshooting"; import { App } from "../../components/common/App"; import { dispatcher } from "../../dispatcher"; +import { handleUncaughtError } from "../../utils/handleUncaughtError"; import { GlobalStyle } from "./styles"; initializeDigmaMessageListener(dispatcher); @@ -14,6 +15,10 @@ initializeDigmaMessageListener(dispatcher); window.sendMessageToDigma = sendMessage; window.cancelMessageToDigma = cancelMessage; +window.onerror = (message, source, lineno, colno, error) => { + handleUncaughtError("troubleshooting", message, source, lineno, colno, error); +}; + const rootElement = document.getElementById("root"); if (rootElement) { diff --git a/src/trackingEvents.ts b/src/trackingEvents.ts index a91f902f6..4c71f2145 100644 --- a/src/trackingEvents.ts +++ b/src/trackingEvents.ts @@ -1,6 +1,8 @@ export const trackingEvents = { TROUBLESHOOTING_LINK_CLICKED: "troubleshooting link clicked", USER_ACTION: "user-action", + ERROR: "error", + ERROR_SCREEN_REFRESH_BUTTON_CLICKED: "error screen refresh button clicked", PAGINATION_BUTTON_CLICKED: "pagination button clicked", CAROUSEL_NAVIGATION_BUTTON_CLICKED: "carousel navigation button clicked", CAROUSEL_PAGE_BUTTON_CLICKED: "carousel page button clicked" diff --git a/src/utils/actions/sendErrorTrackingEvent.ts b/src/utils/actions/sendErrorTrackingEvent.ts new file mode 100644 index 000000000..f2ced3550 --- /dev/null +++ b/src/utils/actions/sendErrorTrackingEvent.ts @@ -0,0 +1,24 @@ +import { trackingEvents } from "../../trackingEvents"; +import { sendTrackingEvent } from "./sendTrackingEvent"; + +export interface ErrorData { + severity: "low" | "medium" | "high"; + message?: string; + action?: string; + [key: string]: unknown; +} + +export const sendErrorTrackingEvent = (error: Error, data: ErrorData) => { + sendTrackingEvent(trackingEvents.ERROR, { + ...(data ? { ...data } : {}), + error: { + source: "ui" + }, + exception: { + type: error.name, + message: error.message, + "stack-trace": error.stack + }, + message: data.message ?? error.message + }); +}; diff --git a/src/utils/handleUncaughtError.ts b/src/utils/handleUncaughtError.ts new file mode 100644 index 000000000..2a6485568 --- /dev/null +++ b/src/utils/handleUncaughtError.ts @@ -0,0 +1,22 @@ +import { logger } from "../logging"; +import { isString } from "../typeGuards/isString"; +import { sendErrorTrackingEvent } from "./actions/sendErrorTrackingEvent"; + +export const handleUncaughtError = ( + app: string, + event: Event | string, + source?: string, + lineno?: number, + colno?: number, + error?: Error +) => { + logger.error(event, source, lineno, colno, error); + const err = error ?? new Error("Unknown error"); + const customMessage = isString(event) ? event : event.type; + sendErrorTrackingEvent(err, { + severity: "high", + message: customMessage, + level: "global", + app + }); +}; From 1469511335de718278f5e4ebaf0a1c02fb721548 Mon Sep 17 00:00:00 2001 From: Kyrylo Shmidt Date: Wed, 10 Jul 2024 13:30:37 +0200 Subject: [PATCH 2/2] Update dependencies --- package-lock.json | 20 ++++++++++++++++++++ package.json | 1 + 2 files changed, 21 insertions(+) diff --git a/package-lock.json b/package-lock.json index 4401dae63..b21ca3790 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "react": "^18.2.0", "react-cool-dimensions": "^3.0.1", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.13", "react-helmet": "^6.1.0", "react-hook-form": "^7.48.2", "react-router-dom": "^6.23.1", @@ -14808,6 +14809,17 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -28631,6 +28643,14 @@ } } }, + "react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", diff --git a/package.json b/package.json index 7f380e2b9..592c2e9c2 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "react": "^18.2.0", "react-cool-dimensions": "^3.0.1", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.13", "react-helmet": "^6.1.0", "react-hook-form": "^7.48.2", "react-router-dom": "^6.23.1",