AUDIT / src /hooks /useErrorReporter.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
5.8 kB
/**
* 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 });
}