All files / src/components ErrorBoundary.tsx

83.33% Statements 15/18
62.5% Branches 10/16
83.33% Functions 5/6
83.33% Lines 15/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 148 149 150 151 152 153                                                                              66x 66x               33x         16x     16x       16x     16x     1x 1x                     112x   48x 6x       42x                                                                                                                   64x               4x  
// src/components/ErrorBoundary.tsx
/**
 * React Error Boundary with Sentry integration.
 * Implements ADR-015: Application Performance Monitoring and Error Tracking.
 *
 * This component catches JavaScript errors anywhere in the child component tree,
 * logs them to Sentry/Bugsink, and displays a fallback UI instead of crashing.
 */
import { Component, ReactNode } from 'react';
import { Sentry, captureException, isSentryConfigured } from '../services/sentry.client';
 
interface ErrorBoundaryProps {
  /** Child components to render */
  children: ReactNode;
  /** Optional custom fallback UI. If not provided, uses default error message. */
  fallback?: ReactNode;
  /** Optional callback when an error is caught */
  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
 
interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
  eventId: string | null;
}
 
/**
 * Error Boundary component that catches React component errors
 * and reports them to Sentry/Bugsink.
 *
 * @example
 * ```tsx
 * <ErrorBoundary fallback={<p>Something went wrong.</p>}>
 *   <MyComponent />
 * </ErrorBoundary>
 * ```
 */
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      eventId: null,
    };
  }
 
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return { hasError: true, error };
  }
 
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    // Log to console in development
    console.error('ErrorBoundary caught an error:', error, errorInfo);
 
    // Report to Sentry with component stack
    const eventId = captureException(error, {
      componentStack: errorInfo.componentStack,
    });
 
    this.setState({ eventId: eventId ?? null });
 
    // Call optional onError callback
    this.props.onError?.(error, errorInfo);
  }
 
  handleReload = (): void => {
    window.location.reload();
  };
 
  handleReportFeedback = (): void => {
    if (isSentryConfigured && this.state.eventId) {
      // Open Sentry feedback dialog if available
      Sentry.showReportDialog({ eventId: this.state.eventId });
    }
  };
 
  render(): ReactNode {
    if (this.state.hasError) {
      // Custom fallback UI if provided
      if (this.props.fallback) {
        return this.props.fallback;
      }
 
      // Default fallback UI
      return (
        <div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
          <div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-center">
            <div className="text-red-500 dark:text-red-400 mb-4">
              <svg
                className="w-16 h-16 mx-auto"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
                aria-hidden="true"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
                />
              </svg>
            </div>
            <h1 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
              Something went wrong
            </h1>
            <p className="text-gray-600 dark:text-gray-400 mb-6">
              We&apos;re sorry, but an unexpected error occurred. Our team has been notified.
            </p>
            <div className="flex flex-col sm:flex-row gap-3 justify-center">
              <button
                onClick={this.handleReload}
                className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
              >
                Reload Page
              </button>
              {isSentryConfigured && this.state.eventId && (
                <button
                  onClick={this.handleReportFeedback}
                  className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
                >
                  Report Feedback
                </button>
              )}
            </div>
            {this.state.error && process.env.NODE_ENV === 'development' && (
              <details className="mt-6 text-left">
                <summary className="cursor-pointer text-sm text-gray-500 dark:text-gray-400">
                  Error Details (Development Only)
                </summary>
                <pre className="mt-2 p-3 bg-gray-100 dark:bg-gray-900 rounded text-xs overflow-auto max-h-48 text-red-600 dark:text-red-400">
                  {this.state.error.message}
                  {'\n\n'}
                  {this.state.error.stack}
                </pre>
              </details>
            )}
          </div>
        </div>
      );
    }
 
    return this.props.children;
  }
}
 
/**
 * Pre-configured Sentry ErrorBoundary from @sentry/react.
 * Use this for simpler integration when you don't need custom UI.
 */
export const SentryErrorBoundary = Sentry.ErrorBoundary;