feat(admin): time-series trend charts on the metrics tabs (0.87.0)
Turn the dormant timing data in crawl_metrics / page_analysis into "is it
healthy over time" views. New bucketed series queries (GROUP BY date_trunc,
hour|day via a closed Bucket enum so the unit can't be attacker-controlled)
behind GET /v1/admin/{crawler,analysis}/metrics/series, with a shared
SeriesParams/resolve_bucket helper (bad bucket → 400) and migration 0030
indexing page_analysis(analyzed_at) for the analysis scan.
Frontend: a dependency-free SVG TrendChart (line+area, null buckets render as
gaps, empty-state, role=img) embedded above the per-op tables in Crawler and
Analysis → Metrics, driven by each panel's existing window selector with
AbortController-cancelled fetches. A buildSeries() util fills the continuous
bucket axis (throughput 0 for empty buckets, success/duration null) — unit
tested alongside the chart and the series API client.
Closes the Phase-1 observability set (audit log · health checks · trends).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,9 @@ import {
|
||||
getAnalysisMetrics,
|
||||
listAuditLog,
|
||||
getHealth,
|
||||
updateHealthThresholds
|
||||
updateHealthThresholds,
|
||||
getCrawlerMetricsSeries,
|
||||
getAnalysisMetricsSeries
|
||||
} from './admin';
|
||||
|
||||
function ok(body: unknown, status = 200): Response {
|
||||
@@ -532,6 +534,20 @@ describe('admin crawler api client', () => {
|
||||
}
|
||||
};
|
||||
|
||||
it('getCrawlerMetricsSeries GETs the crawler series with days', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(ok({ buckets: [] }));
|
||||
await getCrawlerMetricsSeries(7);
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/admin\/crawler\/metrics\/series\?days=7$/);
|
||||
});
|
||||
|
||||
it('getAnalysisMetricsSeries omits days=0 (all-time)', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(ok({ buckets: [] }));
|
||||
await getAnalysisMetricsSeries(0);
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/admin\/analysis\/metrics\/series$/);
|
||||
});
|
||||
|
||||
it('getHealth GETs /v1/admin/health', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(ok(healthFixture));
|
||||
const r = await getHealth();
|
||||
|
||||
Reference in New Issue
Block a user