feat: Add comprehensive error boundaries for graceful error handling
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

Implemented React error boundaries to catch and handle errors gracefully:

**Core Error Handling Components:**
- Created ErrorBoundary class component with error catching and logging
- Created specialized fallback UIs (MinimalErrorFallback, DataErrorFallback,
  ComponentErrorFallback, FormErrorFallback, ChartErrorFallback, ImageErrorFallback)
- Added withErrorBoundary HOC for easy component wrapping
- Created errorLogger service with Sentry integration placeholder

**Error Logging Service (errorLogger.ts):**
- Centralized error logging with severity levels (FATAL, ERROR, WARNING, INFO, DEBUG)
- Context enrichment (URL, userAgent, timestamp, environment)
- Local storage of last 10 errors in sessionStorage for debugging
- User context management (setUser, clearUser)
- Breadcrumb support for debugging trails

**App Integration:**
- Wrapped root layout with top-level ErrorBoundary for catastrophic errors
- Added NetworkStatusIndicator to main page for offline sync visibility
- Wrapped daily summary section with isolated DataErrorFallback
- Added error boundary to AI assistant page with ComponentErrorFallback
- Wrapped feeding tracking form with FormErrorFallback using withErrorBoundary HOC
- Protected analytics charts with isolated ChartErrorFallback boundaries

**Error Recovery Features:**
- Isolated error boundaries prevent cascade failures
- Retry buttons on all fallback UIs
- Error count tracking with user warnings
- Development-mode error details display
- Automatic error logging to service (when Sentry integrated)

Next: Integration with Sentry for production error tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-01 19:37:04 +00:00
parent 7cb2ff97de
commit 68e33712f1
8 changed files with 797 additions and 69 deletions

View File

