Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add RouteErrorBoundary for improved error handling and integrate react-error-boundary package #5396

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
216 changes: 216 additions & 0 deletions client/src/routes/RouteErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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<PlatformInfo> => {
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 (
<div
role="alert"
className="flex min-h-screen flex-col items-center justify-center bg-surface-primary bg-gradient-to-br"
>
<div className="bg-surface-primary/60 mx-4 w-11/12 max-w-4xl rounded-2xl border border-border-light p-8 shadow-2xl backdrop-blur-xl">
<h2 className="mb-6 text-center text-3xl font-medium tracking-tight text-text-primary">
Oops! Something Unexpected Occurred
</h2>

{/* Error Message */}
<div className="mb-4 rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-sm text-gray-600 dark:text-gray-200">
<h3 className="mb-2 font-medium">Error Message:</h3>
<pre className="whitespace-pre-wrap text-sm font-light leading-relaxed text-text-primary">
{errorDetails.message}
</pre>
</div>

{/* Status Information */}
{(typeof errorDetails.status === 'number' ||
typeof errorDetails.statusText === 'string') && (
<div className="mb-4 rounded-xl border border-yellow-500/20 bg-yellow-500/5 p-4 text-sm text-text-primary">
<h3 className="mb-2 font-medium">Status:</h3>
<p className="text-text-primary">
{typeof errorDetails.status === 'number' && `${errorDetails.status} `}
{typeof errorDetails.statusText === 'string' && errorDetails.statusText}
</p>
</div>
)}

{/* Stack Trace - Collapsible */}
{errorDetails.stack != null && errorDetails.stack.trim() !== '' && (
<details className="group mb-4 rounded-xl border border-border-light p-4">
<summary className="mb-2 flex cursor-pointer items-center justify-between text-sm font-medium text-text-primary">
<span>Stack Trace</span>
<div className="flex items-center">
<Button
variant="outline"
size="sm"
onClick={handleCopyStack}
className="ml-2 px-2 py-1 text-xs"
>
Copy
</Button>
</div>
</summary>
<div className="overflow-x-auto rounded-lg bg-black/5 p-4 dark:bg-white/5">
{formatStackTrace(errorDetails.stack).map(({ number, content }) => (
<div key={number} className="flex">
<span className="select-none pr-4 font-mono text-xs text-text-secondary">
{String(number).padStart(3, '0')}
</span>
<pre className="flex-1 font-mono text-xs leading-relaxed text-text-primary">
{content}
</pre>
</div>
))}
</div>
</details>
)}

{/* Additional Error Data */}
{errorDetails.data != null && (
<details className="group mb-4 rounded-xl border border-border-light p-4">
<summary className="mb-2 flex cursor-pointer items-center justify-between text-sm font-medium text-text-primary">
<span>Additional Details</span>
<span className="transition-transform group-open:rotate-90">{'>'}</span>
</summary>
<pre className="whitespace-pre-wrap text-xs font-light leading-relaxed text-text-primary">
{JSON.stringify(errorDetails.data, null, 2)}
</pre>
</details>
)}

<div className="mt-6 flex flex-col gap-4">
<p className="text-sm font-light text-text-secondary">Please try one of the following:</p>
<ul className="list-inside list-disc text-sm text-text-secondary">
<li>Refresh the page</li>
<li>Clear your browser cache</li>
<li>Check your internet connection</li>
<li>Contact the Admin if the issue persists</li>
</ul>
<div className="mt-4 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Button
variant="submit"
onClick={() => window.location.reload()}
className="w-full sm:w-auto"
>
Refresh Page
</Button>
<Button variant="outline" onClick={handleDownloadLogs} className="w-full sm:w-auto">
Download Error Logs
</Button>
</div>
</div>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions client/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,10 +28,12 @@ export const router = createBrowserRouter([
{
path: 'share/:shareId',
element: <ShareRoute />,
errorElement: <RouteErrorBoundary />,
},
{
path: '/',
element: <StartupLayout />,
errorElement: <RouteErrorBoundary />,
children: [
{
path: 'register',
Expand All @@ -49,9 +52,11 @@ export const router = createBrowserRouter([
{
path: 'verify',
element: <VerifyEmail />,
errorElement: <RouteErrorBoundary />,
},
{
element: <AuthLayout />,
errorElement: <RouteErrorBoundary />,
children: [
{
path: '/',
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading