Kaynağa Gözat

Merge pull request #1271 from porter-dev/por-57-lack-of-information-for-error-boundary

[POR-57] Lack of information for error boundary
abelanger5 4 yıl önce
ebeveyn
işleme
a793e639f2

+ 3 - 2
.github/workflows/dev.yaml

@@ -28,7 +28,7 @@ jobs:
       - name: Write Dashboard Environment Variables
         run: |
           cat >./dashboard/.env <<EOL
-          NODE_ENV=production
+          NODE_ENV=development
           API_SERVER=dashboard.dev.getporter.dev
           DISCORD_KEY=${{secrets.DISCORD_KEY}}
           DISCORD_CID=${{secrets.DISCORD_CID}}
@@ -38,6 +38,7 @@ jobs:
           ADDON_CHART_REPO_URL=https://chart-addons.dev.getporter.dev
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
+          SENTRY_ENV=development
           EOL
       - name: Build
         run: |
@@ -49,4 +50,4 @@ jobs:
         run: |
           aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name dev
             
-          kubectl rollout restart deployment/porter
+          kubectl rollout restart deployment/porter

+ 1 - 0
.github/workflows/production.yaml

@@ -38,6 +38,7 @@ jobs:
           ADDON_CHART_REPO_URL=https://chart-addons.getporter.dev
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
+          SENTRY_ENV=production
           EOL
       - name: Build
         run: |

+ 1 - 0
.github/workflows/staging.yaml

@@ -38,6 +38,7 @@ jobs:
           ADDON_CHART_REPO_URL=https://chart-addons.staging.getporter.dev
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
+          SENTRY_ENV=staging
           EOL
       - name: Build
         run: |

+ 82 - 50
dashboard/package-lock.json

@@ -2656,13 +2656,13 @@
       "dev": true
     },
     "@sentry/browser": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.12.0.tgz",
-      "integrity": "sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ==",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz",
+      "integrity": "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag==",
       "requires": {
-        "@sentry/core": "6.12.0",
-        "@sentry/types": "6.12.0",
-        "@sentry/utils": "6.12.0",
+        "@sentry/core": "6.13.2",
+        "@sentry/types": "6.13.2",
+        "@sentry/utils": "6.13.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
@@ -2674,14 +2674,14 @@
       }
     },
     "@sentry/core": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.12.0.tgz",
-      "integrity": "sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ==",
-      "requires": {
-        "@sentry/hub": "6.12.0",
-        "@sentry/minimal": "6.12.0",
-        "@sentry/types": "6.12.0",
-        "@sentry/utils": "6.12.0",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
+      "integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
+      "requires": {
+        "@sentry/hub": "6.13.2",
+        "@sentry/minimal": "6.13.2",
+        "@sentry/types": "6.13.2",
+        "@sentry/utils": "6.13.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
@@ -2693,12 +2693,12 @@
       }
     },
     "@sentry/hub": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.12.0.tgz",
-      "integrity": "sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg==",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
+      "integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
       "requires": {
-        "@sentry/types": "6.12.0",
-        "@sentry/utils": "6.12.0",
+        "@sentry/types": "6.13.2",
+        "@sentry/utils": "6.13.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
@@ -2710,12 +2710,12 @@
       }
     },
     "@sentry/minimal": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.12.0.tgz",
-      "integrity": "sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw==",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
+      "integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
       "requires": {
-        "@sentry/hub": "6.12.0",
-        "@sentry/types": "6.12.0",
+        "@sentry/hub": "6.13.2",
+        "@sentry/types": "6.13.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
@@ -2727,14 +2727,14 @@
       }
     },
     "@sentry/react": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.12.0.tgz",
-      "integrity": "sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ==",
-      "requires": {
-        "@sentry/browser": "6.12.0",
-        "@sentry/minimal": "6.12.0",
-        "@sentry/types": "6.12.0",
-        "@sentry/utils": "6.12.0",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz",
+      "integrity": "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ==",
+      "requires": {
+        "@sentry/browser": "6.13.2",
+        "@sentry/minimal": "6.13.2",
+        "@sentry/types": "6.13.2",
+        "@sentry/utils": "6.13.2",
         "hoist-non-react-statics": "^3.3.2",
         "tslib": "^1.9.3"
       },
@@ -2747,14 +2747,14 @@
       }
     },
     "@sentry/tracing": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.12.0.tgz",
