From 62b45a76dc2241c5726aaba908cede4d04df78fb Mon Sep 17 00:00:00 2001 From: Marcello Fitton <106866560+angelplusultra@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:08:12 -0800 Subject: [PATCH] Implement Global Error Boundary (#4765) * Implement global error boundary * add 404 page for generic path catching * devbuild --------- Co-authored-by: Timothy Carambat --- .github/workflows/dev-build.yaml | 2 +- frontend/package.json | 3 +- frontend/src/App.jsx | 45 +++++---- .../ErrorBoundaryFallback/index.jsx | 93 +++++++++++++++++++ frontend/src/main.jsx | 8 ++ frontend/src/pages/404.jsx | 35 +++---- frontend/yarn.lock | 43 ++++++++- 7 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 frontend/src/components/ErrorBoundaryFallback/index.jsx diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 9e58d09b..5121838c 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['upgrade-multer'] # put your current branch to create a build. Core team only. + branches: ['enhancement-error-boundary'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/package.json b/frontend/package.json index dab34cc4..bfbb9cb5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,7 @@ "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-error-boundary": "^6.0.0", "react-highlight-words": "^0.21.0", "react-i18next": "^14.1.1", "react-loading-skeleton": "^3.1.0", @@ -75,4 +76,4 @@ "tailwindcss": "^3.3.1", "vite": "^4.3.0" } -} \ No newline at end of file +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1ae785cc..7e7b58a5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import React, { Suspense } from "react"; -import { Outlet } from "react-router-dom"; +import { Outlet, useLocation } from "react-router-dom"; import { I18nextProvider } from "react-i18next"; import { AuthProvider } from "@/AuthContext"; import { ToastContainer } from "react-toastify"; @@ -12,25 +12,34 @@ import { FullScreenLoader } from "./components/Preloader"; import { ThemeProvider } from "./ThemeContext"; import { PWAModeProvider } from "./PWAContext"; import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp"; +import { ErrorBoundary } from "react-error-boundary"; +import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback"; export default function App() { + const location = useLocation(); return ( - - - }> - - - - - - - - - - - - - - + + + + }> + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/components/ErrorBoundaryFallback/index.jsx b/frontend/src/components/ErrorBoundaryFallback/index.jsx new file mode 100644 index 00000000..03297859 --- /dev/null +++ b/frontend/src/components/ErrorBoundaryFallback/index.jsx @@ -0,0 +1,93 @@ +import { NavLink } from "react-router-dom"; +import { House, ArrowClockwise, Copy, Check } from "@phosphor-icons/react"; +import { useState } from "react"; + +export default function ErrorBoundaryFallback({ error, resetErrorBoundary }) { + const [copied, setCopied] = useState(false); + + const copyErrorDetails = async () => { + const details = { + url: window.location.href, + error: error?.name || "Unknown Error", + message: error?.message || "No message available", + stack: error?.stack || "No stack trace available", + userAgent: navigator.userAgent, + timestamp: new Date().toISOString(), + }; + + const formattedDetails = ` +Error Report +============ +Timestamp: ${details.timestamp} +URL: ${details.url} +User Agent: ${details.userAgent} + +Error: ${details.error} +Message: ${details.message} + +Stack Trace: +${details.stack} + `.trim(); + + try { + await navigator.clipboard.writeText(formattedDetails); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error("Failed to copy error details:", err); + } + }; + + return ( +
+

+ An error occurred. +

+

+ {error?.message} +

+ {import.meta.env.DEV && ( +
+
+ +
+
+            {error?.stack}
+          
+
+ )} +
+ + + + Home + +
+
+ ); +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 58e496cc..51767edf 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -372,6 +372,14 @@ const router = createBrowserRouter([ return { element: }; }, }, + // Catch-all route for 404s + { + path: "*", + lazy: async () => { + const { default: NotFound } = await import("@/pages/404"); + return { element: }; + }, + }, ], }, ]); diff --git a/frontend/src/pages/404.jsx b/frontend/src/pages/404.jsx index 6f2ac8e4..79cebbf6 100644 --- a/frontend/src/pages/404.jsx +++ b/frontend/src/pages/404.jsx @@ -1,24 +1,25 @@ -import Header from "../components/Header"; -import Footer from "../components/Footer"; +import { NavLink } from "react-router-dom"; +import { House, MagnifyingGlass } from "@phosphor-icons/react"; -export default function Contact() { +export default function NotFound() { return ( -
-
- ); } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1f709c62..d060ddef 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -183,6 +183,11 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.7" +"@babel/runtime@^7.12.5": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" @@ -3206,6 +3211,13 @@ react-dropzone@^14.2.3: file-selector "^0.6.0" prop-types "^15.8.1" +react-error-boundary@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-6.0.0.tgz#a9e552146958fa77d873b587aa6a5e97544ee954" + integrity sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA== + dependencies: + "@babel/runtime" "^7.12.5" + react-highlight-words@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.21.0.tgz#a109acdf7dc6fac3ed7db82e9cba94e8d65c281c" @@ -3583,7 +3595,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3655,7 +3676,14 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -4196,7 +4224,16 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==