From d7eebfaf07de156d508484d682028bbae091d3a5 Mon Sep 17 00:00:00 2001 From: Tyagiquamar <104281681+Tyagiquamar@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:46:11 +0530 Subject: [PATCH 1/2] fix(stats): preserve usage error status in aggregates and doctor JSONL usage logs record per-request status but parseLogFile dropped it, so /stats and doctor always reported successes. Preserve status, count errors in getStats, and surface the count in doctor diagnostics. --- src/doctor.ts | 2 +- src/stats.error-status.test.ts | 58 ++++++++++++++++++++++++++++++++++ src/stats.ts | 10 ++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/stats.error-status.test.ts diff --git a/src/doctor.ts b/src/doctor.ts index 5db1e80e..10cf8e1b 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -316,7 +316,7 @@ async function collectLogInfo(): Promise { return { requestsLast24h: stats.totalRequests, costLast24h: `$${stats.totalCost.toFixed(4)}`, - errorsFound: 0, // TODO: parse error logs + errorsFound: stats.errorCount, }; } catch { return { diff --git a/src/stats.error-status.test.ts b/src/stats.error-status.test.ts new file mode 100644 index 00000000..425e2ccf --- /dev/null +++ b/src/stats.error-status.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("usage log error status", () => { + let home: string; + + beforeEach(async () => { + vi.resetModules(); + home = await mkdtemp(join(tmpdir(), "clawrouter-error-status-")); + vi.doMock("node:os", async () => { + const actual = await vi.importActual("node:os"); + return { ...actual, homedir: () => home }; + }); + const logs = join(home, ".openclaw", "blockrun", "logs"); + await mkdir(logs, { recursive: true }); + const lines = [ + JSON.stringify({ + timestamp: "2026-10-24T10:00:00.000Z", + model: "gpt-4", + tier: "PREMIUM", + cost: 0.01, + baselineCost: 0.02, + savings: 0.5, + latencyMs: 120, + status: "success", + }), + JSON.stringify({ + timestamp: "2026-10-24T10:01:00.000Z", + model: "gpt-4", + tier: "PREMIUM", + cost: 0, + baselineCost: 0.02, + savings: 0, + latencyMs: 50, + status: "error", + }), + ].join("\n"); + await writeFile(join(logs, "usage-2026-10-24.jsonl"), lines); + }); + + afterEach(async () => { + vi.doUnmock("node:os"); + await rm(home, { recursive: true, force: true }); + }); + + it("counts error entries and marks them in recent log output", async () => { + const { getStats, formatRecentLogs } = await import("./stats.js"); + + const stats = await getStats(1); + expect(stats.totalRequests).toBe(2); + expect(stats.errorCount).toBe(1); + + const recent = await formatRecentLogs(1); + expect(recent).toContain("ERROR"); + }); +}); diff --git a/src/stats.ts b/src/stats.ts index 37df65a7..d16dd5dd 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -64,6 +64,7 @@ export type AggregatedStats = { byModel: Record; dailyBreakdown: DailyStats[]; entriesWithBaseline: number; // Entries with valid baseline tracking + errorCount: number; }; /** @@ -86,6 +87,7 @@ async function parseLogFile(filePath: string): Promise { baselineCost: entry.baselineCost || entry.cost || 0, savings: entry.savings || 0, latencyMs: entry.latencyMs || 0, + status: entry.status === "error" ? "error" : entry.status === "success" ? "success" : undefined, }); } catch { // Skip malformed lines, keep valid ones @@ -163,6 +165,7 @@ export async function getStats(days: number = 7): Promise { let totalCost = 0; let totalBaselineCost = 0; let totalLatency = 0; + let errorCount = 0; for (const file of filesToRead) { const date = file.replace("usage-", "").replace(".jsonl", ""); @@ -171,6 +174,12 @@ export async function getStats(days: number = 7): Promise { if (entries.length === 0) continue; + for (const entry of entries) { + if (entry.status === "error") { + errorCount++; + } + } + const dayStats = aggregateDay(date, entries); dailyBreakdown.push(dayStats); @@ -237,6 +246,7 @@ export async function getStats(days: number = 7): Promise { byModel: byModelWithPercentage, dailyBreakdown: dailyBreakdown.reverse(), // Oldest first for charts entriesWithBaseline, // How many entries have valid baseline tracking + errorCount, }; } From f45f0e3501c0ae836f28d1cb353a7bbad2c20968 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Sun, 25 Oct 2026 16:52:45 -0500 Subject: [PATCH 2/2] style(stats): prettier format + drop status cast now that UsageEntry carries it --- src/stats.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/stats.ts b/src/stats.ts index d16dd5dd..98404989 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -87,7 +87,7 @@ async function parseLogFile(filePath: string): Promise { baselineCost: entry.baselineCost || entry.cost || 0, savings: entry.savings || 0, latencyMs: entry.latencyMs || 0, - status: entry.status === "error" ? "error" : entry.status === "success" ? "success" : undefined, + status: entry.status === "error" || entry.status === "success" ? entry.status : undefined, }); } catch { // Skip malformed lines, keep valid ones @@ -377,8 +377,7 @@ export async function formatRecentLogs(requestedDays: number = 1): Promise 24 ? e.model.slice(0, 21) + "..." : e.model; const cost = `$${e.cost.toFixed(4)}`; const ms = e.latencyMs > 9999 ? `${(e.latencyMs / 1000).toFixed(1)}s` : `${e.latencyMs}ms`; - const status = - (e as UsageEntry & { status?: string }).status === "error" ? " ERROR " : " OK "; + const status = e.status === "error" ? " ERROR " : " OK "; totalCost += e.cost; lines.push( `║ ${displayTime.padEnd(16)}║ ${model.padEnd(24)}║ ${cost.padStart(7)}║ ${ms.padStart(4)}║${status}║`,