-      "integrity": "sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA==",
-      "requires": {
-        "@sentry/hub": "6.12.0",
-        "@sentry/minimal": "6.12.0",
-        "@sentry/types": "6.12.0",
-        "@sentry/utils": "6.12.0",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
+      "integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
+      "requires": {
+        "@sentry/hub": "6.13.2",
+        "@sentry/minimal": "6.13.2",
+        "@sentry/types": "6.13.2",
+        "@sentry/utils": "6.13.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
@@ -2766,16 +2766,16 @@
       }
     },
     "@sentry/types": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.12.0.tgz",
-      "integrity": "sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA=="
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
+      "integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg=="
     },
     "@sentry/utils": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.12.0.tgz",
-      "integrity": "sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA==",
+      "version": "6.13.2",
+      "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
+      "integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
       "requires": {
-        "@sentry/types": "6.12.0",
+        "@sentry/types": "6.13.2",
         "tslib": "^1.9.3"
       },
       "dependencies": {
@@ -5994,7 +5994,6 @@
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
       "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
-      "dev": true,
       "requires": {
         "stackframe": "^1.1.1"
       }
@@ -10437,11 +10436,44 @@
         "figgy-pudding": "^3.5.1"
       }
     },
+    "stack-generator": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz",
+      "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==",
+      "requires": {
+        "stackframe": "^1.1.1"
+      }
+    },
     "stackframe": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
