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.
Files changed (78) hide show
  1. package/README.md +119 -76
  2. package/dashboard-ui/README.md +73 -0
  3. package/dashboard-ui/eslint.config.js +23 -0
  4. package/dashboard-ui/index.html +13 -0
  5. package/dashboard-ui/package-lock.json +3382 -0
  6. package/dashboard-ui/package.json +32 -0
  7. package/dashboard-ui/src/App.css +184 -0
  8. package/dashboard-ui/src/App.tsx +182 -0
  9. package/dashboard-ui/src/components/BlockedModal.tsx +108 -0
  10. package/dashboard-ui/src/components/CachePanel.tsx +45 -0
  11. package/dashboard-ui/src/components/HealthCharts.tsx +142 -0
  12. package/dashboard-ui/src/components/InsightsPanel.tsx +49 -0
  13. package/dashboard-ui/src/components/KpiGrid.tsx +178 -0
  14. package/dashboard-ui/src/components/LiveLogs.tsx +76 -0
  15. package/dashboard-ui/src/components/Login.tsx +83 -0
  16. package/dashboard-ui/src/components/RoutesTable.tsx +110 -0
  17. package/dashboard-ui/src/hooks/useMetrics.ts +131 -0
  18. package/dashboard-ui/src/index.css +652 -0
  19. package/dashboard-ui/src/main.tsx +10 -0
  20. package/dashboard-ui/src/pages/InsightsPage.tsx +42 -0
  21. package/dashboard-ui/src/pages/LogsPage.tsx +26 -0
  22. package/dashboard-ui/src/pages/OverviewPage.tsx +32 -0
  23. package/dashboard-ui/src/pages/RoutesPage.tsx +26 -0
  24. package/dashboard-ui/src/utils/formatters.ts +27 -0
  25. package/dashboard-ui/tsconfig.app.json +28 -0
  26. package/dashboard-ui/tsconfig.json +7 -0
  27. package/dashboard-ui/tsconfig.node.json +26 -0
  28. package/dashboard-ui/vite.config.ts +12 -0
  29. package/dist/analyzer.d.ts +6 -0
  30. package/dist/analyzer.d.ts.map +1 -0
  31. package/dist/analyzer.js +70 -0
  32. package/dist/analyzer.js.map +1 -0
  33. package/dist/dashboard/dashboardRouter.d.ts +4 -4
  34. package/dist/dashboard/dashboardRouter.d.ts.map +1 -1
  35. package/dist/dashboard/dashboardRouter.js +67 -21
  36. package/dist/dashboard/dashboardRouter.js.map +1 -1
  37. package/dist/dashboard-ui/assets/index-CX-zE-Qy.css +1 -0
  38. package/dist/dashboard-ui/assets/index-Q9TGkd8n.js +41 -0
  39. package/dist/dashboard-ui/index.html +14 -0
  40. package/dist/index.d.ts +11 -10
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +35 -11
  43. package/dist/index.js.map +1 -1
  44. package/dist/logger.d.ts +3 -3
  45. package/dist/logger.d.ts.map +1 -1
  46. package/dist/logger.js +167 -9
  47. package/dist/logger.js.map +1 -1
  48. package/dist/queryHelper.d.ts.map +1 -1
  49. package/dist/queryHelper.js +1 -0
  50. package/dist/queryHelper.js.map +1 -1
  51. package/dist/rateLimit.d.ts +5 -0
  52. package/dist/rateLimit.d.ts.map +1 -0
  53. package/dist/rateLimit.js +67 -0
  54. package/dist/rateLimit.js.map +1 -0
  55. package/dist/store.d.ts +9 -2
  56. package/dist/store.d.ts.map +1 -1
  57. package/dist/store.js +147 -25
  58. package/dist/store.js.map +1 -1
  59. package/dist/types.d.ts +93 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/example/server.ts +68 -37
  62. package/package.json +9 -6
  63. package/src/analyzer.ts +78 -0
  64. package/src/dashboard/dashboardRouter.ts +88 -23
  65. package/src/index.ts +70 -30
  66. package/src/logger.ts +177 -13
  67. package/src/queryHelper.ts +2 -0
  68. package/src/rateLimit.ts +86 -0
  69. package/src/store.ts +136 -27
  70. package/src/types.ts +98 -0
  71. package/tests/analyzer.test.ts +108 -0
  72. package/tests/auth.test.ts +79 -0
  73. package/tests/bandwidth.test.ts +72 -0
  74. package/tests/integration.test.ts +51 -54
  75. package/tests/rateLimit.test.ts +57 -0
  76. package/tests/store.test.ts +37 -18
  77. package/tsconfig.json +1 -0
  78. 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 'express';
2
- import request from 'supertest';
3
- import { performanceToolkit } from '../src/index';
1
+ import express from "express";
2
+ import request from "supertest";
3
+ import { performanceToolkit } from "../src/index";
4
4
 
