This commit is contained in:
Michael Steuer 2026-04-24 15:24:29 -07:00 committed by GitHub
commit 8df14c5a6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 4 deletions

View file

@ -0,0 +1,74 @@
/// <reference types="node" />
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);
});

View file

@ -61,8 +61,8 @@ export const api = {
if (params.component && params.component !== "all") qs.set("component", params.component);
return fetchJSON<LogsResponse>(`/api/logs?${qs.toString()}`);
},
getAnalytics: (days: number) =>
fetchJSON<AnalyticsResponse>(`/api/analytics/usage?days=${days}`),
getAnalytics: (days: number): Promise<NormalizedAnalyticsResponse> =>
fetchJSON<AnalyticsResponse>(`/api/analytics/usage?days=${days}`).then(normalizeAnalyticsResponse),
getConfig: () => fetchJSON<Record<string, unknown>>("/api/config"),
getDefaults: () => fetchJSON<Record<string, unknown>>("/api/config/defaults"),
getSchema: () => fetchJSON<{ fields: Record<string, unknown>; 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<AnalyticsResponse, "skills"> & {
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 {

View file

@ -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<AnalyticsResponse | null>(null);
const [data, setData] = useState<NormalizedAnalyticsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { t } = useI18n();