[autoresearch] baseline: system prompt quality tests (5/5 passing)
This commit is contained in:
1 parent
05cc036f6d
commit
60b0101717
142 files changed
+18589
-9947
No files matched your search
@@ -0,0 +1,658 @@
|
||||
/**
|
||||
* DevLogger — Development-only comprehensive interaction logger.
|
||||
*
|
||||
* Captures the FULL lifecycle of every chat interaction:
|
||||
* 1. User input + context assembly
|
||||
* 2. API request (headers, body, model, params)
|
||||
* 3. SSE stream chunks (raw + parsed)
|
||||
* 4. Response parsing (thinking extraction, transcript cleanup)
|
||||
* 5. DB writes (what was persisted)
|
||||
* 6. Error states + recovery actions
|
||||
*
|
||||
* All data is held in memory and can be exported as JSON or viewed
|
||||
* in the browser console via `window.__devLog`.
|
||||
*
|
||||
* Only active when import.meta.env.DEV is true.
|
||||
*/
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────
|
||||
|
||||
export interface InteractionLog {
|
||||
id: string;
|
||||
chatId: number | null;
|
||||
startedAt: string;
|
||||
finishedAt: string | null;
|
||||
durationMs: number | null;
|
||||
status: "pending" | "streaming" | "complete" | "error" | "aborted";
|
||||
|
||||
// 1. User input
|
||||
userInput: {
|
||||
rawText: string;
|
||||
charCount: number;
|
||||
estimatedTokens: number;
|
||||
};
|
||||
|
||||
// 2. Context sent to API
|
||||
context: {
|
||||
messageCount: number;
|
||||
messages: Array<{ role: string; contentPreview: string; contentLength: number }>;
|
||||
totalContextChars: number;
|
||||
memoryEnabled: boolean;
|
||||
agenticMode: boolean;
|
||||
};
|
||||
|
||||
// 3. API request
|
||||
request: {
|
||||
url: string;
|
||||
method: string;
|
||||
model: string;
|
||||
stream: boolean;
|
||||
maxTokens: number;
|
||||
stopSequences: string[];
|
||||
headers: Record<string, string>;
|
||||
bodySize: number;
|
||||
fullBody: unknown;
|
||||
};
|
||||
|
||||
// 4. API response
|
||||
response: {
|
||||
httpStatus: number | null;
|
||||
httpStatusText: string | null;
|
||||
contentType: string | null;
|
||||
responseHeaders: Record<string, string>;
|
||||
};
|
||||
|
||||
// 5. Streaming data
|
||||
streaming: {
|
||||
chunksReceived: number;
|
||||
rawChunks: Array<{
|
||||
index: number;
|
||||
timestamp: string;
|
||||
raw: string;
|
||||
parsed: unknown | null;
|
||||
deltaContent: string | null;
|
||||
error: string | null;
|
||||
}>;
|
||||
totalDeltaChars: number;
|
||||
tokenCount: number;
|
||||
firstTokenAt: string | null;
|
||||
lastTokenAt: string | null;
|
||||
timeToFirstTokenMs: number | null;
|
||||
};
|
||||
|
||||
// 6. Parsed result
|
||||
parsed: {
|
||||
rawContent: string;
|
||||
visibleContent: string;
|
||||
reasoningContent: string;
|
||||
thinkingOpen: boolean;
|
||||
isCorrupted: boolean;
|
||||
transcriptLeakageDetected: boolean;
|
||||
contentLength: number;
|
||||
};
|
||||
|
||||
// 7. Usage stats from API
|
||||
usage: {
|
||||
promptTokens: number | null;
|
||||
completionTokens: number | null;
|
||||
totalTokens: number | null;
|
||||
tokensPerSecond: number | null;
|
||||
};
|
||||
|
||||
// 8. DB persistence
|
||||
dbWrites: Array<{
|
||||
timestamp: string;
|
||||
operation: string;
|
||||
table: string;
|
||||
params: unknown[];
|
||||
resultId: number | null;
|
||||
}>;
|
||||
|
||||
// 9. Errors
|
||||
errors: Array<{
|
||||
timestamp: string;
|
||||
phase: string;
|
||||
message: string;
|
||||
stack: string | null;
|
||||
raw: unknown;
|
||||
}>;
|
||||
|
||||
// 10. Sanitization / encoding diagnostics
|
||||
sanitization: Array<{
|
||||
timestamp: string;
|
||||
stage: string;
|
||||
changed: boolean;
|
||||
repairedEncoding: boolean;
|
||||
beforePreview: string;
|
||||
afterPreview: string;
|
||||
}>;
|
||||
|
||||
// 11. Timeline of phases
|
||||
timeline: Array<{
|
||||
timestamp: string;
|
||||
phase: string;
|
||||
detail: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ─── Logger Class ────────────────────────────────────────────────
|
||||
|
||||
const IS_DEV = Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV);
|
||||
const MAX_LOGS = 50;
|
||||
|
||||
class DevLogger {
|
||||
private interactions: InteractionLog[] = [];
|
||||
private current: InteractionLog | null = null;
|
||||
private enabled: boolean;
|
||||
private startTime: number = 0;
|
||||
|
||||
constructor() {
|
||||
this.enabled = IS_DEV;
|
||||
if (this.enabled && typeof window !== "undefined") {
|
||||
(window as unknown as Record<string, unknown>).__devLog = this;
|
||||
console.log(
|
||||
"%c[DevLogger] 🔍 Development logging ACTIVE — access via window.__devLog",
|
||||
"color: #22c55e; font-weight: bold; font-size: 13px",
|
||||
);
|
||||
console.log(
|
||||
"%c .current → current interaction\n .interactions → all logged interactions\n .export() → JSON export\n .dump() → console table\n .dumpLast() → last interaction detail\n .clear() → reset",
|
||||
"color: #999; font-size: 11px",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Interaction Lifecycle ──
|
||||
|
||||
startInteraction(chatId: number | null, userText: string, opts: {
|
||||
memoryEnabled: boolean;
|
||||
agenticMode: boolean;
|
||||
}): string {
|
||||
if (!this.enabled) return "";
|
||||
|
||||
const id = `int_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
this.startTime = performance.now();
|
||||
|
||||
this.current = {
|
||||
id,
|
||||
chatId,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: null,
|
||||
durationMs: null,
|
||||
status: "pending",
|
||||
userInput: {
|
||||
rawText: userText,
|
||||
charCount: userText.length,
|
||||
estimatedTokens: Math.ceil(userText.length / 4),
|
||||
},
|
||||
context: {
|
||||
messageCount: 0,
|
||||
messages: [],
|
||||
totalContextChars: 0,
|
||||
memoryEnabled: opts.memoryEnabled,
|
||||
agenticMode: opts.agenticMode,
|
||||
},
|
||||
request: {
|
||||
url: "",
|
||||
method: "POST",
|
||||
model: "",
|
||||
stream: true,
|
||||
maxTokens: 0,
|
||||
stopSequences: [],
|
||||
headers: {},
|
||||
bodySize: 0,
|
||||
fullBody: null,
|
||||
},
|
||||
response: {
|
||||
httpStatus: null,
|
||||
httpStatusText: null,
|
||||
contentType: null,
|
||||
responseHeaders: {},
|
||||
},
|
||||
streaming: {
|
||||
chunksReceived: 0,
|
||||
rawChunks: [],
|
||||
totalDeltaChars: 0,
|
||||
tokenCount: 0,
|
||||
firstTokenAt: null,
|
||||
lastTokenAt: null,
|
||||
timeToFirstTokenMs: null,
|
||||
},
|
||||
parsed: {
|
||||
rawContent: "",
|
||||
visibleContent: "",
|
||||
reasoningContent: "",
|
||||
thinkingOpen: false,
|
||||
isCorrupted: false,
|
||||
transcriptLeakageDetected: false,
|
||||
contentLength: 0,
|
||||
},
|
||||
usage: {
|
||||
promptTokens: null,
|
||||
completionTokens: null,
|
||||
totalTokens: null,
|
||||
tokensPerSecond: null,
|
||||
},
|
||||
dbWrites: [],
|
||||
errors: [],
|
||||
sanitization: [],
|
||||
timeline: [],
|
||||
};
|
||||
|
||||
this.addTimeline("init", `Interaction started — chatId=${chatId}, text="${userText.slice(0, 60)}…"`);
|
||||
this.logPhase("📝 User Input", { text: userText.slice(0, 200), chars: userText.length });
|
||||
return id;
|
||||
}
|
||||
|
||||
logContext(messages: Array<{ role: string; content: string }>) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const ctx = this.current.context;
|
||||
ctx.messageCount = messages.length;
|
||||
ctx.messages = messages.map((m) => ({
|
||||
role: m.role,
|
||||
contentPreview: m.content.slice(0, 100),
|
||||
contentLength: m.content.length,
|
||||
}));
|
||||
ctx.totalContextChars = messages.reduce((sum, m) => sum + m.content.length, 0);
|
||||
|
||||
this.addTimeline("context", `${messages.length} messages, ${ctx.totalContextChars} chars total`);
|
||||
this.logPhase("📦 Context Assembly", {
|
||||
messageCount: messages.length,
|
||||
totalChars: ctx.totalContextChars,
|
||||
roles: messages.map((m) => m.role).join(", "),
|
||||
messages: ctx.messages,
|
||||
});
|
||||
}
|
||||
|
||||
logRequest(url: string, body: unknown, headers: Record<string, string>) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const bodyStr = JSON.stringify(body);
|
||||
const req = this.current.request;
|
||||
req.url = url;
|
||||
req.fullBody = body;
|
||||
req.bodySize = bodyStr.length;
|
||||
req.headers = sanitizeHeaders(headers);
|
||||
|
||||
if (typeof body === "object" && body !== null) {
|
||||
const b = body as Record<string, unknown>;
|
||||
req.model = String(b.model ?? "");
|
||||
req.stream = Boolean(b.stream);
|
||||
req.maxTokens = Number(b.max_tokens ?? 0);
|
||||
req.stopSequences = Array.isArray(b.stop) ? (b.stop as string[]) : [];
|
||||
}
|
||||
|
||||
this.addTimeline("request", `POST ${url} — model=${req.model}, stream=${req.stream}, body=${bodyStr.length}B`);
|
||||
this.logPhase("🚀 API Request", {
|
||||
url,
|
||||
model: req.model,
|
||||
stream: req.stream,
|
||||
maxTokens: req.maxTokens,
|
||||
bodySize: bodyStr.length,
|
||||
stopSequences: req.stopSequences,
|
||||
fullBody: body,
|
||||
});
|
||||
}
|
||||
|
||||
logResponse(res: Response) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const resp = this.current.response;
|
||||
resp.httpStatus = res.status;
|
||||
resp.httpStatusText = res.statusText;
|
||||
resp.contentType = res.headers.get("content-type");
|
||||
res.headers.forEach((value, key) => {
|
||||
resp.responseHeaders[key] = value;
|
||||
});
|
||||
|
||||
this.addTimeline("response", `HTTP ${res.status} ${res.statusText} — ${resp.contentType}`);
|
||||
this.logPhase("📡 API Response", {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
contentType: resp.contentType,
|
||||
headers: resp.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
logStreamChunk(raw: string, parsed: unknown | null, deltaContent: string | null, error: string | null = null) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const s = this.current.streaming;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
s.chunksReceived++;
|
||||
|
||||
// Store first 100 chunks in full + every 50th after that for inspection
|
||||
if (s.chunksReceived <= 100 || s.chunksReceived % 50 === 0) {
|
||||
s.rawChunks.push({
|
||||
index: s.chunksReceived,
|
||||
timestamp: now,
|
||||
raw: raw.slice(0, 500),
|
||||
parsed,
|
||||
deltaContent,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
if (deltaContent) {
|
||||
s.totalDeltaChars += deltaContent.length;
|
||||
s.tokenCount++;
|
||||
s.lastTokenAt = now;
|
||||
|
||||
if (!s.firstTokenAt) {
|
||||
s.firstTokenAt = now;
|
||||
s.timeToFirstTokenMs = performance.now() - this.startTime;
|
||||
this.addTimeline("first_token", `TTFT=${s.timeToFirstTokenMs.toFixed(0)}ms, delta="${deltaContent}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.current.errors.push({ timestamp: now, phase: "stream_chunk", message: error, stack: null, raw });
|
||||
}
|
||||
|
||||
// Log first 3, then every 50th chunk to console
|
||||
if (s.chunksReceived <= 3 || s.chunksReceived % 50 === 0) {
|
||||
console.debug(
|
||||
`%c[DevLogger] 🔄 Chunk #${s.chunksReceived}`,
|
||||
"color: #60a5fa",
|
||||
{ delta: deltaContent, totalChars: s.totalDeltaChars, raw: raw.slice(0, 120) },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logNonStreamResponse(data: unknown) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
this.addTimeline("non_stream_response", "Full response received (non-streaming)");
|
||||
this.logPhase("📩 Non-Stream Response", data);
|
||||
}
|
||||
|
||||
logParsedResult(result: {
|
||||
rawContent: string;
|
||||
visibleContent: string;
|
||||
reasoningContent: string;
|
||||
thinkingOpen: boolean;
|
||||
isCorrupted: boolean;
|
||||
transcriptLeakageDetected: boolean;
|
||||
}) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const p = this.current.parsed;
|
||||
Object.assign(p, result);
|
||||
p.contentLength = result.visibleContent.length;
|
||||
|
||||
this.addTimeline("parsed", `visible=${result.visibleContent.length}ch, reasoning=${result.reasoningContent.length}ch, corrupted=${result.isCorrupted}`);
|
||||
|
||||
const logStyle = result.isCorrupted
|
||||
? "%c[DevLogger] ⚠️ PARSED RESULT (CORRUPTED)"
|
||||
: "%c[DevLogger] ✅ Parsed Result";
|
||||
const logColor = result.isCorrupted ? "color: #ef4444; font-weight: bold" : "color: #22c55e";
|
||||
|
||||
console.groupCollapsed(logStyle, logColor);
|
||||
console.log("Raw content:", result.rawContent);
|
||||
console.log("Visible content:", result.visibleContent);
|
||||
console.log("Reasoning:", result.reasoningContent || "(none)");
|
||||
console.log("Thinking still open:", result.thinkingOpen);
|
||||
console.log("Corrupted:", result.isCorrupted);
|
||||
console.log("Transcript leak:", result.transcriptLeakageDetected);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
logUsage(usage: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
total_tokens?: number;
|
||||
tokens_per_second?: number;
|
||||
} | null) {
|
||||
if (!this.enabled || !this.current || !usage) return;
|
||||
|
||||
this.current.usage = {
|
||||
promptTokens: usage.prompt_tokens ?? null,
|
||||
completionTokens: usage.completion_tokens ?? null,
|
||||
totalTokens: usage.total_tokens ?? null,
|
||||
tokensPerSecond: usage.tokens_per_second ?? null,
|
||||
};
|
||||
|
||||
this.addTimeline("usage", `prompt=${usage.prompt_tokens}, completion=${usage.completion_tokens}, tps=${usage.tokens_per_second?.toFixed(1)}`);
|
||||
this.logPhase("📊 Usage Stats", usage);
|
||||
}
|
||||
|
||||
logDBWrite(operation: string, table: string, params: unknown[], resultId: number | null = null) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
this.current.dbWrites.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
operation,
|
||||
table,
|
||||
params: params.map((p) => typeof p === "string" && p.length > 200 ? p.slice(0, 200) + "…" : p),
|
||||
resultId,
|
||||
});
|
||||
|
||||
this.addTimeline("db_write", `${operation} → ${table}${resultId ? ` (id=${resultId})` : ""}`);
|
||||
console.debug(`%c[DevLogger] 💾 DB ${operation}`, "color: #a855f7", { table, params: params.map((p) => typeof p === "string" && p.length > 100 ? p.slice(0, 100) + "…" : p), resultId });
|
||||
}
|
||||
|
||||
logSanitization(stage: string, payload: {
|
||||
before: string;
|
||||
after: string;
|
||||
changed: boolean;
|
||||
repairedEncoding?: boolean;
|
||||
}) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
this.current.sanitization.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
stage,
|
||||
changed: payload.changed,
|
||||
repairedEncoding: Boolean(payload.repairedEncoding),
|
||||
beforePreview: payload.before.slice(0, 240),
|
||||
afterPreview: payload.after.slice(0, 240),
|
||||
});
|
||||
|
||||
if (payload.changed || payload.repairedEncoding) {
|
||||
this.addTimeline(
|
||||
"sanitize",
|
||||
`${stage}: changed=${payload.changed} repairedEncoding=${Boolean(payload.repairedEncoding)}`,
|
||||
);
|
||||
console.warn(`%c[DevLogger] 🧼 Sanitization @ ${stage}`, "color: #f59e0b; font-weight: bold", {
|
||||
repairedEncoding: Boolean(payload.repairedEncoding),
|
||||
before: payload.before,
|
||||
after: payload.after,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logError(phase: string, error: unknown) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.current.errors.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
phase,
|
||||
message: err.message,
|
||||
stack: err.stack ?? null,
|
||||
raw: error,
|
||||
});
|
||||
this.current.status = "error";
|
||||
|
||||
this.addTimeline("error", `[${phase}] ${err.message}`);
|
||||
console.error(`%c[DevLogger] ❌ Error in ${phase}`, "color: #ef4444; font-weight: bold", err);
|
||||
}
|
||||
|
||||
logAbort(partialContent: string) {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
this.current.status = "aborted";
|
||||
this.addTimeline("abort", `User aborted — partial content: ${partialContent.length} chars`);
|
||||
this.logPhase("🛑 Aborted", { partialContentLength: partialContent.length, preview: partialContent.slice(0, 200) });
|
||||
}
|
||||
|
||||
finishInteraction() {
|
||||
if (!this.enabled || !this.current) return;
|
||||
|
||||
const elapsed = performance.now() - this.startTime;
|
||||
this.current.durationMs = Math.round(elapsed);
|
||||
this.current.finishedAt = new Date().toISOString();
|
||||
if (this.current.status === "pending" || this.current.status === "streaming") {
|
||||
this.current.status = "complete";
|
||||
}
|
||||
|
||||
this.addTimeline("finish", `Duration=${elapsed.toFixed(0)}ms, status=${this.current.status}`);
|
||||
|
||||
// Summary console output
|
||||
const c = this.current;
|
||||
console.groupCollapsed(
|
||||
`%c[DevLogger] 🏁 Interaction ${c.id} — ${c.status} in ${elapsed.toFixed(0)}ms`,
|
||||
c.status === "error" ? "color: #ef4444; font-weight: bold" : "color: #22c55e; font-weight: bold",
|
||||
);
|
||||
console.table({
|
||||
"User Input": `${c.userInput.charCount} chars`,
|
||||
"Context Messages": c.context.messageCount,
|
||||
"Context Chars": c.context.totalContextChars,
|
||||
"Model": c.request.model,
|
||||
"Stream": c.request.stream,
|
||||
"HTTP Status": c.response.httpStatus,
|
||||
"Chunks Received": c.streaming.chunksReceived,
|
||||
"TTFT (ms)": c.streaming.timeToFirstTokenMs?.toFixed(0) ?? "n/a",
|
||||
"Stream Tokens": c.streaming.tokenCount,
|
||||
"Stream Chars": c.streaming.totalDeltaChars,
|
||||
"Final Content": `${c.parsed.contentLength} chars`,
|
||||
"Reasoning": c.parsed.reasoningContent ? `${c.parsed.reasoningContent.length} chars` : "none",
|
||||
"Corrupted": c.parsed.isCorrupted,
|
||||
"Prompt Tokens": c.usage.promptTokens,
|
||||
"Completion Tokens": c.usage.completionTokens,
|
||||
"TPS": c.usage.tokensPerSecond?.toFixed(1) ?? "n/a",
|
||||
"DB Writes": c.dbWrites.length,
|
||||
"Sanitizations": c.sanitization.length,
|
||||
"Errors": c.errors.length,
|
||||
"Duration (ms)": elapsed.toFixed(0),
|
||||
});
|
||||
if (c.errors.length > 0) {
|
||||
console.warn("Errors:", c.errors);
|
||||
}
|
||||
if (c.parsed.isCorrupted) {
|
||||
console.warn("⚠️ CORRUPTED RESPONSE — raw content:", c.parsed.rawContent);
|
||||
}
|
||||
// Log response content comparison for debugging "response 1" issue
|
||||
if (c.parsed.visibleContent.trim().length <= 5) {
|
||||
console.warn(
|
||||
"⚠️ SUSPICIOUSLY SHORT RESPONSE:",
|
||||
{
|
||||
rawFromStream: c.streaming.totalDeltaChars,
|
||||
rawContent: c.parsed.rawContent,
|
||||
visibleContent: c.parsed.visibleContent,
|
||||
allChunks: c.streaming.rawChunks,
|
||||
},
|
||||
);
|
||||
}
|
||||
console.groupEnd();
|
||||
|
||||
// Archive
|
||||
this.interactions.push(this.current);
|
||||
if (this.interactions.length > MAX_LOGS) {
|
||||
this.interactions = this.interactions.slice(-MAX_LOGS);
|
||||
}
|
||||
this.current = null;
|
||||
}
|
||||
|
||||
// ── Public Inspection API ──
|
||||
|
||||
/** Export all logs as JSON (for file download / clipboard). */
|
||||
export(): string {
|
||||
return JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
interactionCount: this.interactions.length,
|
||||
interactions: this.interactions,
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
/** Console table of all interactions. */
|
||||
dump() {
|
||||
if (this.interactions.length === 0) {
|
||||
console.log("[DevLogger] No interactions recorded yet.");
|
||||
return;
|
||||
}
|
||||
console.table(
|
||||
this.interactions.map((i) => ({
|
||||
id: i.id,
|
||||
chatId: i.chatId,
|
||||
status: i.status,
|
||||
model: i.request.model,
|
||||
userInput: i.userInput.rawText.slice(0, 40),
|
||||
responseLen: i.parsed.contentLength,
|
||||
corrupted: i.parsed.isCorrupted,
|
||||
chunks: i.streaming.chunksReceived,
|
||||
ttft: i.streaming.timeToFirstTokenMs?.toFixed(0),
|
||||
tps: i.usage.tokensPerSecond?.toFixed(1),
|
||||
durationMs: i.durationMs,
|
||||
errors: i.errors.length,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/** Detailed dump of the last interaction. */
|
||||
dumpLast() {
|
||||
const last = this.interactions[this.interactions.length - 1];
|
||||
if (!last) {
|
||||
console.log("[DevLogger] No interactions recorded yet.");
|
||||
return;
|
||||
}
|
||||
console.log(`[DevLogger] Last interaction: ${last.id}`);
|
||||
console.log("Timeline:");
|
||||
last.timeline.forEach((t) => console.log(` ${t.timestamp} [${t.phase}] ${t.detail}`));
|
||||
console.log("\nFull data:", last);
|
||||
}
|
||||
|
||||
/** Return the latest interaction, optionally filtered by chatId. */
|
||||
getLatestInteraction(chatId?: number | null): InteractionLog | null {
|
||||
if (this.interactions.length === 0) return null;
|
||||
if (chatId === undefined) return this.interactions[this.interactions.length - 1] ?? null;
|
||||
|
||||
for (let index = this.interactions.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = this.interactions[index];
|
||||
if (candidate.chatId === chatId) return candidate;
|
||||
}
|
||||
|
||||
return this.interactions[this.interactions.length - 1] ?? null;
|
||||
}
|
||||
|
||||
/** Clear all logs. */
|
||||
clear() {
|
||||
this.interactions = [];
|
||||
this.current = null;
|
||||
console.log("[DevLogger] Cleared all logs.");
|
||||
}
|
||||
|
||||
// ── Internal helpers ──
|
||||
|
||||
private addTimeline(phase: string, detail: string) {
|
||||
this.current?.timeline.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
phase,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
|
||||
private logPhase(label: string, data: unknown) {
|
||||
console.groupCollapsed(`%c[DevLogger] ${label}`, "color: #60a5fa; font-weight: bold");
|
||||
console.log(data);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
|
||||
const safe = { ...headers };
|
||||
// Mask authorization tokens
|
||||
for (const key of Object.keys(safe)) {
|
||||
if (key.toLowerCase().includes("auth") || key.toLowerCase().includes("key")) {
|
||||
safe[key] = safe[key].slice(0, 10) + "…[MASKED]";
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
// ── Singleton Export ──
|
||||
|
||||
export const devLog = new DevLogger();
|
||||
Reference in new issue
Block a user