diff --git a/src/usage/log.ts b/src/usage/log.ts index 7d7ed5ea11..7bca650964 100644 --- a/src/usage/log.ts +++ b/src/usage/log.ts @@ -1142,6 +1142,14 @@ export async function readUsageEntriesForManagement(): Promise; + if (typeof row.requestId !== "string" || typeof row.provider !== "string") return undefined; + return normalizeUsageEntry(row as unknown as PersistedUsageEntry); +} + export function readUsageEntries(): PersistedUsageEntry[] { const path = usageLogPath(); if (!existsSync(path)) return []; @@ -1150,10 +1158,8 @@ export function readUsageEntries(): PersistedUsageEntry[] { for (const line of lines) { if (!line.trim()) continue; try { - const parsed = JSON.parse(line) as PersistedUsageEntry; - if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") { - entries.push(normalizeUsageEntry(parsed)); - } + const parsed = normalizePersistedUsageRow(JSON.parse(line)); + if (parsed) entries.push(parsed); } catch { /* keep reading after a partially written or hand-edited line */ } @@ -1166,10 +1172,8 @@ function parseUsageLines(lines: string[]): PersistedUsageEntry[] { for (const line of lines) { if (!line.trim()) continue; try { - const parsed = JSON.parse(line) as PersistedUsageEntry; - if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") { - entries.push(normalizeUsageEntry(parsed)); - } + const parsed = normalizePersistedUsageRow(JSON.parse(line)); + if (parsed) entries.push(parsed); } catch { /* skip partial / hand-edited lines */ } diff --git a/tests/usage-log.test.ts b/tests/usage-log.test.ts index 8414287b01..e22c7e84b3 100644 --- a/tests/usage-log.test.ts +++ b/tests/usage-log.test.ts @@ -226,7 +226,7 @@ describe("usage log", () => { test("usage byte-prefix truncation and entry-count truncation report independent metadata", async () => { writeFileSync( usageLogPath(), - `${Array.from({ length: 500_001 }, (_, index) => JSON.stringify({ requestId: String(index) })).join("\n")}\n`, + `${Array.from({ length: 500_001 }, (_, index) => JSON.stringify({ requestId: String(index), provider: "p" })).join("\n")}\n`, ); const snapshot = await readUsageSnapshotForManagement(); expect(snapshot.entries).toHaveLength(500_000); @@ -739,14 +739,26 @@ describe("usage log", () => { }]); }); - test("skips malformed JSONL lines while keeping valid entries", () => { + test("skips malformed JSONL and rows without string usage identities", async () => { writeFileSync(usageLogPath(), [ - "{\"requestId\":\"a\",\"timestamp\":1,\"provider\":\"p\",\"model\":\"m\",\"status\":200,\"durationMs\":1,\"usageStatus\":\"unreported\"}", + persistedLine("a"), "{not-json", - "{\"requestId\":\"b\",\"timestamp\":2,\"provider\":\"p\",\"model\":\"m\",\"status\":200,\"durationMs\":1,\"usageStatus\":\"reported\",\"usage\":{\"inputTokens\":1,\"outputTokens\":2},\"totalTokens\":3}", + "null", + "42", + "[]", + "{}", + JSON.stringify({ provider: "p", timestamp: 2 }), + JSON.stringify({ requestId: 42, provider: "p", timestamp: 2 }), + JSON.stringify({ requestId: "missing-provider", timestamp: 2 }), + JSON.stringify({ requestId: "null-provider", provider: null, timestamp: 2 }), + JSON.stringify({ requestId: "number-provider", provider: 42, timestamp: 2 }), + JSON.stringify({ requestId: "object-provider", provider: {}, timestamp: 2 }), + JSON.stringify({ requestId: "array-provider", provider: [], timestamp: 2 }), + persistedLine("b"), ].join("\n")); expect(readUsageEntries().map(entry => entry.requestId)).toEqual(["a", "b"]); + expect((await readUsageEntriesForManagement()).map(entry => entry.requestId)).toEqual(["a", "b"]); }); test("keeps missing usage distinct from zero usage", () => {