express-api-stress-tester 2.0.2 → 2.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-api-stress-tester",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "High-performance distributed API stress testing platform for Express.js APIs — simulate up to 10M concurrent virtual users",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -11,7 +11,7 @@
11
11
  "express-api-stress-tester": "src/cli.js"
12
12
  },
13
13
  "scripts": {
14
- "test": "node --experimental-vm-modules node_modules/.bin/jest --forceExit",
14
+ "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --forceExit",
15
15
  "stress": "node src/cli.js"
16
16
  },
17
17
  "keywords": [
package/src/cli.js CHANGED
@@ -48,6 +48,40 @@ function printSummary(summary) {
48
48
  console.log(` Result: ${resultColor(summary.result)}`);
49
49
  console.log(chalk.bold.cyan('═══════════════════════════════════════════'));
50
50
  console.log('');
51
+
52
+ if (summary.statusCodes && typeof summary.statusCodes === 'object') {
53
+ const codes = Object.entries(summary.statusCodes)
54
+ .filter(([, c]) => Number(c) > 0)
55
+ .sort((a, b) => Number(b[1]) - Number(a[1]))
56
+ .map(([code, count]) => `${code}:${count}`)
57
+ .join(' ');
58
+ if (codes) {
59
+ console.log(chalk.bold('Status Codes:'));
60
+ console.log(` ${codes}`);
61
+ console.log('');
62
+ }
63
+ }
64
+
65
+ if (summary.perEndpoint && typeof summary.perEndpoint === 'object' && Object.keys(summary.perEndpoint).length > 0) {
66
+ console.log(chalk.bold('Per-Endpoint Metrics:'));
67
+ const entries = Object.entries(summary.perEndpoint).sort(
68
+ (a, b) => (b[1].requestsPerSec || 0) - (a[1].requestsPerSec || 0),
69
+ );
70
+ for (const [endpoint, m] of entries) {
71
+ const codes = m.statusCodes && typeof m.statusCodes === 'object'
72
+ ? Object.entries(m.statusCodes)
73
+ .filter(([, c]) => Number(c) > 0)
74
+ .sort((x, y) => Number(y[1]) - Number(x[1]))
75
+ .slice(0, 6)
76
+ .map(([code, count]) => `${code}:${count}`)
77
+ .join(' ')
78
+ : '';
79
+ console.log(
80
+ ` ${endpoint} | rps=${m.requestsPerSec ?? 0} avg=${m.avgResponseTime ?? 0}ms p95=${m.p95 ?? 0}ms err=${m.errorRate ?? 0}%${codes ? ` codes=${codes}` : ''}`,
81
+ );
82
+ }
83
+ console.log('');
84
+ }
51
85
  }
52
86
 
53
87
  async function runCommand(configPath, opts) {
@@ -159,12 +159,13 @@ function getEndpointMetrics(endpoint) {
159
159
  maxLatency: -Infinity,
160
160
  responseTimes: [],
161
161
  sampleCount: 0,
162
+ statusCodes: {},
162
163
  };
163
164
  }
164
165
  return perEndpoint[endpoint];
165
166
  }
166
167
 
