import { createContext, useContext, useEffect, useRef, ReactNode } from 'react'; /** * Captured console error with metadata */ export interface ConsoleError { message: string; source?: string; lineno?: number; colno?: number; timestamp: number; } /** * Context value for bug report error capture */ export interface BugReportContextValue { /** Get all captured console errors */ getConsoleErrors: () => ConsoleError[]; /** Clear all captured console errors */ clearConsoleErrors: () => void; } const BugReportContext = createContext(null); /** Maximum number of errors to retain */ const MAX_ERRORS = 20; export interface BugReportProviderProps { children: ReactNode; /** Maximum number of errors to capture (default: 20) */ maxErrors?: number; } /** * Provider component that captures console errors for bug reports. * Wrap your app with this provider to enable error capture. * * @example * ```tsx * import { BugReportProvider } from '@convex-dev/feedback/react'; * * function App() { * return ( * * * * ); * } * ``` */ export function BugReportProvider({ children, maxErrors = MAX_ERRORS }: BugReportProviderProps) { const errorsRef = useRef([]); useEffect(() => { // Capture window.onerror const originalOnError = window.onerror; window.onerror = (message, source, lineno, colno, error) => { errorsRef.current.push({ message: String(message), source, lineno: lineno ?? undefined, colno: colno ?? undefined, timestamp: Date.now(), }); // Keep only last N errors if (errorsRef.current.length > maxErrors) { errorsRef.current = errorsRef.current.slice(-maxErrors); } // Call original handler if exists if (originalOnError) { return originalOnError(message, source, lineno, colno, error); } return false; }; // Capture unhandled promise rejections const handleUnhandledRejection = (event: PromiseRejectionEvent) => { errorsRef.current.push({ message: `Unhandled Promise Rejection: ${String(event.reason)}`, timestamp: Date.now(), }); if (errorsRef.current.length > maxErrors) { errorsRef.current = errorsRef.current.slice(-maxErrors); } }; window.addEventListener('unhandledrejection', handleUnhandledRejection); // Override console.error const originalConsoleError = console.error; console.error = (...args) => { errorsRef.current.push({ message: args.map((arg) => { if (arg instanceof Error) { return `${arg.name}: ${arg.message}`; } return String(arg); }).join(' '), timestamp: Date.now(), }); if (errorsRef.current.length > maxErrors) { errorsRef.current = errorsRef.current.slice(-maxErrors); } originalConsoleError.apply(console, args); }; return () => { window.onerror = originalOnError; window.removeEventListener('unhandledrejection', handleUnhandledRejection); console.error = originalConsoleError; }; }, [maxErrors]); const getConsoleErrors = () => [...errorsRef.current]; const clearConsoleErrors = () => { errorsRef.current = []; }; return ( {children} ); } /** * Hook to access bug report context for error capture. * Must be used within a BugReportProvider. * * @example * ```tsx * import { useBugReportContext } from '@convex-dev/feedback/react'; * * function MyComponent() { * const { getConsoleErrors, clearConsoleErrors } = useBugReportContext(); * // ... * } * ``` */ export function useBugReportContext(): BugReportContextValue { const context = useContext(BugReportContext); if (!context) { throw new Error('useBugReportContext must be used within a BugReportProvider'); } return context; }