@@ -4,6 +4,8 @@ import { lazy, Suspense } from 'react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { LoadingFallback } from '@/components/common/LoadingFallback';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { ComponentErrorFallback } from '@/components/common/ErrorFallbacks';
// Lazy load the AI chat interface component
const AIChatInterface = lazy(() =>
@@ -16,9 +18,14 @@ export default function AIAssistantPage() {
return (
<ProtectedRoute>
<AppShell>
<Suspense fallback={<LoadingFallback variant="chat" />}>
<AIChatInterface />
</Suspense>
<ErrorBoundary
isolate
fallback={<ComponentErrorFallback error={new Error('AI Assistant failed to load')} />}
>
<Suspense fallback={<LoadingFallback variant="chat" />}>
<AIChatInterface />
</Suspense>
</ErrorBoundary>
</AppShell>
</ProtectedRoute>
);

View File

@@ -16,6 +16,8 @@ import {
} from '@mui/material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { ChartErrorFallback } from '@/components/common/ErrorFallbacks';
import {
TrendingUp,
Hotel,
@@ -232,21 +234,27 @@ export default function AnalyticsPage() {
</Box>
<TabPanel value={tabValue} index={0}>
<Box sx={{ p: 3 }}>
<WeeklySleepChart />
</Box>
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
<Box sx={{ p: 3 }}>
<WeeklySleepChart />
</Box>
</ErrorBoundary>
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Box sx={{ p: 3 }}>
<FeedingFrequencyGraph />
</Box>
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
<Box sx={{ p: 3 }}>
<FeedingFrequencyGraph />
</Box>
</ErrorBoundary>
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Box sx={{ p: 3 }}>
<GrowthCurve />
</Box>
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
<Box sx={{ p: 3 }}>
<GrowthCurve />
</Box>
</ErrorBoundary>
</TabPanel>
<TabPanel value={tabValue} index={3}>

View File

@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { ThemeRegistry } from '@/components/ThemeRegistry';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
// import { PerformanceMonitor } from '@/components/common/PerformanceMonitor'; // Temporarily disabled
import './globals.css';
@@ -37,10 +38,12 @@ export default function RootLayout({
<link rel="apple-touch-icon" href="/icon-192x192.png" />
</head>
<body className={inter.className}>
<ThemeRegistry>
{/* <PerformanceMonitor /> */}
{children}
</ThemeRegistry>
<ErrorBoundary>
<ThemeRegistry>
{/* <PerformanceMonitor /> */}
{children}
</ThemeRegistry>
</ErrorBoundary>
</body>
</html>
);

View File

@@ -5,6 +5,9 @@ import { Box, Typography, Button, Paper, Grid, CircularProgress } from '@mui/mat
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { EmailVerificationBanner } from '@/components/common/EmailVerificationBanner';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { DataErrorFallback } from '@/components/common/ErrorFallbacks';
import { NetworkStatusIndicator } from '@/components/common/NetworkStatusIndicator';
import {
Restaurant,
Hotel,
@@ -84,6 +87,7 @@ export default function HomePage() {
return (
<ProtectedRoute>
<AppShell>
<NetworkStatusIndicator />
<Box>
<EmailVerificationBanner />
@@ -139,59 +143,64 @@ export default function HomePage() {
<Typography variant="h6" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
Today's Summary{selectedChild ? ` - ${selectedChild.name}` : ''}
</Typography>
<Paper sx={{ p: 3 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : !dailySummary ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body2" color="text.secondary">
{children.length === 0
? 'Add a child to start tracking'
: 'No activities tracked today'}
</Typography>
</Box>
) : (
<Grid container spacing={3}>
<Grid item xs={4}>
<Box textAlign="center">
<Restaurant sx={{ fontSize: 32, color: 'primary.main', mb: 1 }} />
<Typography variant="h5" fontWeight="600">
{dailySummary.feedingCount || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
Feedings
</Typography>
</Box>
<ErrorBoundary
isolate
fallback={<DataErrorFallback error={new Error('Failed to load summary')} />}
>
<Paper sx={{ p: 3 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : !dailySummary ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body2" color="text.secondary">
{children.length === 0
? 'Add a child to start tracking'
: 'No activities tracked today'}
</Typography>
</Box>
) : (
<Grid container spacing={3}>
<Grid item xs={4}>
<Box textAlign="center">
<Restaurant sx={{ fontSize: 32, color: 'primary.main', mb: 1 }} />
<Typography variant="h5" fontWeight="600">
{dailySummary.feedingCount || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
Feedings
</Typography>
</Box>
</Grid>
<Grid item xs={4}>
<Box textAlign="center">
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
<Typography variant="h5" fontWeight="600">
{dailySummary.sleepTotalMinutes
? formatSleepHours(dailySummary.sleepTotalMinutes)
: '0m'}
</Typography>
<Typography variant="body2" color="text.secondary">
Sleep
</Typography>
</Box>
</Grid>
<Grid item xs={4}>
<Box textAlign="center">
<BabyChangingStation sx={{ fontSize: 32, color: 'warning.main', mb: 1 }} />
<Typography variant="h5" fontWeight="600">
{dailySummary.diaperCount || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
Diapers
</Typography>
</Box>
</Grid>
</Grid>
<Grid item xs={4}>
<Box textAlign="center">
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
<Typography variant="h5" fontWeight="600">
{dailySummary.sleepTotalMinutes
? formatSleepHours(dailySummary.sleepTotalMinutes)
: '0m'}
</Typography>
<Typography variant="body2" color="text.secondary">
Sleep
</Typography>
</Box>
</Grid>
<Grid item xs={4}>
<Box textAlign="center">
<BabyChangingStation sx={{ fontSize: 32, color: 'warning.main', mb: 1 }} />
<Typography variant="h5" fontWeight="600">
{dailySummary.diaperCount || 0}
</Typography>
<Typography variant="body2" color="text.secondary">
Diapers
</Typography>
</Box>
</Grid>
</Grid>
)}
</Paper>
)}
</Paper>
</ErrorBoundary>
{/* Next Predicted Activity */}
<Box sx={{ mt: 4 }}>

View File

@@ -44,6 +44,7 @@ import {
import { useRouter } from 'next/navigation';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { withErrorBoundary } from '@/components/common/ErrorFallbacks';
import { useAuth } from '@/lib/auth/AuthContext';
import { trackingApi, Activity } from '@/lib/api/tracking';
import { childrenApi, Child } from '@/lib/api/children';
@@ -60,7 +61,7 @@ interface FeedingData {
amountDescription?: string;
}
export default function FeedingTrackPage() {
function FeedingTrackPage() {
const router = useRouter();
const { user } = useAuth();
const [children, setChildren] = useState<Child[]>([]);
@@ -654,3 +655,5 @@ export default function FeedingTrackPage() {
</ProtectedRoute>
);
}
export default withErrorBoundary(FeedingTrackPage, 'form');