feat(web,coder): add analytics + results pages for token usage and run history
New /analytics route: token usage dashboard with aggregate summary, per-session breakdown, context window stats, and per-category token distribution. Data served from existing agent_sessions + tool_cost_stats. New /results route: browsable archive of orchestrator flow runs and arena battles. Two-tab layout (Analysis Runs / Arena Battles) using existing API endpoints (no new backend). Sidebar gains Results (ScrollText icon) and Token Analytics (BarChart3 icon) nav buttons above Settings.
This commit is contained in:
78
apps/coder/src/routes/analytics.ts
Normal file
78
apps/coder/src/routes/analytics.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Sql } from '../db.js';
|
||||
|
||||
// token-analyzer-ui: aggregate token/cost analytics across all agent_sessions.
|
||||
// v1 — global view only (no per-project or per-user filtering).
|
||||
|
||||
export interface AnalyticsSummary {
|
||||
total_input_tokens: number;
|
||||
total_output_tokens: number;
|
||||
total_cost: number;
|
||||
session_count: number;
|
||||
}
|
||||
|
||||
export interface SessionAnalyticsRow {
|
||||
session_id: string;
|
||||
session_name: string;
|
||||
total_input_tokens: number;
|
||||
total_output_tokens: number;
|
||||
total_cost: number;
|
||||
last_active_at: string | null;
|
||||
}
|
||||
|
||||
export interface TokenBreakdownAgg {
|
||||
category: string;
|
||||
total_tokens: number;
|
||||
}
|
||||
|
||||
export function registerAnalyticsRoutes(app: FastifyInstance, sql: Sql): void {
|
||||
// GET /api/analytics/summary — aggregate totals across all agent_sessions.
|
||||
app.get('/api/analytics/summary', async () => {
|
||||
const [row] = await sql<AnalyticsSummary[]>`
|
||||
SELECT
|
||||
COALESCE(SUM(a.input_tokens), 0)::BIGINT AS total_input_tokens,
|
||||
COALESCE(SUM(a.output_tokens), 0)::BIGINT AS total_output_tokens,
|
||||
COALESCE(SUM(a.cost), 0)::DOUBLE PRECISION AS total_cost,
|
||||
COUNT(DISTINCT c.session_id)::INT AS session_count
|
||||
FROM agent_sessions a
|
||||
JOIN chats c ON c.id = a.chat_id
|
||||
`;
|
||||
return row ?? { total_input_tokens: 0, total_output_tokens: 0, total_cost: 0, session_count: 0 };
|
||||
});
|
||||
|
||||
// GET /api/analytics/sessions — per-session token/cost breakdown.
|
||||
app.get('/api/analytics/sessions', async () => {
|
||||
const rows = await sql<SessionAnalyticsRow[]>`
|
||||
SELECT
|
||||
c.session_id AS session_id,
|
||||
s.name AS session_name,
|
||||
COALESCE(SUM(a.input_tokens), 0)::BIGINT AS total_input_tokens,
|
||||
COALESCE(SUM(a.output_tokens), 0)::BIGINT AS total_output_tokens,
|
||||
COALESCE(SUM(a.cost), 0)::DOUBLE PRECISION AS total_cost,
|
||||
MAX(a.last_active_at) AS last_active_at
|
||||
FROM agent_sessions a
|
||||
JOIN chats c ON c.id = a.chat_id
|
||||
JOIN sessions s ON s.id = c.session_id
|
||||
GROUP BY c.session_id, s.name
|
||||
ORDER BY MAX(a.last_active_at) DESC NULLS LAST
|
||||
`;
|
||||
return { sessions: rows };
|
||||
});
|
||||
|
||||
// GET /api/analytics/token-breakdown — aggregate token_breakdown categories
|
||||
// across all tasks that carry the JSONB field.
|
||||
app.get('/api/analytics/token-breakdown', async () => {
|
||||
const rows = await sql<{ category: string; total_tokens: number }[]>`
|
||||
SELECT
|
||||
key AS category,
|
||||
SUM((value->>0)::BIGINT)::BIGINT AS total_tokens
|
||||
FROM tasks,
|
||||
LATERAL jsonb_each(token_breakdown)
|
||||
WHERE token_breakdown IS NOT NULL
|
||||
AND jsonb_typeof(token_breakdown) = 'object'
|
||||
GROUP BY key
|
||||
ORDER BY total_tokens DESC
|
||||
`;
|
||||
return { categories: rows };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user