express-performance-toolkit 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -76
- package/dashboard-ui/README.md +73 -0
- package/dashboard-ui/eslint.config.js +23 -0
- package/dashboard-ui/index.html +13 -0
- package/dashboard-ui/package-lock.json +3382 -0
- package/dashboard-ui/package.json +32 -0
- package/dashboard-ui/src/App.css +184 -0
- package/dashboard-ui/src/App.tsx +182 -0
- package/dashboard-ui/src/components/BlockedModal.tsx +108 -0
- package/dashboard-ui/src/components/CachePanel.tsx +45 -0
- package/dashboard-ui/src/components/HealthCharts.tsx +142 -0
- package/dashboard-ui/src/components/InsightsPanel.tsx +49 -0
- package/dashboard-ui/src/components/KpiGrid.tsx +178 -0
- package/dashboard-ui/src/components/LiveLogs.tsx +76 -0
- package/dashboard-ui/src/components/Login.tsx +83 -0
- package/dashboard-ui/src/components/RoutesTable.tsx +110 -0
- package/dashboard-ui/src/hooks/useMetrics.ts +131 -0
- package/dashboard-ui/src/index.css +652 -0
- package/dashboard-ui/src/main.tsx +10 -0
- package/dashboard-ui/src/pages/InsightsPage.tsx +42 -0
- package/dashboard-ui/src/pages/LogsPage.tsx +26 -0
- package/dashboard-ui/src/pages/OverviewPage.tsx +32 -0
- package/dashboard-ui/src/pages/RoutesPage.tsx +26 -0
- package/dashboard-ui/src/utils/formatters.ts +27 -0
- package/dashboard-ui/tsconfig.app.json +28 -0
- package/dashboard-ui/tsconfig.json +7 -0
- package/dashboard-ui/tsconfig.node.json +26 -0
- package/dashboard-ui/vite.config.ts +12 -0
- package/dist/analyzer.d.ts +6 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +70 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/dashboard/dashboardRouter.d.ts +4 -4
- package/dist/dashboard/dashboardRouter.d.ts.map +1 -1
- package/dist/dashboard/dashboardRouter.js +67 -21
- package/dist/dashboard/dashboardRouter.js.map +1 -1
- package/dist/dashboard-ui/assets/index-CX-zE-Qy.css +1 -0
- package/dist/dashboard-ui/assets/index-Q9TGkd8n.js +41 -0
- package/dist/dashboard-ui/index.html +14 -0
- package/dist/index.d.ts +11 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -11
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +3 -3
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +167 -9
- package/dist/logger.js.map +1 -1
- package/dist/queryHelper.d.ts.map +1 -1
- package/dist/queryHelper.js +1 -0
- package/dist/queryHelper.js.map +1 -1
- package/dist/rateLimit.d.ts +5 -0
- package/dist/rateLimit.d.ts.map +1 -0
- package/dist/rateLimit.js +67 -0
- package/dist/rateLimit.js.map +1 -0
- package/dist/store.d.ts +9 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +147 -25
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -1
- package/example/server.ts +68 -37
- package/package.json +9 -6
- package/src/analyzer.ts +78 -0
- package/src/dashboard/dashboardRouter.ts +88 -23
- package/src/index.ts +70 -30
- package/src/logger.ts +177 -13
- package/src/queryHelper.ts +2 -0
- package/src/rateLimit.ts +86 -0
- package/src/store.ts +136 -27
- package/src/types.ts +98 -0
- package/tests/analyzer.test.ts +108 -0
- package/tests/auth.test.ts +79 -0
- package/tests/bandwidth.test.ts +72 -0
- package/tests/integration.test.ts +51 -54
- package/tests/rateLimit.test.ts +57 -0
- package/tests/store.test.ts +37 -18
- package/tsconfig.json +1 -0
- package/src/dashboard/dashboard.html +0 -756
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import request from "supertest";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { performanceToolkit } from "../src/index";
|
|
4
|
+
|
|
5
|
+
describe("Bandwidth Tracking", () => {
|
|
6
|
+
it("should track response payload size correctly", async () => {
|
|
7
|
+
const app = express();
|
|
8
|
+
const toolkit = performanceToolkit({
|
|
9
|
+
logSlowRequests: { console: false },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
app.use(toolkit.middleware);
|
|
13
|
+
|
|
14
|
+
app.get("/api/small", (req, res) => {
|
|
15
|
+
res.send("Hello"); // 5 bytes
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
app.get("/api/large", (req, res) => {
|
|
19
|
+
res.json({
|
|
20
|
+
message:
|
|
21
|
+
"This is a larger response payload for testing tracking logic.",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 1st request
|
|
26
|
+
await request(app).get("/api/small").expect(200);
|
|
27
|
+
|
|
28
|
+
// 2nd request
|
|
29
|
+
const largePayload = {
|
|
30
|
+
message: "This is a larger response payload for testing tracking logic.",
|
|
31
|
+
};
|
|
32
|
+
const largePayloadStr = JSON.stringify(largePayload);
|
|
33
|
+
await request(app).get("/api/large").expect(200);
|
|
34
|
+
|
|
35
|
+
const metrics = toolkit.store.getMetrics();
|
|
36
|
+
|
|
37
|
+
// Small route: 'Hello' is 5 bytes
|
|
38
|
+
expect(metrics.routes["GET /api/small"].totalBytes).toBe(5);
|
|
39
|
+
|
|
40
|
+
// Large route
|
|
41
|
+
expect(metrics.routes["GET /api/large"].totalBytes).toBe(
|
|
42
|
+
Buffer.byteLength(largePayloadStr),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Global stats
|
|
46
|
+
expect(metrics.totalBytesSent).toBe(5 + Buffer.byteLength(largePayloadStr));
|
|
47
|
+
expect(metrics.avgResponseSize).toBe(
|
|
48
|
+
Math.round((5 + Buffer.byteLength(largePayloadStr)) / 2),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle chunked res.write calls", async () => {
|
|
53
|
+
const app = express();
|
|
54
|
+
const toolkit = performanceToolkit({
|
|
55
|
+
logSlowRequests: { console: false },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.use(toolkit.middleware);
|
|
59
|
+
|
|
60
|
+
app.get("/api/stream", (req, res) => {
|
|
61
|
+
res.write("Part 1");
|
|
62
|
+
res.write("Part 2");
|
|
63
|
+
res.end("Part 3");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await request(app).get("/api/stream").expect(200);
|
|
67
|
+
|
|
68
|
+
const metrics = toolkit.store.getMetrics();
|
|
69
|
+
const expectedSize = Buffer.byteLength("Part 1Part 2Part 3");
|
|
70
|
+
expect(metrics.routes["GET /api/stream"].totalBytes).toBe(expectedSize);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import express from
|
|
2
|
-
import request from
|
|
3
|
-
import { performanceToolkit } from
|
|
1
|
+
import express from "express";
|
|
2
|
+
import request from "supertest";
|
|
3
|
+
import { performanceToolkit } from "../src/index";
|
|
4
4
|
|
|
5
|
-
describe(
|
|
5
|
+
describe("Integration Tests", () => {
|
|
6
6
|
let app: express.Application;
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
@@ -11,113 +11,110 @@ describe('Integration Tests', () => {
|
|
|
11
11
|
const toolkit = performanceToolkit({
|
|
12
12
|
cache: {
|
|
13
13
|
ttl: 60000,
|
|
14
|
-
exclude: [
|
|
14
|
+
exclude: ["/no-cache"],
|
|
15
15
|
},
|
|
16
|
-
compression: false,
|
|
16
|
+
compression: false, // disable for easier testing
|
|
17
17
|
logSlowRequests: {
|
|
18
18
|
slowThreshold: 100,
|
|
19
|
-
console: false,
|
|
19
|
+
console: false, // suppress console in tests
|
|
20
20
|
},
|
|
21
21
|
dashboard: true,
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
app.use(toolkit.middleware);
|
|
25
|
-
app.use(
|
|
25
|
+
app.use("/__perf", toolkit.dashboardRouter);
|
|
26
26
|
|
|
27
|
-
app.get(
|
|
28
|
-
res.json({ message:
|
|
27
|
+
app.get("/api/test", (_req, res) => {
|
|
28
|
+
res.json({ message: "hello" });
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
app.get(
|
|
31
|
+
app.get("/api/slow", (_req, res) => {
|
|
32
32
|
setTimeout(() => {
|
|
33
|
-
res.json({ message:
|
|
33
|
+
res.json({ message: "slow response" });
|
|
34
34
|
}, 150);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
app.get(
|
|
37
|
+
app.get("/no-cache", (_req, res) => {
|
|
38
38
|
res.json({ random: Math.random() });
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
app.post(
|
|
41
|
+
app.post("/api/data", express.json(), (req, res) => {
|
|
42
42
|
res.status(201).json({ received: req.body });
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
describe(
|
|
47
|
-
it(
|
|
46
|
+
describe("Cache Middleware", () => {
|
|
47
|
+
it("should cache GET responses", async () => {
|
|
48
48
|
// First request — cache miss
|
|
49
|
-
const res1 = await request(app).get(
|
|
49
|
+
const res1 = await request(app).get("/api/test");
|
|
50
50
|
expect(res1.status).toBe(200);
|
|
51
|
-
expect(res1.headers[
|
|
51
|
+
expect(res1.headers["x-cache"]).toBe("MISS");
|
|
52
52
|
|
|
53
53
|
// Second request — cache hit
|
|
54
|
-
const res2 = await request(app).get(
|
|
54
|
+
const res2 = await request(app).get("/api/test");
|
|
55
55
|
expect(res2.status).toBe(200);
|
|
56
|
-
expect(res2.headers[
|
|
57
|
-
expect(res2.body).toEqual({ message:
|
|
56
|
+
expect(res2.headers["x-cache"]).toBe("HIT");
|
|
57
|
+
expect(res2.body).toEqual({ message: "hello" });
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it(
|
|
61
|
-
const res = await request(app)
|
|
62
|
-
.post('/api/data')
|
|
63
|
-
.send({ name: 'test' });
|
|
60
|
+
it("should not cache POST requests", async () => {
|
|
61
|
+
const res = await request(app).post("/api/data").send({ name: "test" });
|
|
64
62
|
expect(res.status).toBe(201);
|
|
65
|
-
expect(res.headers[
|
|
63
|
+
expect(res.headers["x-cache"]).toBeUndefined();
|
|
66
64
|
});
|
|
67
65
|
|
|
68
|
-
it(
|
|
69
|
-
const res1 = await request(app).get(
|
|
70
|
-
expect(res1.headers[
|
|
66
|
+
it("should respect exclude patterns", async () => {
|
|
67
|
+
const res1 = await request(app).get("/no-cache");
|
|
68
|
+
expect(res1.headers["x-cache"]).toBeUndefined();
|
|
71
69
|
|
|
72
|
-
const res2 = await request(app).get(
|
|
73
|
-
expect(res2.headers[
|
|
70
|
+
const res2 = await request(app).get("/no-cache");
|
|
71
|
+
expect(res2.headers["x-cache"]).toBeUndefined();
|
|
74
72
|
});
|
|
75
73
|
});
|
|
76
74
|
|
|
77
|
-
describe(
|
|
78
|
-
it(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
expect(
|
|
82
|
-
expect(res.text).toContain('Express Performance Dashboard');
|
|
75
|
+
describe("Dashboard", () => {
|
|
76
|
+
it("should redirect or serve dashboard HTML", async () => {
|
|
77
|
+
// express.static without trailing slash redirects (301) to add the trailing slash
|
|
78
|
+
const res = await request(app).get("/__perf");
|
|
79
|
+
expect([200, 301]).toContain(res.status);
|
|
83
80
|
});
|
|
84
81
|
|
|
85
|
-
it(
|
|
82
|
+
it("should serve metrics JSON", async () => {
|
|
86
83
|
// Make some requests first
|
|
87
|
-
await request(app).get(
|
|
88
|
-
await request(app).get(
|
|
84
|
+
await request(app).get("/api/test");
|
|
85
|
+
await request(app).get("/api/test");
|
|
89
86
|
|
|
90
|
-
const res = await request(app).get(
|
|
87
|
+
const res = await request(app).get("/__perf/api/metrics");
|
|
91
88
|
expect(res.status).toBe(200);
|
|
92
|
-
expect(res.body).toHaveProperty(
|
|
93
|
-
expect(res.body).toHaveProperty(
|
|
94
|
-
expect(res.body).toHaveProperty(
|
|
95
|
-
expect(res.body).toHaveProperty(
|
|
96
|
-
expect(res.body).toHaveProperty(
|
|
89
|
+
expect(res.body).toHaveProperty("totalRequests");
|
|
90
|
+
expect(res.body).toHaveProperty("avgResponseTime");
|
|
91
|
+
expect(res.body).toHaveProperty("slowRequests");
|
|
92
|
+
expect(res.body).toHaveProperty("cacheHits");
|
|
93
|
+
expect(res.body).toHaveProperty("recentLogs");
|
|
97
94
|
expect(res.body.totalRequests).toBeGreaterThanOrEqual(2);
|
|
98
95
|
});
|
|
99
96
|
|
|
100
|
-
it(
|
|
101
|
-
await request(app).get(
|
|
97
|
+
it("should reset metrics via POST", async () => {
|
|
98
|
+
await request(app).get("/api/test");
|
|
102
99
|
|
|
103
|
-
const resetRes = await request(app).post(
|
|
100
|
+
const resetRes = await request(app).post("/__perf/api/reset");
|
|
104
101
|
expect(resetRes.status).toBe(200);
|
|
105
102
|
expect(resetRes.body.success).toBe(true);
|
|
106
103
|
|
|
107
|
-
const metricsRes = await request(app).get(
|
|
104
|
+
const metricsRes = await request(app).get("/__perf/api/metrics");
|
|
108
105
|
// The GET request to fetch metrics itself gets logged, so we expect at most 1
|
|
109
106
|
expect(metricsRes.body.totalRequests).toBeLessThanOrEqual(1);
|
|
110
107
|
});
|
|
111
108
|
});
|
|
112
109
|
|
|
113
|
-
describe(
|
|
114
|
-
it(
|
|
115
|
-
await request(app).get(
|
|
110
|
+
describe("Slow Request Detection", () => {
|
|
111
|
+
it("should detect slow requests", async () => {
|
|
112
|
+
await request(app).get("/api/slow");
|
|
116
113
|
|
|
117
114
|
// Give on-finished time to fire
|
|
118
115
|
await new Promise((r) => setTimeout(r, 50));
|
|
119
116
|
|
|
120
|
-
const metricsRes = await request(app).get(
|
|
117
|
+
const metricsRes = await request(app).get("/__perf/api/metrics");
|
|
121
118
|
expect(metricsRes.body.slowRequests).toBeGreaterThanOrEqual(1);
|
|
122
119
|
});
|
|
123
120
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { performanceToolkit } from '../src/index';
|
|
4
|
+
|
|
5
|
+
describe('Smart Rate Limiter', () => {
|
|
6
|
+
it('should allow requests within the limit', async () => {
|
|
7
|
+
const app = express();
|
|
8
|
+
const toolkit = performanceToolkit({
|
|
9
|
+
rateLimit: {
|
|
10
|
+
windowMs: 1000,
|
|
11
|
+
max: 3,
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.use(toolkit.middleware);
|
|
16
|
+
app.get('/api', (req, res) => { res.send('OK'); });
|
|
17
|
+
|
|
18
|
+
await request(app).get('/api').expect(200);
|
|
19
|
+
await request(app).get('/api').expect(200);
|
|
20
|
+
await request(app).get('/api').expect(200);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should return 429 and increment rateLimitHits on exceeding max', async () => {
|
|
24
|
+
const app = express();
|
|
25
|
+
const toolkit = performanceToolkit({
|
|
26
|
+
rateLimit: {
|
|
27
|
+
windowMs: 5000, // 5 seconds
|
|
28
|
+
max: 2,
|
|
29
|
+
message: 'Rate limit exceeded'
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
app.use(toolkit.middleware);
|
|
34
|
+
app.get('/api', (req, res) => { res.send('OK'); });
|
|
35
|
+
|
|
36
|
+
// 1st request
|
|
37
|
+
await request(app).get('/api').expect(200);
|
|
38
|
+
// 2nd request
|
|
39
|
+
await request(app).get('/api').expect(200);
|
|
40
|
+
// 3rd request (exceeds max of 2)
|
|
41
|
+
const res = await request(app).get('/api').expect(429);
|
|
42
|
+
|
|
43
|
+
expect(res.text).toBe('Rate limit exceeded');
|
|
44
|
+
expect(res.headers['retry-after']).toBeDefined();
|
|
45
|
+
|
|
46
|
+
// Check MetricsStore
|
|
47
|
+
const metrics = toolkit.store.getMetrics();
|
|
48
|
+
expect(metrics.rateLimitHits).toBe(1);
|
|
49
|
+
expect(metrics.routes['GET /api'].rateLimitHits).toBe(1);
|
|
50
|
+
|
|
51
|
+
// Check blocked event details
|
|
52
|
+
expect(metrics.blockedEvents).toHaveLength(1);
|
|
53
|
+
expect(metrics.blockedEvents[0].ip).toBeDefined();
|
|
54
|
+
expect(metrics.blockedEvents[0].path).toBe('/api');
|
|
55
|
+
expect(metrics.blockedEvents[0].method).toBe('GET');
|
|
56
|
+
});
|
|
57
|
+
});
|
package/tests/store.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { MetricsStore } from
|
|
2
|
-
import { LogEntry } from
|
|
1
|
+
import { MetricsStore } from "../src/store";
|
|
2
|
+
import { LogEntry } from "../src/types";
|
|
3
3
|
|
|
4
|
-
describe(
|
|
4
|
+
describe("MetricsStore", () => {
|
|
5
5
|
let store: MetricsStore;
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
@@ -10,8 +10,8 @@ describe('MetricsStore', () => {
|
|
|
10
10
|
|
|
11
11
|
function makeEntry(overrides: Partial<LogEntry> = {}): LogEntry {
|
|
12
12
|
return {
|
|
13
|
-
method:
|
|
14
|
-
path:
|
|
13
|
+
method: "GET",
|
|
14
|
+
path: "/api/test",
|
|
15
15
|
statusCode: 200,
|
|
16
16
|
responseTime: 50,
|
|
17
17
|
timestamp: Date.now(),
|
|
@@ -21,7 +21,7 @@ describe('MetricsStore', () => {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
it(
|
|
24
|
+
it("should add log entries and update aggregate stats", () => {
|
|
25
25
|
store.addLog(makeEntry({ responseTime: 100 }));
|
|
26
26
|
store.addLog(makeEntry({ responseTime: 200 }));
|
|
27
27
|
|
|
@@ -30,7 +30,7 @@ describe('MetricsStore', () => {
|
|
|
30
30
|
expect(metrics.avgResponseTime).toBe(150);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it(
|
|
33
|
+
it("should enforce ring buffer max size", () => {
|
|
34
34
|
for (let i = 0; i < 10; i++) {
|
|
35
35
|
store.addLog(makeEntry({ responseTime: i * 10 }));
|
|
36
36
|
}
|
|
@@ -40,7 +40,7 @@ describe('MetricsStore', () => {
|
|
|
40
40
|
expect(metrics.recentLogs.length).toBeLessThanOrEqual(5);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it(
|
|
43
|
+
it("should track status codes", () => {
|
|
44
44
|
store.addLog(makeEntry({ statusCode: 200 }));
|
|
45
45
|
store.addLog(makeEntry({ statusCode: 200 }));
|
|
46
46
|
store.addLog(makeEntry({ statusCode: 404 }));
|
|
@@ -52,27 +52,31 @@ describe('MetricsStore', () => {
|
|
|
52
52
|
expect(metrics.statusCodes[500]).toBe(1);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it(
|
|
56
|
-
store.addLog(
|
|
57
|
-
|
|
55
|
+
it("should track per-route stats", () => {
|
|
56
|
+
store.addLog(
|
|
57
|
+
makeEntry({ method: "GET", path: "/api/users", responseTime: 100 }),
|
|
58
|
+
);
|
|
59
|
+
store.addLog(
|
|
60
|
+
makeEntry({ method: "GET", path: "/api/users", responseTime: 200 }),
|
|
61
|
+
);
|
|
58
62
|
|
|
59
63
|
const metrics = store.getMetrics();
|
|
60
|
-
const route = metrics.routes[
|
|
64
|
+
const route = metrics.routes["GET /api/users"];
|
|
61
65
|
expect(route).toBeDefined();
|
|
62
66
|
expect(route.count).toBe(2);
|
|
63
67
|
expect(route.avgTime).toBe(150);
|
|
64
68
|
});
|
|
65
69
|
|
|
66
|
-
it(
|
|
67
|
-
store.addLog(makeEntry({ path:
|
|
70
|
+
it("should track slow requests in route stats", () => {
|
|
71
|
+
store.addLog(makeEntry({ path: "/api/slow", slow: true }));
|
|
68
72
|
store.recordSlowRequest();
|
|
69
73
|
|
|
70
74
|
const metrics = store.getMetrics();
|
|
71
75
|
expect(metrics.slowRequests).toBe(1);
|
|
72
|
-
expect(metrics.routes[
|
|
76
|
+
expect(metrics.routes["GET /api/slow"].slowCount).toBe(1);
|
|
73
77
|
});
|
|
74
78
|
|
|
75
|
-
it(
|
|
79
|
+
it("should track cache hits and misses", () => {
|
|
76
80
|
store.recordCacheHit();
|
|
77
81
|
store.recordCacheHit();
|
|
78
82
|
store.recordCacheMiss();
|
|
@@ -83,7 +87,7 @@ describe('MetricsStore', () => {
|
|
|
83
87
|
expect(metrics.cacheHitRate).toBe(67); // 2/3 = 66.7% → rounds to 67
|
|
84
88
|
});
|
|
85
89
|
|
|
86
|
-
it(
|
|
90
|
+
it("should reset all metrics", () => {
|
|
87
91
|
store.addLog(makeEntry());
|
|
88
92
|
store.recordCacheHit();
|
|
89
93
|
store.recordSlowRequest();
|
|
@@ -96,8 +100,23 @@ describe('MetricsStore', () => {
|
|
|
96
100
|
expect(metrics.recentLogs.length).toBe(0);
|
|
97
101
|
});
|
|
98
102
|
|
|
99
|
-
it(
|
|
103
|
+
it("should calculate cache hit rate as 0 when no cache activity", () => {
|
|
100
104
|
const metrics = store.getMetrics();
|
|
101
105
|
expect(metrics.cacheHitRate).toBe(0);
|
|
102
106
|
});
|
|
107
|
+
|
|
108
|
+
it("should track global and per-route high query requests (N+1)", () => {
|
|
109
|
+
store.addLog(makeEntry({ path: "/api/posts", highQueries: true }));
|
|
110
|
+
const metrics = store.getMetrics();
|
|
111
|
+
expect(metrics.highQueryRequests).toBe(1);
|
|
112
|
+
expect(metrics.routes["GET /api/posts"].highQueryCount).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should expose event loop lag and memory usage", () => {
|
|
116
|
+
const metrics = store.getMetrics();
|
|
117
|
+
expect(typeof metrics.eventLoopLag).toBe("number");
|
|
118
|
+
expect(metrics.memoryUsage).toBeDefined();
|
|
119
|
+
expect(typeof metrics.memoryUsage.rss).toBe("number");
|
|
120
|
+
expect(typeof metrics.memoryUsage.heapUsed).toBe("number");
|
|
121
|
+
});
|
|
103
122
|
});
|