-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🌄 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
- Loading branch information
Showing
4 changed files
with
235 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.