mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 19:22:32 +03:00
* feat(dashboard): request count log per provider, per date (#4009) Some providers bill by request rather than by token, so operators need a plain per-provider, per-date request count breakdown, not just token aggregates. Adds a new getProviderDailyUsageRows() aggregation query (src/lib/db/usageAnalytics.ts), a dedicated GET /api/usage/requests-by-provider-date route (kept separate from the frozen /api/usage/analytics route to respect the file-size baseline), and a sortable, single-date-filterable table on Dashboard -> Analytics. Closes #4009 * chore(6812): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)
This commit is contained in:
committed by
GitHub
parent
e8fce67e70
commit
28fcd418a4
115
src/app/api/usage/requests-by-provider-date/route.ts
Normal file
115
src/app/api/usage/requests-by-provider-date/route.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { buildUnifiedSource, getProviderDailyUsageRows } from "@/lib/db/usageAnalytics";
|
||||
|
||||
/**
|
||||
* GET /api/usage/requests-by-provider-date — #4009
|
||||
*
|
||||
* Per-day, per-provider request counts (some providers bill by request, not
|
||||
* token, so operators need a plain count broken down by date). Sliced into
|
||||
* its own route (rather than folding into /api/usage/analytics) because that
|
||||
* route's file size is frozen at the quality-gate baseline.
|
||||
*
|
||||
* Query params (all optional, mirror /api/usage/analytics):
|
||||
* - range: "1d" | "7d" | "30d" | "90d" | "ytd" | "all" (default "30d")
|
||||
* - startDate / endDate: ISO-8601, overrides `range` when both are set
|
||||
* - date: YYYY-MM-DD, narrows the result to a single calendar date
|
||||
*/
|
||||
|
||||
function getRangeStartIso(range: string): string | null {
|
||||
const end = new Date();
|
||||
const start = new Date(end);
|
||||
|
||||
switch (range) {
|
||||
case "1d":
|
||||
start.setDate(start.getDate() - 1);
|
||||
break;
|
||||
case "7d":
|
||||
start.setDate(start.getDate() - 7);
|
||||
break;
|
||||
case "30d":
|
||||
start.setDate(start.getDate() - 30);
|
||||
break;
|
||||
case "90d":
|
||||
start.setDate(start.getDate() - 90);
|
||||
break;
|
||||
case "ytd":
|
||||
start.setMonth(0, 1);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case "all":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return start.toISOString();
|
||||
}
|
||||
|
||||
interface DateWindow {
|
||||
sinceIso: string | null;
|
||||
untilIso: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the query window from the request's params: an explicit single
|
||||
* `date` wins, then an explicit `startDate`/`endDate` pair, then falls back
|
||||
* to the named `range` preset.
|
||||
*/
|
||||
function resolveDateWindow(searchParams: URLSearchParams, range: string): DateWindow {
|
||||
const singleDate = searchParams.get("date") || undefined;
|
||||
if (singleDate) {
|
||||
return { sinceIso: `${singleDate}T00:00:00.000Z`, untilIso: `${singleDate}T23:59:59.999Z` };
|
||||
}
|
||||
const startDate = searchParams.get("startDate") || undefined;
|
||||
const endDate = searchParams.get("endDate") || undefined;
|
||||
return {
|
||||
sinceIso: startDate || getRangeStartIso(range),
|
||||
untilIso: endDate || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveRawCutoffDate(): Promise<string> {
|
||||
const { getUserDatabaseSettings } = await import("@/lib/db/databaseSettings");
|
||||
const dbSettings = getUserDatabaseSettings();
|
||||
const rawRetentionDays = dbSettings.aggregation?.rawDataRetentionDays ?? 30;
|
||||
const rawCutoff = new Date();
|
||||
rawCutoff.setDate(rawCutoff.getDate() - rawRetentionDays);
|
||||
return rawCutoff.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function errorResponse(error: unknown): Promise<Response> {
|
||||
console.error("Error computing requests-by-provider-date:", error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return import("@omniroute/open-sse/utils/error").then(({ buildErrorBody }) =>
|
||||
NextResponse.json(buildErrorBody(500, message || "Failed to compute requests-by-provider-date"), {
|
||||
status: 500,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const range = searchParams.get("range") || "30d";
|
||||
const singleDate = searchParams.get("date") || undefined;
|
||||
const { sinceIso, untilIso } = resolveDateWindow(searchParams, range);
|
||||
const rawCutoffDate = await resolveRawCutoffDate();
|
||||
|
||||
const { unifiedSource, unifiedParams } = buildUnifiedSource({
|
||||
sinceIso,
|
||||
untilIso,
|
||||
rawCutoffDate,
|
||||
apiKeyWhere: "",
|
||||
apiKeyParams: {},
|
||||
});
|
||||
|
||||
const rows = getProviderDailyUsageRows(unifiedSource, unifiedParams);
|
||||
|
||||
return NextResponse.json({ rows, range, date: singleDate ?? null });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user