fix: aggregate hourly usage data into daily/monthly views for dashboard
This commit is contained in:
parent
790a338f4d
commit
e2bb06df60
1 changed files with 116 additions and 3 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { ApiUsageSummary } from '@prisma/client';
|
||||||
import { prisma } from '@tpmjs/db';
|
import { prisma } from '@tpmjs/db';
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
@ -5,6 +6,115 @@ import { API_KEY_SCOPES } from '~/lib/api-keys';
|
||||||
import { authenticateRequest, hasScope } from '~/lib/api-keys/middleware';
|
import { authenticateRequest, hasScope } from '~/lib/api-keys/middleware';
|
||||||
import { auth } from '~/lib/auth';
|
import { auth } from '~/lib/auth';
|
||||||
|
|
||||||
|
type Period = 'hourly' | 'daily' | 'monthly';
|
||||||
|
|
||||||
|
interface AggregatedSummary {
|
||||||
|
periodStart: Date;
|
||||||
|
totalRequests: number;
|
||||||
|
successRequests: number;
|
||||||
|
errorRequests: number;
|
||||||
|
totalTokensIn: number;
|
||||||
|
totalTokensOut: number;
|
||||||
|
avgLatencyMs: number;
|
||||||
|
endpointCounts: Record<string, number>;
|
||||||
|
estimatedCostCents: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate hourly summaries into daily or monthly periods
|
||||||
|
*/
|
||||||
|
function aggregateSummaries(
|
||||||
|
hourlySummaries: ApiUsageSummary[],
|
||||||
|
period: Period
|
||||||
|
): AggregatedSummary[] {
|
||||||
|
if (period === 'hourly') {
|
||||||
|
return hourlySummaries.map((s) => ({
|
||||||
|
periodStart: s.periodStart,
|
||||||
|
totalRequests: s.totalRequests,
|
||||||
|
successRequests: s.successRequests,
|
||||||
|
errorRequests: s.errorRequests,
|
||||||
|
totalTokensIn: s.totalTokensIn,
|
||||||
|
totalTokensOut: s.totalTokensOut,
|
||||||
|
avgLatencyMs: s.avgLatencyMs,
|
||||||
|
endpointCounts: s.endpointCounts as Record<string, number>,
|
||||||
|
estimatedCostCents: s.estimatedCostCents,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by period
|
||||||
|
const groups = new Map<string, ApiUsageSummary[]>();
|
||||||
|
|
||||||
|
for (const summary of hourlySummaries) {
|
||||||
|
const date = new Date(summary.periodStart);
|
||||||
|
let key: string;
|
||||||
|
|
||||||
|
if (period === 'daily') {
|
||||||
|
key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
|
||||||
|
} else {
|
||||||
|
// monthly
|
||||||
|
key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = groups.get(key) || [];
|
||||||
|
existing.push(summary);
|
||||||
|
groups.set(key, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate each group
|
||||||
|
const result: AggregatedSummary[] = [];
|
||||||
|
|
||||||
|
for (const [key, summaries] of groups) {
|
||||||
|
const [year, month, day] = key.split('-').map(Number) as [number, number, number | undefined];
|
||||||
|
const periodStart =
|
||||||
|
period === 'daily'
|
||||||
|
? new Date(Date.UTC(year, month - 1, day ?? 1))
|
||||||
|
: new Date(Date.UTC(year, month - 1, 1));
|
||||||
|
|
||||||
|
const aggregated: AggregatedSummary = {
|
||||||
|
periodStart,
|
||||||
|
totalRequests: 0,
|
||||||
|
successRequests: 0,
|
||||||
|
errorRequests: 0,
|
||||||
|
totalTokensIn: 0,
|
||||||
|
totalTokensOut: 0,
|
||||||
|
avgLatencyMs: 0,
|
||||||
|
endpointCounts: {},
|
||||||
|
estimatedCostCents: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalLatencyWeight = 0;
|
||||||
|
|
||||||
|
for (const s of summaries) {
|
||||||
|
aggregated.totalRequests += s.totalRequests;
|
||||||
|
aggregated.successRequests += s.successRequests;
|
||||||
|
aggregated.errorRequests += s.errorRequests;
|
||||||
|
aggregated.totalTokensIn += s.totalTokensIn;
|
||||||
|
aggregated.totalTokensOut += s.totalTokensOut;
|
||||||
|
aggregated.estimatedCostCents += s.estimatedCostCents;
|
||||||
|
|
||||||
|
// Weighted average for latency
|
||||||
|
totalLatencyWeight += s.avgLatencyMs * s.totalRequests;
|
||||||
|
|
||||||
|
// Merge endpoint counts
|
||||||
|
const counts = s.endpointCounts as Record<string, number>;
|
||||||
|
for (const [endpoint, count] of Object.entries(counts)) {
|
||||||
|
aggregated.endpointCounts[endpoint] = (aggregated.endpointCounts[endpoint] || 0) + count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aggregated.totalRequests > 0) {
|
||||||
|
aggregated.avgLatencyMs = Math.round(totalLatencyWeight / aggregated.totalRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(aggregated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by period start
|
||||||
|
result.sort((a, b) => a.periodStart.getTime() - b.periodStart.getTime());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
|
@ -82,11 +192,11 @@ export async function GET(request: NextRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch usage summaries
|
// Fetch hourly summaries (we always store hourly, then aggregate for daily/monthly)
|
||||||
const summaries = await prisma.apiUsageSummary.findMany({
|
const hourlySummaries = await prisma.apiUsageSummary.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
periodType: period,
|
periodType: 'hourly',
|
||||||
periodStart: {
|
periodStart: {
|
||||||
gte: start,
|
gte: start,
|
||||||
lte: end,
|
lte: end,
|
||||||
|
|
@ -96,6 +206,9 @@ export async function GET(request: NextRequest) {
|
||||||
orderBy: { periodStart: 'asc' },
|
orderBy: { periodStart: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Aggregate into requested period
|
||||||
|
const summaries = aggregateSummaries(hourlySummaries, period);
|
||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
const totals = summaries.reduce(
|
const totals = summaries.reduce(
|
||||||
(acc, summary) => ({
|
(acc, summary) => ({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue