Files
maternal-app/maternal-web/components/common/ErrorBoundary.tsx
Andrei 68e33712f1
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
feat: Add comprehensive error boundaries for graceful error handling
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>
2025-10-01 19:37:04 +00:00

210 lines
5.7 KiB
TypeScript

'use client';
import React, { Component, ReactNode } from 'react';
import { Box, Button, Typography, Paper, Alert } from '@mui/material';
import { ErrorOutline, Refresh, Home } from '@mui/icons-material';
import errorLogger, { ErrorSeverity } from '@/lib/services/errorLogger';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
isolate?: boolean; // If true, only this section fails, not the whole app
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
errorCount: number;
}
/**
* Error Boundary component that catches JavaScript errors in child components
* Displays fallback UI and logs errors for monitoring
*/
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
// Update state so the next render will show the fallback UI
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to console in development
if (process.env.NODE_ENV === 'development') {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
// Update state with error details
this.setState((prevState) => ({
errorInfo,
errorCount: prevState.errorCount + 1,
}));
// Call custom error handler if provided
if (this.props.onError) {
this.props.onError(error, errorInfo);
}
// Log to error tracking service (e.g., Sentry)
this.logErrorToService(error, errorInfo);
}
logErrorToService = (error: Error, errorInfo: React.ErrorInfo) => {
// Log to error tracking service
errorLogger.logError(
error,
{
componentStack: errorInfo.componentStack,
errorBoundary: this.props.isolate ? 'isolated' : 'global',
},
ErrorSeverity.ERROR
);
};
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
// Custom fallback UI if provided
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback UI
return (
<Box
sx={{
minHeight: this.props.isolate ? '200px' : '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
bgcolor: this.props.isolate ? 'transparent' : 'background.default',
}}
>
<Paper
elevation={3}
sx={{
maxWidth: 600,
width: '100%',
p: 4,
borderRadius: 3,
textAlign: 'center',
}}
>
<ErrorOutline
sx={{
fontSize: 64,
color: 'error.main',
mb: 2,
}}
/>
<Typography variant="h5" fontWeight="600" gutterBottom>
Oops! Something went wrong
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
{this.props.isolate
? 'This section encountered an error. The rest of the app should still work.'
: "We're sorry for the inconvenience. The error has been logged and we'll look into it."}
</Typography>
{process.env.NODE_ENV === 'development' && this.state.error && (
<Alert severity="error" sx={{ mb: 3, textAlign: 'left' }}>
<Typography variant="body2" fontWeight="600" gutterBottom>
{this.state.error.message}
</Typography>
<Typography
variant="caption"
component="pre"
sx={{
mt: 1,
fontSize: '0.7rem',
overflow: 'auto',
maxHeight: 200,
}}
>
{this.state.error.stack}
</Typography>
</Alert>
)}
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
<Button
variant="contained"
startIcon={<Refresh />}
onClick={this.handleReset}
size="large"
>
Try Again
</Button>
{!this.props.isolate && (
<Button
variant="outlined"
startIcon={<Home />}
onClick={this.handleGoHome}
size="large"
>
Go Home
</Button>
)}
</Box>
{this.state.errorCount > 1 && (
<Alert severity="warning" sx={{ mt: 3 }}>
This error has occurred {this.state.errorCount} times. You may want to refresh the
entire page or contact support.
</Alert>
)}
</Paper>
</Box>
);
}
return this.props.children;
}
}
/**
* Hook-based error boundary (for functional components)
* Note: This is a workaround since React doesn't have hook-based error boundaries yet
*/
export function useErrorHandler(error?: Error) {
const [, setError] = React.useState();
return React.useCallback(
(error: Error) => {
setError(() => {
throw error;
});
},
[setError]
);
}