From 6f418d6332a0fd2c319d024ab24d79e5176d7ecf Mon Sep 17 00:00:00 2001 From: tdurieux Date: Wed, 6 May 2026 13:46:06 +0300 Subject: [PATCH] log AnonymousError context detail in serialized output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the path/repo/url tied to an AnonymousError when it gets serialized for logging — previously logs only carried name, message, and httpStatus, which made file_not_found entries impossible to trace back to a specific file or repo. Extract the existing detail formatting out of toString() into a public detail() method, harden it against AnonymizedFile getters that can throw, and have serializeError include the result as a "detail" field. Co-Authored-By: Claude Opus 4.7 --- src/core/AnonymousError.ts | 44 ++++++++++++++++++++++++++------------ src/core/logger.ts | 9 ++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/core/AnonymousError.ts b/src/core/AnonymousError.ts index 40c40a8..392c0e9 100644 --- a/src/core/AnonymousError.ts +++ b/src/core/AnonymousError.ts @@ -27,21 +27,37 @@ export default class AnonymousError extends CustomError { this.cause = opt?.cause; } - toString(): string { - let out = ""; - let detail = this.value ? JSON.stringify(this.value) : null; - if (this.value instanceof Repository) { - detail = this.value.repoId; - } else if (this.value instanceof AnonymizedFile) { - detail = `/r/${this.value.repository.repoId}/${this.value.anonymizedPath}`; - } else if (this.value instanceof GitHubRepository) { - detail = `${this.value.fullName}`; - } else if (this.value instanceof User) { - detail = `${this.value.username}`; - } else if (this.value instanceof GitHubBase) { - detail = `GHDownload ${this.value.data.repoId}`; + detail(): string | undefined { + if (this.value == null) return undefined; + try { + if (this.value instanceof Repository) return this.value.repoId; + if (this.value instanceof AnonymizedFile) { + const repoId = this.value.repository?.repoId; + // anonymizedPath getter can throw if the file isn't initialized; + // fall back to whatever path is known. + let p: string | undefined; + try { + p = this.value.anonymizedPath; + } catch { + p = this.value.filePath; + } + return repoId ? `/r/${repoId}/${p ?? ""}` : p; + } + if (this.value instanceof GitHubRepository) return this.value.fullName; + if (this.value instanceof User) return this.value.username; + if (this.value instanceof GitHubBase) { + return `GHDownload ${this.value.data.repoId}`; + } + if (typeof this.value === "string") return this.value; + return JSON.stringify(this.value); + } catch { + return String(this.value); } - out += this.message; + } + + toString(): string { + let out = this.message; + const detail = this.detail(); if (detail) { out += `: ${detail}`; } diff --git a/src/core/logger.ts b/src/core/logger.ts index a14590e..dfbcc99 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -140,6 +140,7 @@ type ErrorLike = { cause?: unknown; request?: { url?: string; method?: string }; response?: { url?: string; status?: number }; + detail?: () => string | undefined; }; export function serializeError(err: unknown): Record { @@ -161,6 +162,14 @@ export function serializeError(err: unknown): Record { // AnonymousError carries an httpStatus and an inner cause. if (typeof e.httpStatus === "number") out.httpStatus = e.httpStatus; if (e.code !== undefined && e.code !== e.message) out.code = e.code; + if (typeof e.detail === "function") { + try { + const d = e.detail(); + if (d) out.detail = d; + } catch { + /* ignore */ + } + } if (e.cause) out.cause = serializeError(e.cause); // Only include the stack when there's nothing else useful — avoids dumping