/** * React error boundary component. * * Catches render-time exceptions in child components and shows a fallback UI. * This is the base component; use PageErrorBoundary or SectionErrorBoundary * for page and section-level boundaries. * * All errors are logged using the telemetry service with structured context * for distributed tracing and debugging. */ import React from "react"; import { Button, makeStyles, Text, tokens } from "@fluentui/react-components"; import { recordCritical } from "../utils/telemetry"; interface ErrorBoundaryState { hasError: boolean; errorMessage: string | null; } interface ErrorBoundaryProps { children: React.ReactNode; title?: string; message?: string; showReloadButton?: boolean; isFullPage?: boolean; onError?: (error: Error, errorInfo: React.ErrorInfo) => void; } interface ErrorBoundaryFallbackProps { title: string; message: string; showReloadButton: boolean; isFullPage: boolean; onReload: () => void; } const useFullPageStyles = makeStyles({ root: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", padding: tokens.spacingVerticalL, textAlign: "center", gap: tokens.spacingVerticalM, }, message: { maxWidth: "40rem", }, }); const useSectionStyles = makeStyles({ root: { display: "flex", flexDirection: "column", alignItems: "flex-start", padding: tokens.spacingVerticalM, backgroundColor: tokens.colorNeutralBackground3, borderRadius: tokens.borderRadiusMedium, border: `1px solid ${tokens.colorStatusWarningForeground1}`, gap: tokens.spacingVerticalM, }, message: { color: tokens.colorNeutralForeground1, }, }); function ErrorBoundaryFallback({ title, message, showReloadButton, isFullPage, onReload, }: ErrorBoundaryFallbackProps): React.ReactElement { const fullPageStyles = useFullPageStyles(); const sectionStyles = useSectionStyles(); const styles = isFullPage ? fullPageStyles : sectionStyles; return (
{title} {message} {showReloadButton && ( )}
); } export class ErrorBoundary extends React.Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, errorMessage: null }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, errorMessage: error.message || "Unknown error" }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { const { onError } = this.props; // Extract componentStack from errorInfo // Note: componentStack is not part of the public React.ErrorInfo type definition, // but is available at runtime via React DevTools. We cast to any to access it. // This captures the React component hierarchy where the error occurred, which is // valuable for debugging but may change in future React versions. // TODO: Consider removing when React types include componentStack in ErrorInfo // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access const componentStack = (errorInfo as any).componentStack as string | undefined; // Log the error using telemetry for distributed tracing recordCritical("component_render_error", error, { component_stack: componentStack, error_message: error.message, }); if (onError) { onError(error, errorInfo); } else { console.error("ErrorBoundary caught an error", { error, errorInfo }); } } handleReload = (): void => { if (this.props.isFullPage) { window.location.reload(); } else { this.setState({ hasError: false, errorMessage: null }); } }; render(): React.ReactNode { if (this.state.hasError) { const { title = "Something went wrong", message = "Please try again or contact support if the problem persists.", showReloadButton = true, isFullPage = true, } = this.props; return ( ); } return this.props.children; } }