Skip to content

Commit ba5c9ef

Browse files
authored
Merge pull request #269 from AustineSamuel/error-boundary
feat(frontend): route-segment error.tsx for claims, policies, and admin
2 parents 55e6ba1 + 9839bc8 commit ba5c9ef

19 files changed

Lines changed: 346 additions & 28 deletions

File tree

frontend/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,38 @@ To enable locally:
144144
NEXT_PUBLIC_ANALYTICS_ENABLED=true
145145
NEXT_PUBLIC_ANALYTICS_DOMAIN=your-domain.com
146146
```
147+
148+
## Architecture notes
149+
150+
### Route error boundaries (`error.tsx`)
151+
152+
Next.js **App Router** isolates render failures per route **segment** using a
153+
client `error.tsx` next to `page.tsx` / `layout.tsx`. When a segment throws
154+
during render (or in a child Server/Client component during that render pass),
155+
only that subtree is replaced by the error UI; the root layout (navigation,
156+
wallet provider, etc.) keeps running.
157+
158+
**Current segment boundaries**
159+
160+
| Segment | Path | Role |
161+
|-------------|-------------------|------|
162+
| Claims | `app/claims/` | Claims board list and nested routes |
163+
| Policies | `app/policies/` | Policy dashboard (`PolicyDashboard`); `/dashboard` redirects here |
164+
| Admin | `app/admin/` | Admin placeholder segment |
165+
| Policy flow | `app/policy/` | Quote/bind policy wizard |
166+
| Quote | `app/quote/` | Quote flow |
167+
| Support | `app/support/` | Support |
168+
169+
Shared UI: `RouteError` (`src/components/route-error.tsx`) — user-safe message,
170+
support reference when present, **Try again** (`reset()`), **Go to dashboard**
171+
link. Development-only collapsible stack trace.
172+
173+
**Observability:** `logRouteSegmentError` (`src/lib/observability.ts`) records
174+
anonymized metadata in production via Plausible (`route_segment_error`: segment,
175+
error name, optional digest). **No** `error.message` or stack is sent to
176+
analytics or shown to users in production.
177+
178+
**Out of scope for these boundaries:** Wallet signing and other **event
179+
handler** errors are not caught by `error.tsx`; components must handle those
180+
inline (try/catch / toast) so users get immediate feedback without replacing the
181+
whole segment.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { render, screen } from '@testing-library/react'
5+
import React from 'react'
6+
7+
const routeErrorMock = jest.fn(({ segment }: { segment: string }) => (
8+
<div data-testid="route-error" data-segment={segment} />
9+
))
10+
11+
jest.mock('@/components/route-error', () => ({
12+
RouteError: (props: { segment: string }) => routeErrorMock(props),
13+
}))
14+
15+
import ClaimsError from '@/app/claims/error'
16+
import PoliciesError from '@/app/policies/error'
17+
import AdminError from '@/app/admin/error'
18+
19+
describe('Route segment error.tsx wiring', () => {
20+
const err = new Error('boom') as Error & { digest?: string }
21+
const reset = jest.fn()
22+
23+
beforeEach(() => {
24+
routeErrorMock.mockClear()
25+
})
26+
27+
it.each([
28+
[ClaimsError, 'claims'],
29+
[PoliciesError, 'policies'],
30+
[AdminError, 'admin'],
31+
] as const)('passes segment to RouteError', (ErrorPage, segment) => {
32+
render(<ErrorPage error={err} reset={reset} />)
33+
expect(screen.getByTestId('route-error')).toHaveAttribute('data-segment', segment)
34+
})
35+
})

frontend/src/app/admin/error.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import { RouteError } from '@/components/route-error'
4+
5+
export default function AdminError({
6+
error,
7+
reset,
8+
}: {
9+
error: Error & { digest?: string }
10+
reset: () => void
11+
}) {
12+
return <RouteError error={error} reset={reset} area="Admin" segment="admin" />
13+
}

frontend/src/app/admin/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReactNode } from 'react'
2+
3+
export default function AdminLayout({ children }: { children: ReactNode }) {
4+
return children
5+
}

frontend/src/app/admin/page.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const metadata = { title: 'Admin' }
2+
3+
/**
4+
* Placeholder admin segment so `error.tsx` can scope failures without affecting
5+
* the rest of the app. Extend with real admin UI when available.
6+
*/
7+
export default function AdminPage() {
8+
return (
9+
<main className="container mx-auto px-4 py-8 max-w-3xl">
10+
<h1 className="text-2xl font-semibold text-gray-900 mb-2">Admin</h1>
11+
<p className="text-sm text-muted-foreground">Administrative tools will appear here.</p>
12+
</main>
13+
)
14+
}

frontend/src/app/claims/error.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client';
22
import { RouteError } from '@/components/route-error';
33
export default function ClaimsError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
4-
return <RouteError error={error} reset={reset} area="Claims Board" />;
4+
return <RouteError error={error} reset={reset} area="Claims Board" segment="claims" />;
55
}
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { PolicyDashboard } from '@/features/policies/components/PolicyDashboard';
2-
3-
export const metadata = { title: 'My Policies' };
1+
import { redirect } from 'next/navigation'
42

3+
/** Canonical policies list lives under `/policies` (segment-level error boundary). */
54
export default function PolicyDashboardPage() {
6-
return (
7-
<main className="container mx-auto px-4 py-8 max-w-6xl">
8-
<h1 className="text-2xl font-bold text-gray-900 mb-6">My Policies</h1>
9-
<PolicyDashboard />
10-
</main>
11-
);
5+
redirect('/policies')
126
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import { RouteError } from '@/components/route-error'
4+
5+
export default function PoliciesError({
6+
error,
7+
reset,
8+
}: {
9+
error: Error & { digest?: string }
10+
reset: () => void
11+
}) {
12+
return <RouteError error={error} reset={reset} area="Policies" segment="policies" />
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { ReactNode } from 'react'
2+
3+
export default function PoliciesLayout({ children }: { children: ReactNode }) {
4+
return children
5+
}

frontend/src/app/policies/page.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PolicyDashboard } from '@/features/policies/components/PolicyDashboard'
2+
3+
export const metadata = { title: 'My Policies' }
4+
5+
export default function PoliciesPage() {
6+
return (
7+
<main className="container mx-auto px-4 py-8 max-w-6xl">
8+
<h1 className="text-2xl font-bold text-gray-900 mb-6">My Policies</h1>
9+
<PolicyDashboard />
10+
</main>
11+
)
12+
}

0 commit comments

Comments
 (0)