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.
- package/README.md +217 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +182 -0
- package/dist/cache.js.map +1 -0
- package/dist/compression.d.ts +7 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +26 -0
- package/dist/compression.js.map +1 -0
- package/dist/dashboard/dashboard.html +756 -0
- package/dist/dashboard/dashboardRouter.d.ts +9 -0
- package/dist/dashboard/dashboardRouter.d.ts.map +1 -0
- package/dist/dashboard/dashboardRouter.js +71 -0
- package/dist/dashboard/dashboardRouter.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +130 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +70 -0
- package/dist/logger.js.map +1 -0
- package/dist/queryHelper.d.ts +8 -0
- package/dist/queryHelper.d.ts.map +1 -0
- package/dist/queryHelper.js +39 -0
- package/dist/queryHelper.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +108 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/example/server.ts +126 -0
- package/example/tsconfig.json +17 -0
- package/jest.config.js +10 -0
- package/package.json +57 -0
- package/src/cache.ts +228 -0
- package/src/compression.ts +25 -0
- package/src/dashboard/dashboard.html +756 -0
- package/src/dashboard/dashboardRouter.ts +45 -0
- package/src/index.ts +141 -0
- package/src/logger.ts +83 -0
- package/src/queryHelper.ts +49 -0
- package/src/store.ts +134 -0
- package/src/types.ts +155 -0
- package/tests/cache.test.ts +76 -0
- package/tests/integration.test.ts +124 -0
- package/tests/store.test.ts +103 -0
- 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
|
+
}
|