Skip to content

Commit b8b7f40

Browse files
authored
🌄 feat: Add RouteErrorBoundary for Improved Client Error handling (#5396)
* feat: Add RouteErrorBoundary for improved error handling and integrate react-error-boundary package * feat: update error message * fix: correct typo in containerClassName prop in Landing component
1 parent ed57bb4 commit b8b7f40

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

‎client/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"react-dnd": "^16.0.1",
8080
"react-dnd-html5-backend": "^16.0.1",
8181
"react-dom": "^18.2.0",
82+
"react-error-boundary": "^5.0.0",
8283
"react-flip-toolkit": "^7.1.0",
8384
"react-gtm-module": "^2.0.11",
8485
"react-hook-form": "^7.43.9",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { useRouteError } from 'react-router-dom';
2+
import { Button } from '~/components/ui';
3+
4+
interface UserAgentData {
5+
getHighEntropyValues(hints: string[]): Promise<{ platform: string; platformVersion: string }>;
6+
}
7+
8+
type PlatformInfo = {
9+
os: string;
10+
version?: string;
11+
};
12+
13+
const formatStackTrace = (stack: string) => {
14+
return stack
15+
.split('\n')
16+
.map((line) => line.trim())
17+
.filter(Boolean)
18+
.map((line, i) => ({
19+
number: i + 1,
20+
content: line,
21+
}));
22+
};
23+
24+
const getPlatformInfo = async (): Promise<PlatformInfo> => {
25+
if ('userAgentData' in navigator) {
26+
try {
27+
const ua = navigator.userAgentData as UserAgentData;
28+
const highEntropyValues = await ua.getHighEntropyValues(['platform', 'platformVersion']);
29+
return {
30+
os: highEntropyValues.platform,
31+
version: highEntropyValues.platformVersion,
32+
};
33+
} catch (e) {
34+
console.warn('Failed to get high entropy values:', e);
35+
}
36+
}
37+
38+
const userAgent = navigator.userAgent.toLowerCase();
39+
40+
if (userAgent.includes('mac')) {
41+
return { os: 'macOS' };
42+
}
43+
if (userAgent.includes('win')) {
44+
return { os: 'Windows' };
45+
}
46+
if (userAgent.includes('linux')) {
47+
return { os: 'Linux' };
48+
}
49+
if (userAgent.includes('android')) {
50+
return { os: 'Android' };
51+
}
52+
if (userAgent.includes('ios') || userAgent.includes('iphone') || userAgent.includes('ipad')) {
53+
return { os: 'iOS' };
54+
}
55+
56+
return { os: 'Unknown' };
57+
};
58+
59+
const getBrowserInfo = async () => {
60+
const platformInfo = await getPlatformInfo();
61+
return {
62+
userAgent: navigator.userAgent,
63+
platform: platformInfo.os,
64+
platformVersion: platformInfo.version,
65+
language: navigator.language,
66+
windowSize: `${window.innerWidth}x${window.innerHeight}`,
67+
};
68+
};
69+
70+
export default function RouteErrorBoundary() {
71+
const typedError = useRouteError() as {
72+
message?: string;
73+
stack?: string;
74+
status?: number;
75+
statusText?: string;
76+
data?: unknown;
77+
};
78+
79+
const errorDetails = {
80+
message: typedError.message ?? 'An unexpected error occurred',
81+
stack: typedError.stack,
82+
status: typedError.status,
83+
statusText: typedError.statusText,
84+
data: typedError.data,
85+
};
86+
87+
const handleDownloadLogs = async () => {
88+
const browser = await getBrowserInfo();
89+
const errorLog = {
90+
timestamp: new Date().toISOString(),
91+
browser,
92+
error: {
93+
...errorDetails,
94+
stack:
95+
errorDetails.stack != null && errorDetails.stack.trim() !== ''
96+
? formatStackTrace(errorDetails.stack)
97+
: undefined,
98+
},
99+
};
100+
101+
const blob = new Blob([JSON.stringify(errorLog, null, 2)], { type: 'application/json' });
102+
const url = URL.createObjectURL(blob);
103+
const a = document.createElement('a');
104+
a.href = url;
105+
a.download = `error-log-${new Date().toISOString()}.json`;
106+
document.body.appendChild(a);
107+
a.click();
108+
document.body.removeChild(a);
109+
URL.revokeObjectURL(url);
110+
};
111+
112+
const handleCopyStack = async () => {
113+
if (errorDetails.stack != null && errorDetails.stack !== '') {
114+
await navigator.clipboard.writeText(errorDetails.stack);
115+
}
116+
};
117+
118+
return (
119+
<div
120+
role="alert"
121+
className="flex min-h-screen flex-col items-center justify-center bg-surface-primary bg-gradient-to-br"
122+
>
123+
<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">
124+
<h2 className="mb-6 text-center text-3xl font-medium tracking-tight text-text-primary">
125+
Oops! Something Unexpected Occurred
126+
</h2>
127+
128+
{/* Error Message */}
129+
<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">
130+
<h3 className="mb-2 font-medium">Error Message:</h3>
131+
<pre className="whitespace-pre-wrap text-sm font-light leading-relaxed text-text-primary">
132+
{errorDetails.message}
133+
</pre>
134+
</div>
135+
136+
{/* Status Information */}
137+
{(typeof errorDetails.status === 'number' ||
138+
typeof errorDetails.statusText === 'string') && (
139+
<div className="mb-4 rounded-xl border border-yellow-500/20 bg-yellow-500/5 p-4 text-sm text-text-primary">
140+
<h3 className="mb-2 font-medium">Status:</h3>
141+
<p className="text-text-primary">
142+
{typeof errorDetails.status === 'number' && `${errorDetails.status} `}
143+
{typeof errorDetails.statusText === 'string' && errorDetails.statusText}
144+
</p>
145+
</div>
146+
)}
147+
148+
{/* Stack Trace - Collapsible */}
149+
{errorDetails.stack != null && errorDetails.stack.trim() !== '' && (
150+
<details className="group mb-4 rounded-xl border border-border-light p-4">
151+
<summary className="mb-2 flex cursor-pointer items-center justify-between text-sm font-medium text-text-primary">
152+
<span>Stack Trace</span>
153+
<div className="flex items-center">
154+
<Button
155+
variant="outline"
156+
size="sm"
157+
onClick={handleCopyStack}
158+
className="ml-2 px-2 py-1 text-xs"
159+
>
160+
Copy
161+
</Button>
162+
</div>
163+
</summary>
164+
<div className="overflow-x-auto rounded-lg bg-black/5 p-4 dark:bg-white/5">
165+
{formatStackTrace(errorDetails.stack).map(({ number, content }) => (
166+
<div key={number} className="flex">
167+
<span className="select-none pr-4 font-mono text-xs text-text-secondary">
168+
{String(number).padStart(3, '0')}
169+
</span>
170+
<pre className="flex-1 font-mono text-xs leading-relaxed text-text-primary">
171+
{content}
172+
</pre>
173+
</div>
174+
))}
175+
</div>
176+
</details>
177+
)}
178+
179+
{/* Additional Error Data */}
180+
{errorDetails.data != null && (
181+
<details className="group mb-4 rounded-xl border border-border-light p-4">
182+
<summary className="mb-2 flex cursor-pointer items-center justify-between text-sm font-medium text-text-primary">
183+
<span>Additional Details</span>
184+
<span className="transition-transform group-open:rotate-90">{'>'}</span>
185+
</summary>
186+
<pre className="whitespace-pre-wrap text-xs font-light leading-relaxed text-text-primary">
187+
{JSON.stringify(errorDetails.data, null, 2)}
188+
</pre>
189+
</details>
190+
)}
191+
192+
<div className="mt-6 flex flex-col gap-4">
193+
<p className="text-sm font-light text-text-secondary">Please try one of the following:</p>
194+
<ul className="list-inside list-disc text-sm text-text-secondary">
195+
<li>Refresh the page</li>
196+
<li>Clear your browser cache</li>
197+
<li>Check your internet connection</li>
198+
<li>Contact the Admin if the issue persists</li>
199+
</ul>
200+
<div className="mt-4 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
201+
<Button
202+
variant="submit"
203+
onClick={() => window.location.reload()}
204+
className="w-full sm:w-auto"
205+
>
206+
Refresh Page
207+
</Button>
208+
<Button variant="outline" onClick={handleDownloadLogs} className="w-full sm:w-auto">
209+
Download Error Logs
210+
</Button>
211+
</div>
212+
</div>
213+
</div>
214+
</div>
215+
);
216+
}

‎client/src/routes/index.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ApiErrorWatcher,
99
} from '~/components/Auth';
1010
import { AuthContextProvider } from '~/hooks/AuthContext';
11+
import RouteErrorBoundary from './RouteErrorBoundary';
1112
import StartupLayout from './Layouts/Startup';
1213
import LoginLayout from './Layouts/Login';
1314
import dashboardRoutes from './Dashboard';
@@ -27,10 +28,12 @@ export const router = createBrowserRouter([
2728
{
2829
path: 'share/:shareId',
2930
element: <ShareRoute />,
31+
errorElement: <RouteErrorBoundary />,
3032
},
3133
{
3234
path: '/',
3335
element: <StartupLayout />,
36+
errorElement: <RouteErrorBoundary />,
3437
children: [
3538
{
3639
path: 'register',
@@ -49,9 +52,11 @@ export const router = createBrowserRouter([
4952
{
5053
path: 'verify',
5154
element: <VerifyEmail />,
55+
errorElement: <RouteErrorBoundary />,
5256
},
5357
{
5458
element: <AuthLayout />,
59+
errorElement: <RouteErrorBoundary />,
5560
children: [
5661
{
5762
path: '/',

‎package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)