From ed580f67b44ba79118cee41450b50ace4862fd49 Mon Sep 17 00:00:00 2001 From: Mitch5000 Date: Mon, 27 Jul 2026 21:37:15 +0100 Subject: [PATCH] Add since timestamp FIXED --- src/openapi.ts | 8 ++++- src/routes/metrics.test.ts | 64 +++++++++++++++++++++++++++++++++++++- src/routes/metrics.ts | 28 +++++++++++++++-- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/openapi.ts b/src/openapi.ts index 559144f..1ca2735 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -176,7 +176,13 @@ export function buildOpenApiSpec(): Record { get: { summary: "Aggregate network metrics" }, }, "/api/v1/metrics/history": { - get: { summary: "Recent aggregate metrics snapshots, oldest first" }, + get: { + summary: "Recent aggregate metrics snapshots, oldest first", + description: + "Returns the buffered metrics history. Pass ?since= " + + "to return only snapshots with timestamp values strictly after that point.", + parameters: ["since"], + }, }, }, }; diff --git a/src/routes/metrics.test.ts b/src/routes/metrics.test.ts index b349a7f..e99b461 100644 --- a/src/routes/metrics.test.ts +++ b/src/routes/metrics.test.ts @@ -60,12 +60,74 @@ describe("metrics route", () => { expect(typeof res.body.snapshots[0].timestamp).toBe("string"); }); + it("returns the full metrics history when since is omitted", async () => { + const app = createApp(); + await seed(app); + + await request(app).get("/api/v1/metrics"); + await request(app).get("/api/v1/metrics"); + await request(app).get("/api/v1/metrics"); + + const res = await request(app).get("/api/v1/metrics/history"); + expect(res.status).toBe(200); + expect(res.body.snapshots).toHaveLength(3); + }); + + it("filters metrics history to snapshots after a valid since timestamp", async () => { + jest.useFakeTimers(); + try { + const app = createApp(); + await seed(app); + + jest.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); + await request(app).get("/api/v1/metrics"); + + jest.setSystemTime(new Date("2026-01-01T00:00:10.000Z")); + await request(app).get("/api/v1/metrics"); + + jest.setSystemTime(new Date("2026-01-01T00:00:20.000Z")); + await request(app).get("/api/v1/metrics"); + + const res = await request(app) + .get("/api/v1/metrics/history") + .query({ since: "2026-01-01T00:00:10.000Z" }); + + expect(res.status).toBe(200); + expect(res.body.snapshots).toHaveLength(1); + expect(res.body.snapshots[0].timestamp).toBe("2026-01-01T00:00:20.000Z"); + } finally { + jest.useRealTimers(); + } + }); + + it("rejects an invalid since timestamp", async () => { + const res = await request(createApp()) + .get("/api/v1/metrics/history") + .query({ since: "not-a-date" }); + + expect(res.status).toBe(400); + expect(res.body.error.message).toBe( + '"since" must be a valid ISO-8601 timestamp', + ); + }); + + it("rejects repeated since timestamp query parameters", async () => { + const res = await request(createApp()).get( + "/api/v1/metrics/history?since=2026-01-01T00:00:00.000Z&since=2026-01-02T00:00:00.000Z", + ); + + expect(res.status).toBe(400); + expect(res.body.error.message).toBe( + '"since" must be a valid ISO-8601 timestamp', + ); + }); + it("records snapshots on a fixed interval when configured", async () => { jest.useFakeTimers(); try { const originalEnv = process.env.METRICS_SNAPSHOT_INTERVAL_MS; process.env.METRICS_SNAPSHOT_INTERVAL_MS = "1000"; - + const app = createApp(); await seed(app); diff --git a/src/routes/metrics.ts b/src/routes/metrics.ts index 5950a85..8514647 100644 --- a/src/routes/metrics.ts +++ b/src/routes/metrics.ts @@ -6,6 +6,7 @@ import { Router, Request, Response } from "express"; import { LiquidityService } from "../services/liquidityService"; import { AnchorService } from "../services/anchorService"; import { SettlementService } from "../services/settlementService"; +import { ApiError } from "../errors/ApiError"; import { BoundedHistory } from "../utils/history"; /** Maximum number of metrics snapshots retained for `GET /history`. */ @@ -71,8 +72,31 @@ export function metricsRouter(deps: { }); // The last (up to) `MAX_HISTORY` metrics snapshots, oldest first. - router.get("/history", (_req: Request, res: Response) => { - res.json({ snapshots: history.all() }); + // When ?since= is provided, only snapshots with a + // timestamp strictly after that point are returned. + router.get("/history", (req: Request, res: Response) => { + const since = req.query.since; + const snapshots = history.all(); + + if (since === undefined) { + res.json({ snapshots }); + return; + } + + if (typeof since !== "string") { + throw ApiError.badRequest('"since" must be a valid ISO-8601 timestamp'); + } + + const sinceTime = new Date(since).getTime(); + if (Number.isNaN(sinceTime)) { + throw ApiError.badRequest('"since" must be a valid ISO-8601 timestamp'); + } + + res.json({ + snapshots: snapshots.filter( + (snapshot) => new Date(snapshot.timestamp).getTime() > sinceTime, + ), + }); }); return router;