mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +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 { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||||
import { getCallLogs } from "@/lib/usageDb";
|
import { getCallLogs } from "@/lib/usageDb";
|
||||||
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
|
import { getCompletedDetails, getPendingById } from "@/lib/usage/usageHistory";
|
||||||
import { getProviderConnections } from "@/lib/localDb";
|
import { getProviderConnections } from "@/lib/db/providers";
|
||||||
import { getProviderNodes } from "@/models";
|
import { getProviderNodes } from "@/models";
|
||||||
import { matchesSearch } from "@/shared/utils/turkishText";
|
import { matchesSearch } from "@/shared/utils/turkishText";
|
||||||
|
|
||||||
@@ -27,6 +27,66 @@ function rowPriority(row: any): number {
|
|||||||
return 2;
|
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({
|
export function buildCallLogListRows({
|
||||||
logs,
|
logs,
|
||||||
connections,
|
connections,
|
||||||
@@ -174,15 +234,8 @@ export async function GET(request: Request) {
|
|||||||
completedDetails: getCompletedDetails().values(),
|
completedDetails: getCompletedDetails().values(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// When correlationId filter is set, also filter in-memory entries
|
const filtered = rows.filter((r: any) => rowMatchesFilter(r, filter));
|
||||||
// (active + completed) that don't match — getCallLogs already filters
|
return NextResponse.json(filtered);
|
||||||
// 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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[API ERROR] /api/usage/call-logs failed:", error);
|
console.error("[API ERROR] /api/usage/call-logs failed:", error);
|
||||||
return NextResponse.json({ error: "Failed to fetch call logs" }, { status: 500 });
|
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