5
- describe('Integration Tests', () => {
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: ['/no-cache'],
14
+ exclude: ["/no-cache"],
15
15
  },
16
- compression: false, // disable for easier testing
16
+ compression: false, // disable for easier testing
17
17
  logSlowRequests: {
18
18
  slowThreshold: 100,
19
- console: false, // suppress console in tests
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('/__perf', toolkit.dashboardRouter);
25
+ app.use("/__perf", toolkit.dashboardRouter);
26
26
 
27
- app.get('/api/test', (_req, res) => {
28
- res.json({ message: 'hello' });
27
+ app.get("/api/test", (_req, res) => {
28
+ res.json({ message: "hello" });
29
29
  });
30
30
 
31
- app.get('/api/slow', (_req, res) => {
31
+ app.get("/api/slow", (_req, res) => {
32
32
  setTimeout(() => {
33
- res.json({ message: 'slow response' });
33
+ res.json({ message: "slow response" });
34
34
  }, 150);
35
35
  });
36
36
 
37
- app.get('/no-cache', (_req, res) => {
37
+ app.get("/no-cache", (_req, res) => {
38
38
  res.json({ random: Math.random() });
39
39
  });
40
40
 
41
- app.post('/api/data', express.json(), (req, res) => {
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('Cache Middleware', () => {
47
- it('should cache GET responses', async () => {
46
+ describe("Cache Middleware", () => {
47
+ it("should cache GET responses", async () => {
48
48
  // First request — cache miss
49
- const res1 = await request(app).get('/api/test');
49
+ const res1 = await request(app).get("/api/test");
50
50
  expect(res1.status).toBe(200);
51
- expect(res1.headers['x-cache']).toBe('MISS');
51
+ expect(res1.headers["x-cache"]).toBe("MISS");
52
52
 
53
53
  // Second request — cache hit
54
- const res2 = await request(app).get('/api/test');
54
+ const res2 = await request(app).get("/api/test");
55
55
  expect(res2.status).toBe(200);
56
- expect(res2.headers['x-cache']).toBe('HIT');
57
- expect(res2.body).toEqual({ message: 'hello' });
56
+ expect(res2.headers["x-cache"]).toBe("HIT");
57
+ expect(res2.body).toEqual({ message: "hello" });
58
58
  });
59
59
 
60
- it('should not cache POST requests', async () => {
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['x-cache']).toBeUndefined();
63
+ expect(res.headers["x-cache"]).toBeUndefined();
66
64
  });
67
65
 
68
- it('should respect exclude patterns', async () => {
69
- const res1 = await request(app).get('/no-cache');
70
- expect(res1.headers['x-cache']).toBeUndefined();
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('/no-cache');
73
- expect(res2.headers['x-cache']).toBeUndefined();
70
+ const res2 = await request(app).get("/no-cache");
71
+ expect(res2.headers["x-cache"]).toBeUndefined();
74
72
  });
75
73
  });
76
74
 
77
- describe('Dashboard', () => {
78
- it('should serve dashboard HTML', async () => {
79
- const res = await request(app).get('/__perf');
80
- expect(res.status).toBe(200);
81
- expect(res.headers['content-type']).toMatch(/html/);
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('should serve metrics JSON', async () => {
82
+ it("should serve metrics JSON", async () => {
86
83
  // Make some requests first
87
- await request(app).get('/api/test');
88
- await request(app).get('/api/test');
84
+ await request(app).get("/api/test");
85
+ await request(app).get("/api/test");
89
86
 
90
- const res = await request(app).get('/__perf/api/metrics');
87
+ const res = await request(app).get("/__perf/api/metrics");
91
88
  expect(res.status).toBe(200);
92
- expect(res.body).toHaveProperty('totalRequests');
93
- expect(res.body).toHaveProperty('avgResponseTime');
94
- expect(res.body).toHaveProperty('slowRequests');
95
- expect(res.body).toHaveProperty('cacheHits');
96
- expect(res.body).toHaveProperty('recentLogs');
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('should reset metrics via POST', async () => {
101
- await request(app).get('/api/test');
97
+ it("should reset metrics via POST", async () => {
98
+ await request(app).get("/api/test");
102
99
 
103
- const resetRes = await request(app).post('/__perf/api/reset');
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('/__perf/api/metrics');
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('Slow Request Detection', () => {
114
- it('should detect slow requests', async () => {
115
- await request(app).get('/api/slow');
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('/__perf/api/metrics');
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
+ });
@@ -1,7 +1,7 @@
1
- import { MetricsStore } from '../src/store';
2
- import { LogEntry } from '../src/types';
1
+ import { MetricsStore } from "../src/store";
2
+ import { LogEntry } from "../src/types";
3
3
 
4
- describe('MetricsStore', () => {
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: 'GET',
14
- path: '/api/test',
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('should add log entries and update aggregate stats', () => {
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('should enforce ring buffer max size', () => {
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('should track status codes', () => {
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('should track per-route stats', () => {
56
- store.addLog(makeEntry({ method: 'GET', path: '/api/users', responseTime: 100 }));
57
- store.addLog(makeEntry({ method: 'GET', path: '/api/users', responseTime: 200 }));
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['GET /api/users'];
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('should track slow requests in route stats', () => {
67
- store.addLog(makeEntry({ path: '/api/slow', slow: true }));
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['GET /api/slow'].slowCount).toBe(1);
76
+ expect(metrics.routes["GET /api/slow"].slowCount).toBe(1);
73
77
  });
74
78
 
75
- it('should track cache hits and misses', () => {
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('should reset all metrics', () => {
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('should calculate cache hit rate as 0 when no cache activity', () => {
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
  });
package/tsconfig.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "composite": true,
3
4
  "target": "ES2020",
4
5
  "module": "commonjs",
5
6
  "lib": ["ES2020"],