fix(logs): apply filter predicates to merged in-memory call-log rows (#11082)

Validated on a combined board over tip aa128736 (incl. sibling #11081): call-logs-row-filter 4/4 green, typecheck:core clean. The merged-row predicate fix closes a real gap — in-memory (in-flight/recently-completed) rows bypassed every filter except correlationId; rowMatchesFilter() now applies search/model/provider/account/apiKey/status/combo uniformly while DB rows stay idempotent. Thank you @AndrianBalanescu!
This commit is contained in:
Andrew B.
2026-08-22 14:55:09 -05:00
committed by GitHub
parent 56540f24c5
commit 80d931ae2d
2 changed files with 110 additions and 10 deletions

View File

@@ -3,7 +3,7 @@ export const dynamic = "force-dynamic";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { getCallLogs } from "@/lib/usageDb";
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
import { getProviderConnections } from "@/lib/localDb";
import { getProviderConnections } from "@/lib/db/providers";
import { getProviderNodes } from "@/models";
import { matchesSearch } from "@/shared/utils/turkishText";
@@ -27,6 +27,66 @@ function rowPriority(row: any): number {
return 2;
}
/**
* Applies the active filter predicates to a single merged call-log row.
*
* `getCallLogs()` already filters the persisted DB rows server-side, but the
* in-memory entries (active/pending + recently-completed) are merged in by
* `buildCallLogListRows()` and would otherwise bypass every filter except
* `correlationId`. Running the same predicates over the merged rows closes that
* gap. It is idempotent for DB rows (they already satisfy the predicate) while
* correctly excluding in-memory rows that do not match.
*/
export function rowMatchesFilter(row: any, filter: Record<string, any>): boolean {
if (!filter) return true;
if (filter.status === "error") {
if (!(Number(row?.status) >= 400 || Boolean(row?.error))) return false;
} else if (filter.status === "ok") {
if (!(Number(row?.status) >= 200 && Number(row?.status) < 300)) return false;
} else if (typeof filter.status === "number" || (typeof filter.status === "string" && !isNaN(Number(filter.status)))) {
if (Number(row?.status) !== Number(filter.status)) return false;
}
if (filter.model && !matchesSearch(row?.model || "", String(filter.model))) {
return false;
}
if (filter.provider && !matchesSearch(row?.provider || "", String(filter.provider))) {
return false;
}
if (filter.account && !matchesSearch(row?.account || "", String(filter.account))) {
return false;
}
if (filter.apiKey && !matchesSearch(row?.apiKeyName || "", String(filter.apiKey))) {
return false;
}
if (filter.combo && !matchesSearch(row?.comboName || "", String(filter.combo))) {
return false;
}
if (filter.correlationId && !matchesSearch(row?.correlationId || "", String(filter.correlationId))) {
return false;
}
if (filter.search) {
const term = String(filter.search);
const haystack = [
row?.model,
row?.provider,
row?.providerDisplay,
row?.account,
row?.apiKeyName,
row?.comboName,
row?.correlationId,
row?.error,
row?.path,
]
.filter(Boolean)
.join(" ");
if (!matchesSearch(haystack, term)) return false;
}
return true;
}
export function buildCallLogListRows({
logs,
connections,
@@ -174,15 +234,8 @@ export async function GET(request: Request) {
completedDetails: getCompletedDetails().values(),
});
// When correlationId filter is set, also filter in-memory entries
// (active + completed) that don't match — getCallLogs already filters
// the DB rows but activeEntries/completedEntries bypass it.
if (filter.correlationId) {
const cid = filter.correlationId;
return NextResponse.json(rows.filter((r: any) => matchesSearch(r.correlationId || "", cid)));
}
return NextResponse.json(rows);
const filtered = rows.filter((r: any) => rowMatchesFilter(r, filter));
return NextResponse.json(filtered);
} catch (error) {
console.error("[API ERROR] /api/usage/call-logs failed:", error);
return NextResponse.json({ error: "Failed to fetch call logs" }, { status: 500 });

View File

@@ -0,0 +1,47 @@
import test from "node:test";
import assert from "node:assert/strict";
import { rowMatchesFilter } from "../../src/app/api/usage/call-logs/route.ts";
test.describe("call-logs rowMatchesFilter unit tests", () => {
const baseRow = {
id: "log-1",
status: 200,
model: "openai/gpt-4o",
provider: "openai",
providerDisplay: "OpenAI Main",
account: "Work Account",
apiKeyName: "DevKey",
comboName: "SmartRouter",
correlationId: "corr-12345",
path: "/v1/chat/completions",
error: null,
};
test("status filter matches ok, error, and explicit status codes", () => {
assert.equal(rowMatchesFilter(baseRow, { status: "ok" }), true);
assert.equal(rowMatchesFilter(baseRow, { status: "error" }), false);
assert.equal(rowMatchesFilter(baseRow, { status: 200 }), true);
assert.equal(rowMatchesFilter(baseRow, { status: 500 }), false);
const errorRow = { ...baseRow, status: 500, error: "Internal Error" };
assert.equal(rowMatchesFilter(errorRow, { status: "ok" }), false);
assert.equal(rowMatchesFilter(errorRow, { status: "error" }), true);
});
test("provider filter matches provider name and excludes mismatched in-memory rows", () => {
assert.equal(rowMatchesFilter(baseRow, { provider: "openai" }), true);
assert.equal(rowMatchesFilter(baseRow, { provider: "anthropic" }), false);
});
test("model filter matches model name and excludes mismatched in-memory rows", () => {
assert.equal(rowMatchesFilter(baseRow, { model: "gpt-4o" }), true);
assert.equal(rowMatchesFilter(baseRow, { model: "claude-3-5-sonnet" }), false);
});
test("search query matches across haystack fields", () => {
assert.equal(rowMatchesFilter(baseRow, { search: "SmartRouter" }), true);
assert.equal(rowMatchesFilter(baseRow, { search: "DevKey" }), true);
assert.equal(rowMatchesFilter(baseRow, { search: "corr-12345" }), true);
assert.equal(rowMatchesFilter(baseRow, { search: "non-existent" }), false);
});
});