export type HttpErrorDiagnostic = Readonly<{ name: string; code?: string; errno?: number; sqlState?: string; frames: readonly string[]; }>; /** Extrae datos técnicos sin serializar mensajes, SQL ni propiedades arbitrarias del error. */ export function httpErrorDiagnostics(failure: unknown): readonly HttpErrorDiagnostic[] { const diagnostics: HttpErrorDiagnostic[] = []; const visited = new Set(); let current = failure; while (current instanceof Error && !visited.has(current) && diagnostics.length < 5) { visited.add(current); diagnostics.push(describeError(current)); current = current.cause instanceof Error ? current.cause : "parent" in current ? current.parent : undefined; } return diagnostics.length ? diagnostics : [{ name: "NonErrorThrown", frames: [] }]; } function describeError(failure: Error): HttpErrorDiagnostic { const code = "code" in failure ? failure.code : undefined; const errno = "errno" in failure ? failure.errno : undefined; const sqlState = "sqlState" in failure ? failure.sqlState : undefined; // El prefijo puede contener un mensaje multilínea: se elimina completo antes de leer los frames. const prefix = failure.message ? `${failure.name}: ${failure.message}` : failure.name; const stack = failure.stack?.startsWith(prefix) ? failure.stack.slice(prefix.length) : ""; return { name: /^[A-Za-z][A-Za-z0-9]*Error$|^Error$/.test(failure.name) ? failure.name : "Error", ...(typeof code === "string" && /^(?:ER_[A-Z0-9_]+|E[A-Z0-9_]+)$/.test(code) ? { code } : {}), ...(typeof errno === "number" && Number.isSafeInteger(errno) ? { errno } : {}), ...(typeof sqlState === "string" && /^[0-9A-Z]{5}$/.test(sqlState) ? { sqlState } : {}), frames: stack .split("\n") .filter((line) => /^\s+at .+:\d+:\d+\)?$/.test(line)) .slice(0, 20) .map((line) => line.trim()), }; }