Skip to content

Commit 89f0750

Browse files
committed
feat: add top-level ErrorBoundary component
Add decoupled ErrorBoundary component that determines which error reporting service to use. Currently supports Bugsnag, but designed to be extensible for other services. Integrate into app root.
1 parent 5bcb71f commit 89f0750

3 files changed

Lines changed: 89 additions & 16 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Bugsnag from "@bugsnag/js";
2+
import React, { type ReactNode } from "react";
3+
import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary";
4+
5+
import { ErrorPage } from "@/components/shared/ErrorPage";
6+
import { getBugsnagConfig } from "@/services/errorManagement/bugsnag";
7+
8+
interface ErrorBoundaryProps {
9+
children: ReactNode;
10+
}
11+
12+
/**
13+
* Application-wide error boundary that uses Bugsnag's error boundary when enabled,
14+
* or falls back to react-error-boundary when Bugsnag is not configured.
15+
*/
16+
export const ErrorBoundary = ({ children }: ErrorBoundaryProps) => {
17+
const bugsnagConfig = getBugsnagConfig();
18+
19+
// Use Bugsnag's ErrorBoundary if enabled
20+
if (bugsnagConfig.enabled) {
21+
const BugsnagBoundary =
22+
Bugsnag.getPlugin("react")!.createErrorBoundary(React);
23+
24+
return (
25+
<BugsnagBoundary
26+
FallbackComponent={(props: {
27+
error: Error;
28+
info: React.ErrorInfo;
29+
clearError: () => void;
30+
}) => (
31+
<ErrorPage
32+
error={props.error}
33+
resetErrorBoundary={props.clearError}
34+
/>
35+
)}
36+
>
37+
{children}
38+
</BugsnagBoundary>
39+
);
40+
}
41+
42+
// Fallback to react-error-boundary when Bugsnag is not enabled
43+
return (
44+
<ReactErrorBoundary FallbackComponent={ErrorPage}>
45+
{children}
46+
</ReactErrorBoundary>
47+
);
48+
};

src/components/shared/ErrorPage.tsx

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Bugsnag from "@bugsnag/js";
2-
import { type ErrorComponentProps, useRouter } from "@tanstack/react-router";
2+
import { useRouter } from "@tanstack/react-router";
33
import { useEffect } from "react";
44

55
import { InfoBox } from "@/components/shared/InfoBox";
@@ -8,30 +8,51 @@ import { BlockStack } from "@/components/ui/layout";
88
import { Paragraph, Text } from "@/components/ui/typography";
99
import { getBugsnagConfig } from "@/services/errorManagement/bugsnag";
1010

11-
export const ErrorPage = ({ error, reset }: ErrorComponentProps) => {
11+
/**
12+
* Unified props for ErrorPage component.
13+
* Supports both router error props (reset) and react-error-boundary props (resetErrorBoundary).
14+
*/
15+
interface ErrorPageProps {
16+
error: unknown;
17+
reset?: () => void;
18+
resetErrorBoundary?: () => void;
19+
}
20+
21+
export const ErrorPage = ({
22+
error,
23+
reset,
24+
resetErrorBoundary,
25+
}: ErrorPageProps) => {
1226
const router = useRouter();
1327

28+
// Use whichever reset function is provided
29+
const resetFn = resetErrorBoundary ?? reset;
30+
31+
// Report error to Bugsnag when used from router (ErrorBoundary component handles its own reporting)
1432
useEffect(() => {
15-
const config = getBugsnagConfig();
33+
// Only report if this is a router error (has 'reset' instead of 'resetErrorBoundary')
34+
if (reset) {
35+
const config = getBugsnagConfig();
1636

17-
if (config.enabled && error instanceof Error) {
18-
Bugsnag.notify(error, (event) => {
19-
event.addMetadata("error_handler", {
20-
pathname: window.location.pathname,
37+
if (config.enabled && error instanceof Error) {
38+
Bugsnag.notify(error, (event) => {
39+
event.addMetadata("error_handler", {
40+
pathname: window.location.pathname,
41+
});
2142
});
22-
});
43+
}
2344
}
24-
}, [error]);
45+
}, [error, reset]);
2546

2647
const handleRefresh = () => {
27-
// Reset error boundary if available (some callers provide this function)
28-
reset?.();
48+
// Reset error boundary if available
49+
resetFn?.();
2950
window.location.reload();
3051
};
3152

3253
const handleGoHome = () => {
33-
// Reset error boundary if available (some callers provide this function)
34-
reset?.();
54+
// Reset error boundary if available
55+
resetFn?.();
3556
router.navigate({ to: "/" });
3657
};
3758

src/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { StrictMode } from "react";
77
import ReactDOM from "react-dom/client";
88
import { scan } from "react-scan";
99

10+
import { ErrorBoundary } from "@/components/shared/ErrorBoundary";
11+
1012
import { router } from "./routes/router";
1113
import { initializeBugsnag } from "./services/errorManagement/bugsnag";
1214

@@ -26,9 +28,11 @@ if (!rootElement.innerHTML) {
2628
const root = ReactDOM.createRoot(rootElement);
2729
root.render(
2830
<StrictMode>
29-
<QueryClientProvider client={queryClient}>
30-
<RouterProvider router={router} />
31-
</QueryClientProvider>
31+
<ErrorBoundary>
32+
<QueryClientProvider client={queryClient}>
33+
<RouterProvider router={router} />
34+
</QueryClientProvider>
35+
</ErrorBoundary>
3236
</StrictMode>,
3337
);
3438
}

0 commit comments

Comments
 (0)