Skip to content

Commit 36e36da

Browse files
feat(monitor): add /monitor/usage health check endpoint (#510)
* fix(baseline): add .ts extension to update.worker import Matches the pattern used by xref and caniuse update routes. Without the extension, TypeScript cannot resolve the module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(monitor): add /monitor/usage health check endpoint Returns name, version, uptime, heapUsed, and heapTotal for the W3C monitoring dashboard at w3.org/2020/12/express-monitor. Closes #190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitor): wrap package.json read in try-catch Prevents a startup crash if package.json is absent in a stripped build artifact. Falls back to version "unknown". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(monitor): add Cache-Control, log errors, validate version, add tests Set Cache-Control: no-store to prevent CDN caching stale health data. Log package.json read errors instead of silently swallowing. Validate version is a non-empty string before using it. Add 5 unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 354993c commit 36e36da

4 files changed

Lines changed: 93 additions & 0 deletions

File tree

app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import respecRouter from "./routes/respec/index.js";
1515
import w3cRouter from "./routes/w3c/index.js";
1616
import baselineRouter from "./routes/api/baseline/index.js";
1717
import wellKnownRouter from "./routes/well-known/index.js";
18+
import monitorRouter from "./routes/monitor/index.js";
1819
import docsRouter from "./routes/docs/index.js";
1920

2021
const app = express();
@@ -51,6 +52,7 @@ app.use("/github/:org/:repo", githubRouter);
5152
app.use("/respec", respecRouter);
5253
app.use("/w3c", w3cRouter);
5354
app.use("/.well-known", wellKnownRouter);
55+
app.use("/monitor", monitorRouter);
5456
app.use("/docs", docsRouter);
5557
app.get("/", (_req, res) => res.redirect("/docs/"));
5658

routes/monitor/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Router } from "express";
2+
3+
import usageRoute from "./usage.js";
4+
5+
const monitor = Router();
6+
monitor.get("/usage", usageRoute);
7+
8+
export default monitor;

routes/monitor/usage.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Request, Response } from "express";
2+
import { readFileSync } from "fs";
3+
import path from "path";
4+
5+
import { PROJECT_ROOT } from "../../utils/constants.js";
6+
7+
let version = "unknown";
8+
try {
9+
const pkg = JSON.parse(
10+
readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8"),
11+
);
12+
if (typeof pkg.version === "string" && pkg.version) {
13+
version = pkg.version;
14+
}
15+
} catch (error) {
16+
console.error("Failed to read version from package.json:", error);
17+
}
18+
19+
export default function route(_req: Request, res: Response) {
20+
const { heapUsed, heapTotal } = process.memoryUsage();
21+
res.set("Cache-Control", "no-store");
22+
res.json({
23+
name: "respec.org",
24+
version,
25+
uptime: process.uptime(),
26+
heapUsed,
27+
heapTotal,
28+
});
29+
}

tests/routes/monitor/usage.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { default: route } = await import(
2+
"../../../build/routes/monitor/usage.js"
3+
);
4+
5+
function mockRes() {
6+
const res = {
7+
_status: 200,
8+
_body: undefined,
9+
_headers: {},
10+
status(code) { res._status = code; return res; },
11+
json(body) { res._body = body; return res; },
12+
set(header, value) { res._headers[header] = value; return res; },
13+
};
14+
return res;
15+
}
16+
17+
describe("monitor/usage", () => {
18+
it("returns all required fields with correct types", () => {
19+
const res = mockRes();
20+
route({}, res);
21+
expect(res._body).toBeDefined();
22+
expect(typeof res._body.name).toBe("string");
23+
expect(typeof res._body.version).toBe("string");
24+
expect(typeof res._body.uptime).toBe("number");
25+
expect(typeof res._body.heapUsed).toBe("number");
26+
expect(typeof res._body.heapTotal).toBe("number");
27+
});
28+
29+
it("returns respec.org as the service name", () => {
30+
const res = mockRes();
31+
route({}, res);
32+
expect(res._body.name).toBe("respec.org");
33+
});
34+
35+
it("sets Cache-Control to no-store", () => {
36+
const res = mockRes();
37+
route({}, res);
38+
expect(res._headers["Cache-Control"]).toBe("no-store");
39+
});
40+
41+
it("returns positive uptime", () => {
42+
const res = mockRes();
43+
route({}, res);
44+
expect(res._body.uptime).toBeGreaterThan(0);
45+
});
46+
47+
it("returns positive heap values", () => {
48+
const res = mockRes();
49+
route({}, res);
50+
expect(res._body.heapUsed).toBeGreaterThan(0);
51+
expect(res._body.heapTotal).toBeGreaterThan(0);
52+
expect(res._body.heapTotal).toBeGreaterThanOrEqual(res._body.heapUsed);
53+
});
54+
});

0 commit comments

Comments
 (0)