express-performance-toolkit 1.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 (51) hide show
  1. package/README.md +217 -0
  2. package/dist/cache.d.ts +25 -0
  3. package/dist/cache.d.ts.map +1 -0
  4. package/dist/cache.js +182 -0
  5. package/dist/cache.js.map +1 -0
  6. package/dist/compression.d.ts +7 -0
  7. package/dist/compression.d.ts.map +1 -0
  8. package/dist/compression.js +26 -0
  9. package/dist/compression.js.map +1 -0
  10. package/dist/dashboard/dashboard.html +756 -0
  11. package/dist/dashboard/dashboardRouter.d.ts +9 -0
  12. package/dist/dashboard/dashboardRouter.d.ts.map +1 -0
  13. package/dist/dashboard/dashboardRouter.js +71 -0
  14. package/dist/dashboard/dashboardRouter.js.map +1 -0
  15. package/dist/index.d.ts +45 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +130 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/logger.d.ts +8 -0
  20. package/dist/logger.d.ts.map +1 -0
  21. package/dist/logger.js +70 -0
  22. package/dist/logger.js.map +1 -0
  23. package/dist/queryHelper.d.ts +8 -0
  24. package/dist/queryHelper.d.ts.map +1 -0
  25. package/dist/queryHelper.js +39 -0
  26. package/dist/queryHelper.js.map +1 -0
  27. package/dist/store.d.ts +24 -0
  28. package/dist/store.d.ts.map +1 -0
  29. package/dist/store.js +108 -0
  30. package/dist/store.js.map +1 -0
  31. package/dist/types.d.ts +135 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +3 -0
  34. package/dist/types.js.map +1 -0
  35. package/example/server.ts +126 -0
  36. package/example/tsconfig.json +17 -0
  37. package/jest.config.js +10 -0
  38. package/package.json +57 -0
  39. package/src/cache.ts +228 -0
  40. package/src/compression.ts +25 -0
  41. package/src/dashboard/dashboard.html +756 -0
  42. package/src/dashboard/dashboardRouter.ts +45 -0
  43. package/src/index.ts +141 -0
  44. package/src/logger.ts +83 -0
  45. package/src/queryHelper.ts +49 -0
  46. package/src/store.ts +134 -0
  47. package/src/types.ts +155 -0
  48. package/tests/cache.test.ts +76 -0
  49. package/tests/integration.test.ts +124 -0
  50. package/tests/store.test.ts +103 -0
  51. package/tsconfig.json +21 -0
