/** * useErrorReporter.ts — Gap 2.2: Cattura errori JS non gestiti e li invia a /api/logs/frontend. * * Auto-registra window.onerror e window.onunhandledrejection al primo import (side effect). * Esporta anche un hook React per garantire mount nell'albero dei componenti. * * Features: * - Batching: accumula errori in un buffer da svuotare ogni 5s (max 10 per batch) * - Dedup: skip errori identici entro 10s (stesso msg+stack prefix) * - Rate limiting: max 20 errori per sessione → previene spam su loop di errori * - Fire-and-forget: fetch con keepalive:true, nessun await, zero impact UX * - Zero crash: try/catch ovunque, mai propaga eccezioni * * Compatibilità: Safari iOS 14+ (unhandledrejection disponibile da iOS 14.5). * Su versioni precedenti il handler viene skippato silenziosamente. */ import { useEffect } from "react"; const _BACKEND_URL = (import.meta.env.VITE_BACKEND_URL ?? "").replace(/\/$/, ""); const _ENDPOINT = _BACKEND_URL ? `${_BACKEND_URL}/api/logs/frontend` : null; // ── State modulo ────────────────────────────────────────────────────────────── let _sessionCount = 0; const _MAX_SESSION = 20; const _dedup = new Map(); // key → last_sent_ms const _DEDUP_TTL = 10_000; let _flushTimer: ReturnType | null = null; const _buf: Array<{ level: string; msg: string; source: string; url?: string; stack?: string; ts_ms: number; }> = []; function _dedupKey(msg: string, stack?: string): string { return `${msg.slice(0, 80)}|${(stack ?? "").slice(0, 80)}`; } function _isDuplicate(msg: string, stack?: string): boolean { const key = _dedupKey(msg, stack); const last = _dedup.get(key); const now = Date.now(); if (last && now - last < _DEDUP_TTL) return true; _dedup.set(key, now); // Prune vecchi entry per tenere la mappa piccola if (_dedup.size > 50) { for (const [k, t] of _dedup) { if (now - t > _DEDUP_TTL) _dedup.delete(k); if (_dedup.size <= 50) break; } } return false; } function _scheduleFlush(): void { if (_flushTimer !== null) return; _flushTimer = setTimeout(_flush, 5_000); } function _flush(): void { _flushTimer = null; if (!_ENDPOINT || _buf.length === 0) { _buf.length = 0; return; } const batch = _buf.splice(0, 10); try { for (const entry of batch) { // keepalive:true → funziona anche se la tab viene chiusa (iOS safe) fetch(_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(entry), keepalive: true, }).catch(() => { /* silent */ }); } } catch { // silent } } function _report( level: "error" | "warn" | "info", msg: string, opts?: { source?: string; url?: string; stack?: string } ): void { try { if (!_ENDPOINT) return; if (_sessionCount >= _MAX_SESSION) return; if (_isDuplicate(msg, opts?.stack)) return; _sessionCount++; _buf.push({ level, msg: msg.slice(0, 500), source: opts?.source ?? "window", url: opts?.url ?? (typeof location !== "undefined" ? location.href : undefined), stack: opts?.stack?.slice(0, 800), ts_ms: Date.now(), }); _scheduleFlush(); } catch { // silent — mai impattare l'app } } // ── Auto-registrazione side effect ──────────────────────────────────────────── let _registered = false; function _registerGlobalHandlers(): void { if (_registered || typeof window === "undefined") return; _registered = true; const _prevOnError = window.onerror; window.onerror = (msg, source, _line, _col, error) => { _report("error", String(msg), { source: "window.onerror", url: String(source ?? ""), stack: error?.stack, }); if (typeof _prevOnError === "function") { return (_prevOnError as (...a: unknown[]) => unknown)(msg, source, _line, _col, error) as boolean; } return false; }; // unhandledrejection — Safari iOS 14.5+ try { window.addEventListener("unhandledrejection", (e) => { const reason = e.reason; const msg = reason instanceof Error ? reason.message : String(reason ?? "Unhandled promise rejection"); const stack = reason instanceof Error ? reason.stack : undefined; _report("error", msg, { source: "unhandledrejection", stack }); }, { passive: true }); } catch { // WKWebView fallback — silent } } // Registra immediatamente all'import (side effect di modulo) _registerGlobalHandlers(); // ── React hook ──────────────────────────────────────────────────────────────── /** * Hook React per montare il reporter nell'albero dei componenti. * La registrazione avviene già come side effect di import, questo hook * garantisce il re-mount su HMR in sviluppo. */ export function useErrorReporter(): void { useEffect(() => { _registerGlobalHandlers(); }, []); } /** * Report manuale di un errore (es. catch block, errore di business logic). * * @example * try { ... } catch(e) { reportError(e, { source: "agentLoop" }); } */ export function reportError( error: Error | string, opts?: { source?: string; level?: "error" | "warn" | "info" } ): void { const msg = error instanceof Error ? error.message : String(error); const stack = error instanceof Error ? error.stack : undefined; _report(opts?.level ?? "error", msg, { source: opts?.source, stack }); }