Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
048ace7
feat: add project hourly stats aggregation for faster dashboards
steebchen Feb 8, 2026
1145618
refactor: use statsAggregatedAt marker for accurate aggregation
steebchen Feb 9, 2026
991d4a5
feat: add API key aggregation and detailed breakdown fields
steebchen Feb 9, 2026
13290da
fix(api): update activity tests to use aggregation tables
steebchen Feb 9, 2026
99d555d
chore: push
steebchen Feb 9, 2026
fe7a33e
feat(scripts): add script to generate random test logs
steebchen Feb 9, 2026
be1d436
fix(scripts): use nanoid directly instead of shortid export
steebchen Feb 9, 2026
b31112a
fix(scripts): move generate-test-logs to packages/db
steebchen Feb 9, 2026
8b8fa93
refactor(scripts): create dedicated scripts package for monorepo util…
steebchen Feb 9, 2026
efe3b85
feat(scripts): add 30% discount for smaller models in test logs
steebchen Feb 9, 2026
de66153
refactor(scripts): move all TS scripts to @llmgateway/scripts package
steebchen Feb 9, 2026
80e30bb
feat(stats): add apiKeyHourlyModelStats table for per-key model break…
steebchen Feb 9, 2026
28eaa80
refactor(admin): use aggregation tables for org metrics, remove lazy …
steebchen Feb 9, 2026
be012f3
Merge remote-tracking branch 'origin/main' into add-stats-aggregations
steebchen Feb 9, 2026
47765e7
feat(stats): sync with main, add image cost fields to aggregation tables
steebchen Feb 9, 2026
7241529
Merge remote-tracking branch 'origin/main' into sync-stats-aggregations
steebchen Feb 10, 2026
5e2e63e
chore(autofix): apply diff
steebchen Feb 10, 2026
27a884a
Merge remote-tracking branch 'origin/main' into sync-stats-aggregations
steebchen Feb 10, 2026
92214be
feat(db): regenerate migrations for stats tables
steebchen Feb 10, 2026
16c772c
Merge branch 'add-stats-aggregations' of github.com:theopenco/llmgate…
steebchen Feb 10, 2026
f127869
fix: regenerate clean migrations
steebchen Feb 10, 2026
a81383c
feat(stats): enhance log processing for backfill and reprocessing
steebchen Feb 10, 2026
c5bdf4d
feat(stats): add stale detection and model breakdown
steebchen Feb 10, 2026
ba07bac
style: use sharp lines in dashboard charts
steebchen Feb 10, 2026
3d85f62
feat(stats): add configuration for backfill and stale detection
steebchen Feb 10, 2026
ef16624
feat(stats): add per-mode breakdowns to aggregations
steebchen Feb 10, 2026
1eb6d36
fix: regenerate clean migrations
steebchen Feb 10, 2026
dc0434a
fix(stats): use UTC strings for timestamps
steebchen Feb 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 17 additions & 31 deletions apps/admin/src/app/organizations/[orgId]/org-metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Loader2,
Server,
} from "lucide-react";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";

import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -90,7 +90,7 @@ function MetricCard({

export function OrgMetricsSection({ orgId }: { orgId: string }) {
const [metrics, setMetrics] = useState<OrganizationMetrics | null>(null);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [window, setWindow] = useState<TokenWindow>("1d");

const loadMetrics = useCallback(
Expand All @@ -103,35 +103,14 @@ export function OrgMetricsSection({ orgId }: { orgId: string }) {
[orgId],
);

const handleWindowChange = useCallback(
(w: TokenWindow) => {
setWindow(w);
loadMetrics(w);
},
[loadMetrics],
);
// Load metrics automatically on mount
useEffect(() => {
loadMetrics(window);
}, [loadMetrics, window]);

if (!metrics && !loading) {
return (
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Usage Metrics</h2>
<Button
variant="outline"
size="sm"
onClick={() => loadMetrics(window)}
>
<Activity className="mr-2 h-4 w-4" />
Load Usage Data
</Button>
</div>
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center text-sm text-muted-foreground">
Click &quot;Load Usage Data&quot; to fetch usage metrics. This queries
the logs table which may be slow for orgs with lots of data.
</div>
</section>
);
}
const handleWindowChange = useCallback((w: TokenWindow) => {
setWindow(w);
}, []);

if (loading) {
return (
Expand All @@ -146,7 +125,14 @@ export function OrgMetricsSection({ orgId }: { orgId: string }) {
}

if (!metrics) {
return null;
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold">Usage Metrics</h2>
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center text-sm text-muted-foreground">
No usage data available.
</div>
</section>
);
}

const windowLabel = window === "7d" ? "Last 7 days" : "Last 24 hours";
Expand Down
8 changes: 8 additions & 0 deletions apps/admin/src/lib/api/v1.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,14 @@ export interface paths {
cacheCount: number;
cacheRate: number;
discountSavings: number;
creditsRequestCount: number;
apiKeysRequestCount: number;
creditsCost: number;
apiKeysCost: number;
creditsServiceFee: number;
apiKeysServiceFee: number;
creditsDataStorageCost: number;
apiKeysDataStorageCost: number;
modelBreakdown: {
id: string;
provider: string;
Expand Down
32 changes: 31 additions & 1 deletion apps/api/src/routes/activity.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";

import { app } from "@/index.js";
import { createTestUser, deleteAll } from "@/testing.js";
import {
createTestUser,
deleteAll,
aggregateLogsForTesting,
} from "@/testing.js";

import { db, tables } from "@llmgateway/db";

Expand Down Expand Up @@ -173,6 +177,9 @@ describe("activity endpoint", () => {
usedMode: "api-keys",
},
]);

// Aggregate logs into the hourly stats tables for the activity endpoint
await aggregateLogsForTesting();
});

afterEach(async () => {
Expand Down Expand Up @@ -335,6 +342,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -513,6 +522,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -628,6 +639,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -727,6 +740,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -830,6 +845,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand All @@ -855,6 +872,7 @@ describe("activity endpoint", () => {

test("GET /activity should return zero error rate and cache rate when no requests", async () => {
await db.delete(tables.log);
await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Expand Down Expand Up @@ -944,6 +962,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -1037,6 +1057,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -1129,6 +1151,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -1202,6 +1226,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -1275,6 +1301,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down Expand Up @@ -1453,6 +1481,8 @@ describe("activity endpoint", () => {
},
]);

await aggregateLogsForTesting();

const res = await app.request("/activity?days=7", {
headers: {
Cookie: token,
Expand Down
Loading
Loading