Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, number>(); // key → last_sent_ms | |
| const _DEDUP_TTL = 10_000; | |
| let _flushTimer: ReturnType<typeof setTimeout> | 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 }); | |
| } | |