-      "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==",
-      "dev": true
+      "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
+    },
+    "stacktrace-gps": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz",
+      "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==",
+      "requires": {
+        "source-map": "0.5.6",
+        "stackframe": "^1.1.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.6",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
+          "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI="
+        }
+      }
+    },
+    "stacktrace-js": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
+      "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
+      "requires": {
+        "error-stack-parser": "^2.0.6",
+        "stack-generator": "^2.0.5",
+        "stacktrace-gps": "^3.0.4"
+      }
     },
     "static-extend": {
       "version": "0.1.2",

+ 3 - 2
dashboard/package.json

@@ -4,8 +4,8 @@
   "private": true,
   "dependencies": {
     "@material-ui/core": "^4.11.3",
-    "@sentry/react": "^6.12.0",
-    "@sentry/tracing": "^6.12.0",
+    "@sentry/react": "^6.13.2",
+    "@sentry/tracing": "^6.13.2",
     "@visx/axis": "^1.6.1",
     "@visx/curve": "^1.0.0",
     "@visx/event": "^1.3.0",
@@ -43,6 +43,7 @@
     "react-transition-group": "^4.4.2",
     "regenerator-runtime": "^0.13.9",
     "semver": "^7.3.5",
+    "stacktrace-js": "^2.0.2",
     "styled-components": "^5.2.0"
   },
   "scripts": {

+ 1 - 1
dashboard/src/App.tsx

@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { BrowserRouter } from "react-router-dom";
-import PorterErrorBoundary from "shared/PorterErrorBoundary";
+import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary";
 import styled, { createGlobalStyle } from "styled-components";
 
 import MainWrapper from "./main/MainWrapper";

+ 2 - 2
dashboard/src/components/UnexpectedErrorPage.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import styled from "styled-components";
 
-const UnexpectedErrorPage = ({ error, resetError }: any) => (
+const UnexpectedErrorPage = ({ error, resetErrorBoundary }: any) => (
   <>
     <StyledPageNotFound>
       <Mega>
@@ -9,7 +9,7 @@ const UnexpectedErrorPage = ({ error, resetError }: any) => (
         <Inside>Unknown Error</Inside>
       </Mega>
       <Flex>
-        <BackButton width="140px" onClick={() => resetError(error)}>
+        <BackButton width="140px" onClick={() => resetErrorBoundary(error)}>
           <i className="material-icons">arrow_back</i>
           Reload page
         </BackButton>

+ 4 - 1
dashboard/src/index.tsx

@@ -4,7 +4,8 @@ import "regenerator-runtime/runtime";
 import * as React from "react";
 import * as ReactDOM from "react-dom";
 import App from "./App";
-import { SetupSentry } from "shared/sentry/setup";
+import { SetupSentry } from "shared/error_handling/sentry/setup";
+import { EnableErrorHandling } from "shared/error_handling/window_error_handling";
 
 declare global {
   interface Window {
@@ -15,4 +16,6 @@ if (process.env.ENABLE_SENTRY) {
   SetupSentry();
 }
 
+EnableErrorHandling();
+
 ReactDOM.render(<App />, document.getElementById("output"));

+ 4 - 1
dashboard/src/main/MainWrapper.tsx

@@ -4,6 +4,7 @@ import { ContextProvider } from "../shared/Context";
 import Main from "./Main";
 import { RouteComponentProps, withRouter } from "react-router";
 import AuthProvider from "shared/auth/AuthContext";
+import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary";
 
 type PropsType = RouteComponentProps & {};
 
@@ -15,7 +16,9 @@ class MainWrapper extends Component<PropsType, StateType> {
     return (
       <ContextProvider history={history} location={location}>
         <AuthProvider>
-          <Main />
+          <MainWrapperErrorBoundary>
+            <Main />
+          </MainWrapperErrorBoundary>
         </AuthProvider>
       </ContextProvider>
     );

+ 0 - 53
dashboard/src/shared/PorterErrorBoundary.tsx

@@ -1,53 +0,0 @@
-import UnexpectedErrorPage from "components/UnexpectedErrorPage";
-import React from "react";
-import { ErrorBoundary } from "react-error-boundary";
-import * as Sentry from "@sentry/react";
-
-export type PorterErrorBoundaryProps<OnResetProps = {}> = {
-  // Component or useful name to describe where the error boundary was setted
-  errorBoundaryLocation: string;
-  // Used in case the boundary shouldn't refresh but instead do other action
-  onReset?: (props: OnResetProps) => unknown;
-};
-
-const PorterErrorBoundary: React.FC<PorterErrorBoundaryProps> = ({
-  errorBoundaryLocation,
-  onReset,
-  children,
-}) => {
-  const handleError = (error: Error, info: { componentStack: string }) => {
-    if (process.env.ENABLE_SENTRY) {
-      Sentry.captureException(error, (scope) => {
-        scope.setTags({
-          error_boundary_location: errorBoundaryLocation,
-          error_message: error?.message,
-          component_stack: info?.componentStack,
-        });
-        return scope;
-      });
-    }
-
-    window?.analytics?.track("React Error", {
-      location: errorBoundaryLocation,
-      error: error.message,
-      componentStack: info?.componentStack,
-      url: window.location.toString(),
-    });
-  };
-
-  const handleOnReset = (props: unknown) => {
-    typeof onReset === "function" ? onReset(props) : window.location.reload();
-  };
-
-  return (
-    <ErrorBoundary
-      onError={handleError}
-      FallbackComponent={UnexpectedErrorPage}
-      onReset={handleOnReset}
-    >
-      {children}
-    </ErrorBoundary>
-  );
-};
-
-export default PorterErrorBoundary;

+ 33 - 0
dashboard/src/shared/error_handling/MainWrapperErrorBoundary.tsx

@@ -0,0 +1,33 @@
+import React, { useContext } from "react";
+import { Context } from "shared/Context";
+import PorterErrorBoundary from "./PorterErrorBoundary";
+
+const MainWrapperErrorBoundary: React.FC = ({ children }) => {
+  const location = "MainWrapperErrorBoundary";
+  const {
+    capabilities,
+    currentCluster,
+    currentProject,
+    devOpsMode,
+    projects,
+  } = useContext(Context);
+
+  return (
+    <PorterErrorBoundary
+      errorBoundaryLocation={location}
+      context={{
+        "Global context state": {
+          capabilities,
+          currentProject,
+          currentCluster,
+          devOpsMode,
+          projects: JSON.stringify(projects),
+        },
+      }}
+    >
+      {children}
+    </PorterErrorBoundary>
+  );
+};
+
+export default MainWrapperErrorBoundary;

+ 84 - 0
dashboard/src/shared/error_handling/PorterErrorBoundary.tsx

@@ -0,0 +1,84 @@
+import UnexpectedErrorPage from "components/UnexpectedErrorPage";
+import React from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import * as Sentry from "@sentry/react";
+import StackTrace from "stacktrace-js";
+import { Context, Primitive } from "@sentry/types";
+import { stackFramesToString } from "./stack_trace_utils";
+
+export type PorterErrorBoundaryProps<OnResetProps = {}> = {
+  // Component or useful name to describe where the error boundary was setted
+  errorBoundaryLocation: string;
+  // Used in case the boundary shouldn't refresh but instead do other action
+  onReset?: (props: OnResetProps) => unknown;
+  // Add more tags to sentry errors
+  tags?: {
+    [key: string]: Primitive;
+  };
+  // Add more context for sentry errors
+  context?: {
+    [key: string]: Context;
+  };
+};
+
+const PorterErrorBoundary: React.FC<PorterErrorBoundaryProps> = ({
+  errorBoundaryLocation,
+  onReset,
+  children,
+  tags,
+  context,
+}) => {
+  const handleError = (err: Error) => {
+    StackTrace.fromError(err).then((stackframes) => {
+      const stackFramesStringify = stackFramesToString(stackframes);
+      // Preserve the old stack just in case
+      const originalStack = err.stack;
+      // Update the error stack with the StackTrace stack (this helps for minified environments)
+      err.stack = stackFramesStringify;
+
+      if (process.env.ENABLE_SENTRY) {
+        Sentry.captureException(err, (scope) => {
+          scope.setTags({
+            error_boundary_location: errorBoundaryLocation,
+            error_message: err?.message,
+            ...(tags || {}),
+          });
+          scope.setContext("Original stack", {
+            originalStack,
+          });
+
+          if (typeof context === "object") {
+            Object.entries(context).forEach(([contextName, contextContent]) => {
+              scope.setContext(contextName, contextContent);
+            });
+          }
+
+          return scope;
+        });
+      }
+
+      window?.analytics?.track("React Error", {
+        location: errorBoundaryLocation,
+        error: stackFramesStringify,
+        componentStack: err.stack,
+        url: window.location.toString(),
+      });
+    });
+  };
+
+  const handleOnReset = (props: unknown) => {
+    typeof onReset === "function" ? onReset(props) : window.location.reload();
+  };
+
+  return (
+    <ErrorBoundary
+      onError={handleError}
+      FallbackComponent={UnexpectedErrorPage}
+      onReset={handleOnReset}
+    >
+      {children}
+    </ErrorBoundary>
+  );
+};
+
+export default PorterErrorBoundary;

+ 3 - 2
dashboard/src/shared/sentry/setup.ts → dashboard/src/shared/error_handling/sentry/setup.ts

@@ -2,6 +2,7 @@ import * as Sentry from "@sentry/react";
 import { Integrations } from "@sentry/tracing";
 
 const SENTRY_DSN = process.env.SENTRY_DSN;
+const SENTRY_ENV = process.env.SENTRY_ENV || "development";
 
 export const SetupSentry = () => {
   if (!SENTRY_DSN) {
@@ -10,8 +11,8 @@ export const SetupSentry = () => {
   Sentry.init({
     dsn: SENTRY_DSN,
     integrations: [new Integrations.BrowserTracing()],
-
+    environment: SENTRY_ENV,
     // Check out https://docs.sentry.io/platforms/javascript/guides/react/configuration/sampling/ for a more refined sample rate
-    tracesSampleRate: 0.25,
+    tracesSampleRate: 1,
   });
 };

+ 9 - 0
dashboard/src/shared/error_handling/stack_trace_utils.ts

@@ -0,0 +1,9 @@
+import { StackFrame } from "stacktrace-js";
+
+export const stackFramesToString = (stackFrames: StackFrame[]) => {
+  return stackFrames
+    .map(function (sf) {
+      return sf.toString();
+    })
+    .join("\n");
+};

+ 35 - 0
dashboard/src/shared/error_handling/window_error_handling.ts

@@ -0,0 +1,35 @@
+import { stackFramesToString } from "./stack_trace_utils";
+import * as Sentry from "@sentry/react";
+
+export function EnableErrorHandling() {
+  window.onerror = function (msg, file, line, col, err) {
+    StackTrace.fromError(err).then((stackframes) => {
+      const stackFramesStringify = stackFramesToString(stackframes);
+      // Preserve the old stack just in case
+      const originalStack = err.stack;
+      // Update the error stack with the StackTrace stack (this helps for minified environments)
+      err.stack = stackFramesStringify;
+
+      if (process.env.ENABLE_SENTRY) {
+        Sentry.captureException(err, (scope) => {
+          scope.setTags({
+            error_boundary_location: "window_error_handling",
+            error_message: err?.message,
+          });
+          scope.setContext("Original stack", {
+            originalStack,
+          });
+
+          return scope;
+        });
+      }
+
+      window?.analytics?.track("React Error", {
+        location: "window_error_handling",
+        error: stackFramesStringify,
+        componentStack: err.stack,
+        url: window.location.toString(),
+      });
+    });
+  };
+}

+ 1 - 0
dashboard/webpack.config.js

@@ -28,6 +28,7 @@ module.exports = () => {
     entry: ["./src/index.tsx"],
     target: "web",
     mode: isDevelopment ? "development" : "production",
+    devtool: "source-map",
     module: {
       rules: [
         {