All files / src/components ErrorBoundary.tsx

100% Statements 68/68
100% Branches 7/7
100% Functions 4/4
100% Lines 68/68

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 1261x 1x 1x 1x 1x 1x 1x   1x                 1x                                 1x 1x       1x       1x 1x 33x 33x 33x   1x           1x 1x 16x 16x 16x   1x           1x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x   1x     1x 1x 1x 1x 1x 1x 1x   1x         1x 1x 61x 24x 24x 24x 24x 24x 24x 24x 24x 24x   24x       24x   37x 37x 1x   1x  
import { captureOwnerStack, Component, ReactNode } from 'react';
import { trackRenderError } from '@/helpers/analytics';
import { getErrorMessage } from '@/helpers/exceptions';
import { i18n } from '@/helpers/i18n';
import { showReportDialog } from './ReportDialog';
import { ErrorReportButton } from './StyledComponents';
import { Logger } from '@/utils/Logger';
 
const logger = new Logger('ErrorBoundary');
 
/** Internal state: whether a descendant threw, and the captured error details. */
interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
  componentStack?: string;
}
 
/**
 * ErrorBoundary component that catches JavaScript errors anywhere in their child component tree,
 * logs those errors, and displays a fallback UI instead of the component tree that crashed.
 * The fallback includes a "Report Error" button that opens the bug-report dialog prefilled with
 * the caught error and its component stack.
 *
 * @component
 *
 * @param props - Component props
 *
 * @example
 * ```tsx
 * <ErrorBoundary fallback={<p>Something went wrong</p>}>
 *   <MyComponent />
 * </ErrorBoundary>
 * ```
 * @source
 */
class ErrorBoundary extends Component<
  { children: ReactNode; fallback: ReactNode },
  ErrorBoundaryState
> {
  /**
   * Creates an instance of ErrorBoundary.
   * @param props - Component props
   * @source
   */
  constructor(props: { children: ReactNode; fallback: ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }
 
  /**
   * Static lifecycle method that is called when a descendant component throws an error.
   * Updates the state to show the fallback UI and retain the error on the next render.
   * @param error - The error that was thrown
   * @returns New state object with hasError set to true and the error captured
   * @source
   */
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    // Update state so the next render will show the fallback UI.
    return { hasError: true, error };
  }
 
  /**
   * Lifecycle method that is called after an error has been thrown in a descendant component.
   * Logs the error and component stack trace, and retains the component stack for reporting.
   * @param error - The error that was thrown
   * @param info - React-supplied error info, including the component stack
   * @source
   */
  componentDidCatch(error: Error, info: { componentStack: string }) {
    logger.fatal(`Component render error: ${getErrorMessage(error)}`, {
      error,
      // Example "componentStack":
      //   in ComponentThatThrows (created by App)
      //   in ErrorBoundary (created by App)
      //   in div (created by App)
      //   in App
      componentStack: info.componentStack,
      // Warning: `captureOwnerStack` is not available in production.
      ownerStack: captureOwnerStack(),
    });
    this.setState({ componentStack: info.componentStack });
    // Report the crash to PostHog, component stack included (non-PII, best-effort).
    void trackRenderError(error, {}, info.componentStack);
  }
 
  /**
   * Opens the bug-report dialog prefilled with the caught error and component stack.
   * @source
   */
  handleReport = () => {
    void showReportDialog(this.state.error, {
      action: 'render-crash',
      componentStack: this.state.componentStack,
    });
  };
 
  /**
   * Renders either the children or the fallback UI (plus a report button) based on
   * whether an error has occurred.
   * @returns The rendered component
   * @source
   */
  render() {
    if (this.state.hasError) {
      // Rendered above the app's ThemeProvider, so use a plain button that needs no theme rather
      // than a themed MUI component (see ErrorReportButton).
      return (
        <>
          {this.props.fallback}
          <ErrorReportButton
            type="button"
            data-testid="error-boundary-report"
            onClick={this.handleReport}
          >
            {i18n('error_boundary_report')}
          </ErrorReportButton>
        </>
      );
    }
 
    return this.props.children;
  }
}
 
export default ErrorBoundary;