@@ -0,0 +1,124 @@
1
+ import express from 'express';
2
+ import request from 'supertest';
3
+ import { performanceToolkit } from '../src/index';
4
+
5
+ describe('Integration Tests', () => {
6
+ let app: express.Application;
7
+
8
+ beforeEach(() => {
9
+ app = express();
10
+
11
+ const toolkit = performanceToolkit({
12
+ cache: {
13
+ ttl: 60000,
14
+ exclude: ['/no-cache'],
15
+ },
16
+ compression: false, // disable for easier testing
17
+ logSlowRequests: {
18
+ slowThreshold: 100,
19
+ console: false, // suppress console in tests
20
+ },
21
+ dashboard: true,
22
+ });
23
+
24
+ app.use(toolkit.middleware);
25
+ app.use('/__perf', toolkit.dashboardRouter);
26
+
27
+ app.get('/api/test', (_req, res) => {
28
+ res.json({ message: 'hello' });
29
+ });
30
+
31
+ app.get('/api/slow', (_req, res) => {
32
+ setTimeout(() => {
33
+ res.json({ message: 'slow response' });
34
+ }, 150);
35
+ });
36
+
37
+ app.get('/no-cache', (_req, res) => {
38
+ res.json({ random: Math.random() });
39
+ });
40
+
41
+ app.post('/api/data', express.json(), (req, res) => {
42
+ res.status(201).json({ received: req.body });
43
+ });
44
+ });
45
+
46
+ describe('Cache Middleware', () => {
47
+ it('should cache GET responses', async () => {
48
+ // First request — cache miss
49
+ const res1 = await request(app).get('/api/test');
50
+ expect(res1.status).toBe(200);
51
+ expect(res1.headers['x-cache']).toBe('MISS');
52
+
53
+ // Second request — cache hit
54
+ const res2 = await request(app).get('/api/test');
55
+ expect(res2.status).toBe(200);
56
+ expect(res2.headers['x-cache']).toBe('HIT');
57
+ expect(res2.body).toEqual({ message: 'hello' });
58
+ });
59
+
60
+ it('should not cache POST requests', async () => {
61
+ const res = await request(app)
62
+ .post('/api/data')
63
+ .send({ name: 'test' });
64
+ expect(res.status).toBe(201);
65
+ expect(res.headers['x-cache']).toBeUndefined();
66
+ });
67
+
68
+ it('should respect exclude patterns', async () => {
69
+ const res1 = await request(app).get('/no-cache');
70
+ expect(res1.headers['x-cache']).toBeUndefined();
71
+
72
+ const res2 = await request(app).get('/no-cache');
73
+ expect(res2.headers['x-cache']).toBeUndefined();
74
+ });
75
+ });
76
+
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');
83
+ });
84
+
85
+ it('should serve metrics JSON', async () => {
86
+ // Make some requests first
87
+ await request(app).get('/api/test');
88
+ await request(app).get('/api/test');
89
+
90
+ const res = await request(app).get('/__perf/api/metrics');
91
+ 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');
97
+ expect(res.body.totalRequests).toBeGreaterThanOrEqual(2);
98
+ });
99
+
100
+ it('should reset metrics via POST', async () => {
101
+ await request(app).get('/api/test');
102
+
103
+ const resetRes = await request(app).post('/__perf/api/reset');
104
+ expect(resetRes.status).toBe(200);
105
+ expect(resetRes.body.success).toBe(true);
106
+
107
+ const metricsRes = await request(app).get('/__perf/api/metrics');
108
+ // The GET request to fetch metrics itself gets logged, so we expect at most 1
109
+ expect(metricsRes.body.totalRequests).toBeLessThanOrEqual(1);
110
+ });
111
+ });
112
+
113
+ describe('Slow Request Detection', () => {
114
+ it('should detect slow requests', async () => {
115
+ await request(app).get('/api/slow');
116
+
117
+ // Give on-finished time to fire
118
+ await new Promise((r) => setTimeout(r, 50));
119
+
120
+ const metricsRes = await request(app).get('/__perf/api/metrics');
121
+ expect(metricsRes.body.slowRequests).toBeGreaterThanOrEqual(1);
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,103 @@
1
+ import { MetricsStore } from '../src/store';
2
+ import { LogEntry } from '../src/types';
3
+
4
+ describe('MetricsStore', () => {
5
+ let store: MetricsStore;
6
+
7
+ beforeEach(() => {
8
+ store = new MetricsStore({ maxLogs: 5 });
9
+ });
10
+
11
+ function makeEntry(overrides: Partial<LogEntry> = {}): LogEntry {
12
+ return {
13
+ method: 'GET',
14
+ path: '/api/test',
15
+ statusCode: 200,
16
+ responseTime: 50,
17
+ timestamp: Date.now(),
18
+ slow: false,
19
+ cached: false,
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ it('should add log entries and update aggregate stats', () => {
25
+ store.addLog(makeEntry({ responseTime: 100 }));
26
+ store.addLog(makeEntry({ responseTime: 200 }));
27
+
28
+ const metrics = store.getMetrics();
29
+ expect(metrics.totalRequests).toBe(2);
30
+ expect(metrics.avgResponseTime).toBe(150);
31
+ });
32
+
33
+ it('should enforce ring buffer max size', () => {
34
+ for (let i = 0; i < 10; i++) {
35
+ store.addLog(makeEntry({ responseTime: i * 10 }));
36
+ }
37
+ const metrics = store.getMetrics();
38
+ expect(metrics.totalRequests).toBe(10);
39
+ // Ring buffer should only keep last 5
40
+ expect(metrics.recentLogs.length).toBeLessThanOrEqual(5);
41
+ });
42
+
43
+ it('should track status codes', () => {
44
+ store.addLog(makeEntry({ statusCode: 200 }));
45
+ store.addLog(makeEntry({ statusCode: 200 }));
46
+ store.addLog(makeEntry({ statusCode: 404 }));
47
+ store.addLog(makeEntry({ statusCode: 500 }));
48
+
49
+ const metrics = store.getMetrics();
50
+ expect(metrics.statusCodes[200]).toBe(2);
51
+ expect(metrics.statusCodes[404]).toBe(1);
52
+ expect(metrics.statusCodes[500]).toBe(1);
53
+ });
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 }));
58
+
59
+ const metrics = store.getMetrics();
60
+ const route = metrics.routes['GET /api/users'];
61
+ expect(route).toBeDefined();
62
+ expect(route.count).toBe(2);
63
+ expect(route.avgTime).toBe(150);
64
+ });
65
+
66
+ it('should track slow requests in route stats', () => {
67
+ store.addLog(makeEntry({ path: '/api/slow', slow: true }));
68
+ store.recordSlowRequest();
69
+
70
+ const metrics = store.getMetrics();
71
+ expect(metrics.slowRequests).toBe(1);
72
+ expect(metrics.routes['GET /api/slow'].slowCount).toBe(1);
73
+ });
74
+
75
+ it('should track cache hits and misses', () => {
76
+ store.recordCacheHit();
77
+ store.recordCacheHit();
78
+ store.recordCacheMiss();
79
+
80
+ const metrics = store.getMetrics();
81
+ expect(metrics.cacheHits).toBe(2);
82
+ expect(metrics.cacheMisses).toBe(1);
83
+ expect(metrics.cacheHitRate).toBe(67); // 2/3 = 66.7% → rounds to 67
84
+ });
85
+
86
+ it('should reset all metrics', () => {
87
+ store.addLog(makeEntry());
88
+ store.recordCacheHit();
89
+ store.recordSlowRequest();
90
+ store.reset();
91
+
92
+ const metrics = store.getMetrics();
93
+ expect(metrics.totalRequests).toBe(0);
94
+ expect(metrics.cacheHits).toBe(0);
95
+ expect(metrics.slowRequests).toBe(0);
96
+ expect(metrics.recentLogs.length).toBe(0);
97
+ });
98
+
99
+ it('should calculate cache hit rate as 0 when no cache activity', () => {
100
+ const metrics = store.getMetrics();
101
+ expect(metrics.cacheHitRate).toBe(0);
102
+ });
103
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "moduleResolution": "node",
17
+ "removeComments": false
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist", "tests", "example"]
21
+ }