167
- function recordEndpoint(endpoint, elapsedMs, isError) {
168
+ function recordEndpoint(endpoint, elapsedMs, isError, status) {
168
169
  const metrics = getEndpointMetrics(endpoint);
169
170
  metrics.totalRequests++;
170
171
  metrics.totalResponseTime += elapsedMs;
@@ -175,6 +176,9 @@ function recordEndpoint(endpoint, elapsedMs, isError) {
175
176
  } else {
176
177
  metrics.successCount++;
177
178
  }
179
+ if (status) {
180
+ metrics.statusCodes[status] = (metrics.statusCodes[status] || 0) + 1;
181
+ }
178
182
  metrics.sampleCount++;
179
183
  if (metrics.responseTimes.length < MAX_SAMPLE_SIZE) {
180
184
  metrics.responseTimes.push(elapsedMs);
@@ -203,7 +207,7 @@ function recordRequestMetrics({ endpointKey, elapsedMs, isError, status }) {
203
207
  successCount++;
204
208
  }
205
209
 
206
- recordEndpoint(endpointKey, elapsedMs, isError);
210
+ recordEndpoint(endpointKey, elapsedMs, isError, status);
207
211
  }
208
212
 
209
213
  async function applyHeaderPlugins(headers) {
@@ -13,6 +13,7 @@ export class ApiMetrics {
13
13
  this.maxLatency = -Infinity;
14
14
  this.responseTimes = [];
15
15
  this._sampleCount = 0;
16
+ this.statusCodes = {};
16
17
  }
17
18
 
18
19
  record(responseTimeMs, isError) {
@@ -55,6 +56,14 @@ export class ApiMetrics {
55
56
  if (partial.responseTimes) {
56
57
  this.mergeResponseTimes(partial.responseTimes);
57
58
  }
59
+
60
+ if (partial.statusCodes && typeof partial.statusCodes === 'object') {
61
+ for (const [code, count] of Object.entries(partial.statusCodes)) {
62
+ const n = Number(count) || 0;
63
+ if (!n) continue;
64
+ this.statusCodes[code] = (this.statusCodes[code] || 0) + n;
65
+ }
66
+ }
58
67
  }
59
68
 
60
69
  mergeResponseTimes(times) {
@@ -111,6 +120,7 @@ export class ApiMetrics {
111
120
  maxLatency: maxLat,
112
121
  errorRate: parseFloat(errorRate),
113
122
  successRate: parseFloat(successRate),
123
+ statusCodes: { ...this.statusCodes },
114
124
  };
115
125
  }
116
126
  }
@@ -21,6 +21,9 @@ export class MetricsCollector {
21
21
  this.responseTimes = [];
22
22
  this._sampleCount = 0;
23
23
  this.perEndpoint = new Map();
24
+
25
+ // Best-effort aggregate status codes across all requests
26
+ this.statusCodes = {};
24
27
  }
25
28
 
26
29
  start() {
@@ -73,6 +76,14 @@ export class MetricsCollector {
73
76
  this.mergeResponseTimes(partial.responseTimes);
74
77
  }
75
78
 
79
+ if (partial.statusCodes && typeof partial.statusCodes === 'object') {
80
+ for (const [code, count] of Object.entries(partial.statusCodes)) {
81
+ const n = Number(count) || 0;
82
+ if (!n) continue;
83
+ this.statusCodes[code] = (this.statusCodes[code] || 0) + n;
84
+ }
85
+ }
86
+
76
87
  if (partial.perEndpoint) {
77
88
  for (const [endpoint, metrics] of Object.entries(partial.perEndpoint)) {
78
89
  this._getEndpointMetrics(endpoint).merge(metrics);
@@ -186,6 +197,7 @@ export class MetricsCollector {
186
197
  result: passed ? 'PASSED' : 'FAILED',
187
198
  elapsedSeconds: elapsed.toFixed(1),
188
199
  perEndpoint: this._buildEndpointSummaries(elapsed),
200
+ statusCodes: { ...this.statusCodes },
189
201
  };
190
202
  }
191
203
 
@@ -32,7 +32,7 @@ export function generateHtmlReport(config, summary) {
32
32
  ];
33
33
 
34
34
  const configRows = [
35
- ['API URL', config.url || 'N/A'],
35
+ ['API URL', config.url || config.baseUrl || 'N/A'],
36
36
  ['Method', (config.method || 'GET').toUpperCase()],
37
37
  ['Concurrent Users', config.concurrency || 1],
38
38
  ...(config.duration ? [['Duration', `${config.duration}s`]] : []),
@@ -26,12 +26,29 @@ export function writeReport(config, summary, reportPath) {
26
26
 
27
27
  function buildTxtReport(config, summary) {
28
28
  const divider = '='.repeat(50);
29
+ const apiUrl = config.url || config.baseUrl || 'N/A';
30
+ const method = (() => {
31
+ if (config.method) return String(config.method).toUpperCase();
32
+ if (Array.isArray(config.routes) && config.routes.length === 1 && config.routes[0]?.method) {
33
+ return String(config.routes[0].method).toUpperCase();
34
+ }
35
+ if (Array.isArray(config.routes) && config.routes.length > 1) return 'MULTI';
36
+ return 'GET';
37
+ })();
38
+ const statusCodesLine = summary.statusCodes && typeof summary.statusCodes === 'object'
39
+ ? Object.entries(summary.statusCodes)
40
+ .filter(([, c]) => Number(c) > 0)
41
+ .sort((a, b) => Number(b[1]) - Number(a[1]))
42
+ .slice(0, 8)
43
+ .map(([code, c]) => `${code}:${c}`)
44
+ .join(' ')
45
+ : '';
29
46
  const lines = [
30
47
  divider,
31
48
  ` API Stress Test Report`,
32
49
  divider,
33
- `API URL: ${config.url}`,
34
- `Method: ${(config.method || 'GET').toUpperCase()}`,
50
+ `API URL: ${apiUrl}`,
51
+ `Method: ${method}`,
35
52
  `Concurrent Users: ${config.concurrency || 1}`,
36
53
  `Duration (s): ${summary.elapsedSeconds}`,
37
54
  `Total Requests: ${summary.totalRequests}`,
@@ -45,6 +62,7 @@ function buildTxtReport(config, summary) {
45
62
  `Success Rate: ${summary.successRate}%`,
46
63
  `CPU Usage: ${summary.cpuPercent}%`,
47
64
  `Memory Usage: ${summary.memoryMB}MB`,
65
+ ...(statusCodesLine ? [`Status Codes: ${statusCodesLine}`] : []),
48
66
  `Result: ${summary.result}`,
49
67
  divider,
50
68
  ...(buildEndpointLines(summary.perEndpoint) || []),