diff --git a/web/src/lib/api.analytics.test.ts b/web/src/lib/api.analytics.test.ts new file mode 100644 index 000000000..78b5c0e4a --- /dev/null +++ b/web/src/lib/api.analytics.test.ts @@ -0,0 +1,74 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { normalizeAnalyticsResponse, type AnalyticsResponse } from "./api.ts"; + +test("normalizeAnalyticsResponse fills in missing skill analytics for older backends", () => { + const raw = { + daily: [], + by_model: [], + totals: { + total_input: 0, + total_output: 0, + total_cache_read: 0, + total_reasoning: 0, + total_estimated_cost: 0, + total_actual_cost: 0, + total_sessions: 0, + total_api_calls: 0, + }, + } as AnalyticsResponse; + + const normalized = normalizeAnalyticsResponse(raw); + + assert.deepEqual(normalized.skills, { + summary: { + total_skill_loads: 0, + total_skill_edits: 0, + total_skill_actions: 0, + distinct_skills_used: 0, + }, + top_skills: [], + }); +}); + +test("normalizeAnalyticsResponse preserves populated skill analytics", () => { + const raw: AnalyticsResponse = { + daily: [], + by_model: [], + totals: { + total_input: 0, + total_output: 0, + total_cache_read: 0, + total_reasoning: 0, + total_estimated_cost: 0, + total_actual_cost: 0, + total_sessions: 0, + total_api_calls: 0, + }, + skills: { + summary: { + total_skill_loads: 2, + total_skill_edits: 1, + total_skill_actions: 3, + distinct_skills_used: 2, + }, + top_skills: [ + { + skill: "systematic-debugging", + view_count: 2, + manage_count: 1, + total_count: 3, + percentage: 100, + last_used_at: 1713900000, + }, + ], + }, + }; + + const normalized = normalizeAnalyticsResponse(raw); + + assert.deepEqual(normalized.skills, raw.skills); +}); diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index b4790f267..5c220c039 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -61,8 +61,8 @@ export const api = { if (params.component && params.component !== "all") qs.set("component", params.component); return fetchJSON(`/api/logs?${qs.toString()}`); }, - getAnalytics: (days: number) => - fetchJSON(`/api/analytics/usage?days=${days}`), + getAnalytics: (days: number): Promise => + fetchJSON(`/api/analytics/usage?days=${days}`).then(normalizeAnalyticsResponse), getConfig: () => fetchJSON>("/api/config"), getDefaults: () => fetchJSON>("/api/config/defaults"), getSchema: () => fetchJSON<{ fields: Record; category_order: string[] }>("/api/config/schema"), @@ -364,10 +364,32 @@ export interface AnalyticsResponse { total_sessions: number; total_api_calls: number; }; + skills?: { + summary?: AnalyticsSkillsSummary; + top_skills?: AnalyticsSkillEntry[]; + }; +} + +export type NormalizedAnalyticsResponse = Omit & { skills: { summary: AnalyticsSkillsSummary; top_skills: AnalyticsSkillEntry[]; }; +}; + +export function normalizeAnalyticsResponse(raw: AnalyticsResponse): NormalizedAnalyticsResponse { + return { + ...raw, + skills: { + summary: { + total_skill_loads: raw.skills?.summary?.total_skill_loads ?? 0, + total_skill_edits: raw.skills?.summary?.total_skill_edits ?? 0, + total_skill_actions: raw.skills?.summary?.total_skill_actions ?? 0, + distinct_skills_used: raw.skills?.summary?.distinct_skills_used ?? 0, + }, + top_skills: Array.isArray(raw.skills?.top_skills) ? raw.skills.top_skills : [], + }, + }; } export interface CronJob { diff --git a/web/src/pages/AnalyticsPage.tsx b/web/src/pages/AnalyticsPage.tsx index ba3061217..02b14ce2c 100644 --- a/web/src/pages/AnalyticsPage.tsx +++ b/web/src/pages/AnalyticsPage.tsx @@ -8,7 +8,12 @@ import { TrendingUp, } from "lucide-react"; import { api } from "@/lib/api"; -import type { AnalyticsResponse, AnalyticsDailyEntry, AnalyticsModelEntry, AnalyticsSkillEntry } from "@/lib/api"; +import type { + AnalyticsDailyEntry, + AnalyticsModelEntry, + AnalyticsSkillEntry, + NormalizedAnalyticsResponse, +} from "@/lib/api"; import { timeAgo } from "@/lib/utils"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; @@ -280,7 +285,7 @@ function SkillTable({ skills }: { skills: AnalyticsSkillEntry[] }) { export default function AnalyticsPage() { const [days, setDays] = useState(30); - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { t } = useI18n();