// Where a caught render error came from. Kept in step with the allowlist in // backend/src/routes/clientErrors.ts, which refuses anything else rather than // logging under a guess — change one and you must change the other. export type ErrorContext = 'page' | 'catalogue' | 'modal'; export interface ClientErrorReport { context: ErrorContext; message: string; stack?: string; componentStack?: string; path: string; } // Fire and forget, and deliberately swallowing — the one place in this change // where swallowing is correct. This runs inside componentDidCatch, so a // reporter that threw — or rejected — would throw from the very thing that // exists to stop throws, and there would be nothing left to catch it. // // The try/catch guards the synchronous part: JSON.stringify(report) and the // call to fetch() itself both run before any promise exists, so React does // not guarantee componentDidCatch is handed a real Error — code can throw // anything, and an object with a circular message or stack makes // JSON.stringify throw. The .catch on the returned promise guards the async // part, the rejection once fetch has actually started. Neither covers the // other; both are required. export function reportClientError(report: ClientErrorReport): void { try { void fetch('/api/client-errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(report) }).catch(() => undefined); } catch { // Nothing to do — see the comment above. } }