diff --git a/client/package.json b/client/package.json index 515f88ae7f3..e9ec1d4f0e6 100644 --- a/client/package.json +++ b/client/package.json @@ -79,6 +79,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-error-boundary": "^5.0.0", "react-flip-toolkit": "^7.1.0", "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", diff --git a/client/src/routes/RouteErrorBoundary.tsx b/client/src/routes/RouteErrorBoundary.tsx new file mode 100644 index 00000000000..082d9645594 --- /dev/null +++ b/client/src/routes/RouteErrorBoundary.tsx @@ -0,0 +1,216 @@ +import { useRouteError } from 'react-router-dom'; +import { Button } from '~/components/ui'; + +interface UserAgentData { + getHighEntropyValues(hints: string[]): Promise<{ platform: string; platformVersion: string }>; +} + +type PlatformInfo = { + os: string; + version?: string; +}; + +const formatStackTrace = (stack: string) => { + return stack + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line, i) => ({ + number: i + 1, + content: line, + })); +}; + +const getPlatformInfo = async (): Promise => { + if ('userAgentData' in navigator) { + try { + const ua = navigator.userAgentData as UserAgentData; + const highEntropyValues = await ua.getHighEntropyValues(['platform', 'platformVersion']); + return { + os: highEntropyValues.platform, + version: highEntropyValues.platformVersion, + }; + } catch (e) { + console.warn('Failed to get high entropy values:', e); + } + } + + const userAgent = navigator.userAgent.toLowerCase(); + + if (userAgent.includes('mac')) { + return { os: 'macOS' }; + } + if (userAgent.includes('win')) { + return { os: 'Windows' }; + } + if (userAgent.includes('linux')) { + return { os: 'Linux' }; + } + if (userAgent.includes('android')) { + return { os: 'Android' }; + } + if (userAgent.includes('ios') || userAgent.includes('iphone') || userAgent.includes('ipad')) { + return { os: 'iOS' }; + } + + return { os: 'Unknown' }; +}; + +const getBrowserInfo = async () => { + const platformInfo = await getPlatformInfo(); + return { + userAgent: navigator.userAgent, + platform: platformInfo.os, + platformVersion: platformInfo.version, + language: navigator.language, + windowSize: `${window.innerWidth}x${window.innerHeight}`, + }; +}; + +export default function RouteErrorBoundary() { + const typedError = useRouteError() as { + message?: string; + stack?: string; + status?: number; + statusText?: string; + data?: unknown; + }; + + const errorDetails = { + message: typedError.message ?? 'An unexpected error occurred', + stack: typedError.stack, + status: typedError.status, + statusText: typedError.statusText, + data: typedError.data, + }; + + const handleDownloadLogs = async () => { + const browser = await getBrowserInfo(); + const errorLog = { + timestamp: new Date().toISOString(), + browser, + error: { + ...errorDetails, + stack: + errorDetails.stack != null && errorDetails.stack.trim() !== '' + ? formatStackTrace(errorDetails.stack) + : undefined, + }, + }; + + const blob = new Blob([JSON.stringify(errorLog, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `error-log-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleCopyStack = async () => { + if (errorDetails.stack != null && errorDetails.stack !== '') { + await navigator.clipboard.writeText(errorDetails.stack); + } + }; + + return ( +
+
+

+ Oops! Something Unexpected Occurred +

+ + {/* Error Message */} +
+

Error Message:

+
+            {errorDetails.message}
+          
+
+ + {/* Status Information */} + {(typeof errorDetails.status === 'number' || + typeof errorDetails.statusText === 'string') && ( +
+

Status:

+

+ {typeof errorDetails.status === 'number' && `${errorDetails.status} `} + {typeof errorDetails.statusText === 'string' && errorDetails.statusText} +

+
+ )} + + {/* Stack Trace - Collapsible */} + {errorDetails.stack != null && errorDetails.stack.trim() !== '' && ( +
+ + Stack Trace +
+ +
+
+
+ {formatStackTrace(errorDetails.stack).map(({ number, content }) => ( +
+ + {String(number).padStart(3, '0')} + +
+                    {content}
+                  
+
+ ))} +
+
+ )} + + {/* Additional Error Data */} + {errorDetails.data != null && ( +
+ + Additional Details + {'>'} + +
+              {JSON.stringify(errorDetails.data, null, 2)}
+            
+
+ )} + +
+

Please try one of the following:

+
    +
  • Refresh the page
  • +
  • Clear your browser cache
  • +
  • Check your internet connection
  • +
  • Contact the Admin if the issue persists
  • +
+
+ + +
+
+
+
+ ); +} diff --git a/client/src/routes/index.tsx b/client/src/routes/index.tsx index 3d87140c151..3cdfe3c46e2 100644 --- a/client/src/routes/index.tsx +++ b/client/src/routes/index.tsx @@ -8,6 +8,7 @@ import { ApiErrorWatcher, } from '~/components/Auth'; import { AuthContextProvider } from '~/hooks/AuthContext'; +import RouteErrorBoundary from './RouteErrorBoundary'; import StartupLayout from './Layouts/Startup'; import LoginLayout from './Layouts/Login'; import dashboardRoutes from './Dashboard'; @@ -27,10 +28,12 @@ export const router = createBrowserRouter([ { path: 'share/:shareId', element: , + errorElement: , }, { path: '/', element: , + errorElement: , children: [ { path: 'register', @@ -49,9 +52,11 @@ export const router = createBrowserRouter([ { path: 'verify', element: , + errorElement: , }, { element: , + errorElement: , children: [ { path: '/', diff --git a/package-lock.json b/package-lock.json index fbf287a3f66..b0e995b096c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -940,6 +940,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-error-boundary": "^5.0.0", "react-flip-toolkit": "^7.1.0", "react-gtm-module": "^2.0.11", "react-hook-form": "^7.43.9", @@ -30464,6 +30465,18 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz", + "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-flip-toolkit": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/react-flip-toolkit/-/react-flip-toolkit-7.1.0.tgz",