mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 23:22:09 +03:00
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:
@@ -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 });
|
||||
|
||||
47
tests/unit/call-logs-row-filter.test.ts
Normal file
47
tests/unit/call-logs-row-filter.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user