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>
237 lines
6.2 KiB
TypeScript
237 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { Box, Button, Typography, Paper } from '@mui/material';
|
|
import { BrokenImage, CloudOff, BugReport, DataObject } from '@mui/icons-material';
|
|
import { ReactNode } from 'react';
|
|
|
|
interface FallbackProps {
|
|
error?: Error;
|
|
resetError?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Generic minimal error fallback
|
|
*/
|
|
export function MinimalErrorFallback({ error, resetError }: FallbackProps) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
p: 2,
|
|
textAlign: 'center',
|
|
bgcolor: 'error.light',
|
|
borderRadius: 2,
|
|
border: '1px solid',
|
|
borderColor: 'error.main',
|
|
}}
|
|
>
|
|
<Typography variant="body2" color="error.dark" gutterBottom>
|
|
Something went wrong
|
|
</Typography>
|
|
{resetError && (
|
|
<Button size="small" onClick={resetError} variant="outlined" color="error">
|
|
Retry
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fallback for image loading errors
|
|
*/
|
|
export function ImageErrorFallback() {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
width: '100%',
|
|
height: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
bgcolor: 'grey.100',
|
|
borderRadius: 1,
|
|
}}
|
|
>
|
|
<BrokenImage sx={{ fontSize: 48, color: 'grey.400' }} />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fallback for API/data fetching errors
|
|
*/
|
|
export function DataErrorFallback({ error, resetError }: FallbackProps) {
|
|
return (
|
|
<Paper
|
|
sx={{
|
|
p: 3,
|
|
textAlign: 'center',
|
|
bgcolor: 'background.paper',
|
|
border: '1px dashed',
|
|
borderColor: 'divider',
|
|
}}
|
|
>
|
|
<CloudOff sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
|
|
<Typography variant="h6" gutterBottom>
|
|
Failed to Load Data
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
{error?.message || 'Unable to fetch the requested data. Please try again.'}
|
|
</Typography>
|
|
{resetError && (
|
|
<Button variant="contained" onClick={resetError}>
|
|
Retry
|
|
</Button>
|
|
)}
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fallback for rendering errors in components
|
|
*/
|
|
export function ComponentErrorFallback({ error, resetError }: FallbackProps) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
p: 3,
|
|
border: '2px dashed',
|
|
borderColor: 'warning.main',
|
|
borderRadius: 2,
|
|
bgcolor: 'warning.light',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
|
<BugReport sx={{ color: 'warning.dark' }} />
|
|
<Typography variant="subtitle1" fontWeight="600" color="warning.dark">
|
|
Component Error
|
|
</Typography>
|
|
</Box>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
This component failed to render. The rest of the page should still work.
|
|
</Typography>
|
|
{process.env.NODE_ENV === 'development' && error && (
|
|
<Typography
|
|
variant="caption"
|
|
component="pre"
|
|
sx={{
|
|
p: 1,
|
|
bgcolor: 'background.paper',
|
|
borderRadius: 1,
|
|
overflow: 'auto',
|
|
fontSize: '0.7rem',
|
|
}}
|
|
>
|
|
{error.message}
|
|
</Typography>
|
|
)}
|
|
{resetError && (
|
|
<Button size="small" variant="outlined" onClick={resetError} sx={{ mt: 1 }}>
|
|
Reload Component
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fallback for form errors
|
|
*/
|
|
export function FormErrorFallback({ error, resetError }: FallbackProps) {
|
|
return (
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 2,
|
|
bgcolor: 'error.light',
|
|
border: '1px solid',
|
|
borderColor: 'error.main',
|
|
borderRadius: 2,
|
|
}}
|
|
>
|
|
<Typography variant="subtitle2" color="error.dark" gutterBottom>
|
|
Form Submission Error
|
|
</Typography>
|
|
<Typography variant="body2" color="error.dark" sx={{ mb: 2 }}>
|
|
{error?.message || 'Unable to process your form. Please try again.'}
|
|
</Typography>
|
|
{resetError && (
|
|
<Button size="small" variant="contained" color="error" onClick={resetError}>
|
|
Try Again
|
|
</Button>
|
|
)}
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fallback for data visualization/chart errors
|
|
*/
|
|
export function ChartErrorFallback({ resetError }: FallbackProps) {
|
|
return (
|
|
<Box
|
|
sx={{
|
|
height: 300,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
bgcolor: 'grey.50',
|
|
borderRadius: 2,
|
|
border: '1px dashed',
|
|
borderColor: 'divider',
|
|
}}
|
|
>
|
|
<DataObject sx={{ fontSize: 48, color: 'text.disabled', mb: 2 }} />
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
Unable to display chart
|
|
</Typography>
|
|
{resetError && (
|
|
<Button size="small" onClick={resetError} sx={{ mt: 1 }}>
|
|
Reload
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wrapper component to easily apply error boundary with custom fallback
|
|
*/
|
|
interface ErrorBoundaryWrapperProps {
|
|
children: ReactNode;
|
|
fallbackType?: 'minimal' | 'component' | 'data' | 'form' | 'chart';
|
|
customFallback?: ReactNode;
|
|
}
|
|
|
|
export function withErrorBoundary<P extends object>(
|
|
Component: React.ComponentType<P>,
|
|
fallbackType: ErrorBoundaryWrapperProps['fallbackType'] = 'component'
|
|
) {
|
|
return function WithErrorBoundaryWrapper(props: P) {
|
|
const { ErrorBoundary } = require('./ErrorBoundary');
|
|
|
|
const getFallback = (error: Error, resetError: () => void) => {
|
|
switch (fallbackType) {
|
|
case 'minimal':
|
|
return <MinimalErrorFallback error={error} resetError={resetError} />;
|
|
case 'data':
|
|
return <DataErrorFallback error={error} resetError={resetError} />;
|
|
case 'form':
|
|
return <FormErrorFallback error={error} resetError={resetError} />;
|
|
case 'chart':
|
|
return <ChartErrorFallback resetError={resetError} />;
|
|
case 'component':
|
|
default:
|
|
return <ComponentErrorFallback error={error} resetError={resetError} />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ErrorBoundary isolate fallback={(error: Error, reset: () => void) => getFallback(error, reset)}>
|
|
<Component {...props} />
|
|
</ErrorBoundary>
|
|
);
|
|
};
|
|
}
|