All files / src/components/shared/ErrorBoundary ErrorBoundary.tsx

94.44% Statements 17/18
100% Branches 8/8
100% Functions 6/6
94.44% Lines 17/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                        32x   32x                                               38x 38x               40x               20x 20x   20x                                                   2x 2x             81x 81x   81x   40x 6x       34x                                                                             41x          
import { Component, ErrorInfo, ReactNode } from 'react';
import i18next from 'i18next';
import { Alert, Button, Box } from '@mui/material';
 
/**
 * Sanitize error message to prevent XSS attacks
 * Removes HTML tags and limits message length
 * @param message - Raw error message
 * @returns Sanitized error message safe for display
 */
function sanitizeErrorMessage(message: string): string {
  // Remove HTML tags
  const withoutTags = message.replace(/<[^>]*>/g, '');
  // Limit length to prevent DoS via large error messages
  return withoutTags.slice(0, 500);
}
 
interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: (props: { error: Error; resetError: () => void }) => ReactNode;
}
 
interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}
 
/**
 * ErrorBoundary Component
 * Catches React component errors and displays fallback UI
 *
 * @example
 * <ErrorBoundary>
 *   <MyComponent />
 * </ErrorBoundary>
 */
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
    };
  }
 
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    // Update state so the next render will show the fallback UI
    return {
      hasError: true,
      error,
    };
  }
 
  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    // Log error details to console
    try {
      const correlationId = (error as Error & { correlationId?: string }).correlationId;
 
      console.error('ErrorBoundary caught an error:', {
        message: error.message,
        stack: error.stack,
        correlationId,
        componentStack: errorInfo.componentStack,
      });
 
      // TODO: Integrate with Sentry or other error tracking service
      // if (window.Sentry) {
      //   window.Sentry.captureException(error, {
      //     contexts: {
      //       react: {
      //         componentStack: errorInfo.componentStack,
      //       },
      //     },
      //     tags: {
      //       correlationId,
      //     },
      //   });
      // }
    } catch (loggingError) {
      // Prevent logging errors from crashing the error boundary
      console.warn('Failed to log error:', loggingError);
    }
  }
 
  resetError = (): void => {
    this.setState({
      hasError: false,
      error: null,
    });
  };
 
  render(): ReactNode {
    const { hasError, error } = this.state;
    const { children, fallback } = this.props;
 
    if (hasError && error) {
      // If custom fallback provided, use it
      if (fallback) {
        return fallback({ error, resetError: this.resetError });
      }
 
      // Default fallback UI
      return (
        <Box
          sx={{
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            minHeight: '400px',
            padding: 3,
          }}
        >
          <Alert
            severity="error"
            role="alert"
            aria-live="assertive"
            sx={{
              maxWidth: '600px',
              width: '100%',
              marginBottom: 2,
            }}
          >
            <Box>
              <strong>{i18next.t('common:errors.somethingWentWrong')}</strong>
              {error.message && (
                <Box sx={{ marginTop: 1 }}>{sanitizeErrorMessage(error.message)}</Box>
              )}
            </Box>
          </Alert>
          <Button
            variant="contained"
            onClick={this.resetError}
            aria-label={i18next.t('common:errors.tryAgain')}
          >
            {i18next.t('common:errors.tryAgain')}
          </Button>
        </Box>
      );
    }
 
    return children;
  }
}
 
export default ErrorBoundary;