word-stress 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/.env.example +8 -0
- package/.instructions.md +361 -0
- package/CHANGELOG.md +88 -0
- package/PUBLISHING.md +103 -0
- package/README.md +120 -0
- package/bin/word-stress +29 -0
- package/package.json +51 -0
- package/src/cli/commands.js +57 -0
- package/src/config.js +159 -0
- package/src/formatters/CsvFormatter.js +72 -0
- package/src/formatters/JsonFormatter.js +17 -0
- package/src/formatters/TableFormatter.js +95 -0
- package/src/formatters/factory.js +31 -0
- package/src/http/client.js +111 -0
- package/src/logger.js +59 -0
- package/src/main.js +75 -0
- package/src/metrics/MetricsCollector.js +169 -0
- package/src/test-modes/BurstTestMode.js +68 -0
- package/src/test-modes/SteadyStateTestMode.js +102 -0
- package/src/test-modes/factory.js +28 -0
- package/src/user-agent.js +51 -0
- package/src/utils.js +99 -0
package/src/main.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main application entry point
|
|
3
|
+
* Orchestrates the stress testing workflow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = require('./logger');
|
|
7
|
+
const { buildConfig, validate } = require('./config');
|
|
8
|
+
const { getTestMode } = require('./test-modes/factory');
|
|
9
|
+
const { getFormatter } = require('./formatters/factory');
|
|
10
|
+
const { buildUrl } = require('./utils');
|
|
11
|
+
const { makeRequest } = require('./http/client');
|
|
12
|
+
const MetricsCollector = require('./metrics/MetricsCollector');
|
|
13
|
+
const { resolveUserAgent } = require('./user-agent');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Main application function
|
|
17
|
+
* @param {Object} args - Parsed CLI arguments
|
|
18
|
+
*/
|
|
19
|
+
async function main(args) {
|
|
20
|
+
try {
|
|
21
|
+
// Build and validate configuration
|
|
22
|
+
const config = buildConfig(args);
|
|
23
|
+
validate(config);
|
|
24
|
+
|
|
25
|
+
// Log test configuration
|
|
26
|
+
logger.log(`\nStarting stress test...`);
|
|
27
|
+
const url = buildUrl(config);
|
|
28
|
+
logger.log(`Target: ${url}`);
|
|
29
|
+
|
|
30
|
+
if (config.mode === 'steady-state') {
|
|
31
|
+
logger.log(`Mode: Steady-State`);
|
|
32
|
+
logger.log(`Clients: ${config.clients}`);
|
|
33
|
+
logger.log(`Interval: ${config.interval}ms`);
|
|
34
|
+
logger.log(`Duration: ${config.duration}s`);
|
|
35
|
+
} else if (config.mode === 'burst') {
|
|
36
|
+
logger.log(`Mode: Burst`);
|
|
37
|
+
logger.log(`Simultaneous Requests: ${config.burstClients}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.log(`Method: ${config.method}`);
|
|
41
|
+
logger.log(`Output Format: ${config.output}`);
|
|
42
|
+
logger.log('');
|
|
43
|
+
|
|
44
|
+
// Resolve user agent
|
|
45
|
+
const userAgent = resolveUserAgent(config);
|
|
46
|
+
if (config.customUserAgent) {
|
|
47
|
+
logger.log(`User-Agent: ${config.customUserAgent} (custom)`);
|
|
48
|
+
} else {
|
|
49
|
+
logger.log(`User-Agent: ${config.browser}`);
|
|
50
|
+
}
|
|
51
|
+
logger.log('');
|
|
52
|
+
|
|
53
|
+
// Create instances for test execution
|
|
54
|
+
const testMode = getTestMode(config);
|
|
55
|
+
const metricsCollector = new MetricsCollector();
|
|
56
|
+
const formatter = getFormatter(config.output);
|
|
57
|
+
|
|
58
|
+
// Run the test with user agent
|
|
59
|
+
const results = await testMode.run(
|
|
60
|
+
{ makeRequest: (url, options) => makeRequest(url, { ...options, userAgent }) },
|
|
61
|
+
metricsCollector
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Format and output results
|
|
65
|
+
const output = formatter.format(results);
|
|
66
|
+
logger.log(output);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error(error.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
main,
|
|
75
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector
|
|
3
|
+
* Aggregates and calculates statistics from test requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { calculatePercentile, calculateMedian } = require('../utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MetricsCollector class
|
|
10
|
+
* Collects per-request metrics and calculates aggregates
|
|
11
|
+
*/
|
|
12
|
+
class MetricsCollector {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.requests = [];
|
|
15
|
+
this.startTime = Date.now();
|
|
16
|
+
this.endTime = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Record a single request result
|
|
21
|
+
* @param {Object} result - Request result object
|
|
22
|
+
* @param {number} result.responseTime - Response time in milliseconds
|
|
23
|
+
* @param {number} result.statusCode - HTTP status code
|
|
24
|
+
* @param {number} result.size - Response size in bytes
|
|
25
|
+
* @param {string|null} result.error - Error type (null if successful)
|
|
26
|
+
*/
|
|
27
|
+
recordRequest(result) {
|
|
28
|
+
this.requests.push({
|
|
29
|
+
responseTime: result.responseTime || 0,
|
|
30
|
+
statusCode: result.statusCode || 0,
|
|
31
|
+
size: result.size || 0,
|
|
32
|
+
error: result.error || null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mark test as complete and set end time
|
|
38
|
+
*/
|
|
39
|
+
complete() {
|
|
40
|
+
this.endTime = Date.now();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get test duration in seconds
|
|
45
|
+
* @returns {number} Duration in seconds
|
|
46
|
+
*/
|
|
47
|
+
getDuration() {
|
|
48
|
+
const end = this.endTime || Date.now();
|
|
49
|
+
return (end - this.startTime) / 1000;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get aggregated metrics
|
|
54
|
+
* @returns {Object} Aggregated metrics
|
|
55
|
+
*/
|
|
56
|
+
getAggregateMetrics() {
|
|
57
|
+
if (this.requests.length === 0) {
|
|
58
|
+
return this._getEmptyMetrics();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const responseTimes = this.requests
|
|
62
|
+
.filter(r => !r.error)
|
|
63
|
+
.map(r => r.responseTime)
|
|
64
|
+
.sort((a, b) => a - b);
|
|
65
|
+
|
|
66
|
+
const duration = this.getDuration();
|
|
67
|
+
|
|
68
|
+
// Count status codes
|
|
69
|
+
const statusCounts = {};
|
|
70
|
+
const errorCounts = {};
|
|
71
|
+
|
|
72
|
+
this.requests.forEach(r => {
|
|
73
|
+
if (r.error) {
|
|
74
|
+
errorCounts[r.error] = (errorCounts[r.error] || 0) + 1;
|
|
75
|
+
} else {
|
|
76
|
+
const statusCategory = `${Math.floor(r.statusCode / 100)}xx`;
|
|
77
|
+
statusCounts[statusCategory] = (statusCounts[statusCategory] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Calculate response time percentiles
|
|
82
|
+
const minResponseTime = responseTimes.length > 0 ? responseTimes[0] : 0;
|
|
83
|
+
const maxResponseTime = responseTimes.length > 0 ? responseTimes[responseTimes.length - 1] : 0;
|
|
84
|
+
const avgResponseTime =
|
|
85
|
+
responseTimes.length > 0
|
|
86
|
+
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
|
87
|
+
: 0;
|
|
88
|
+
|
|
89
|
+
// Calculate total data transferred
|
|
90
|
+
const totalDataTransferred = this.requests.reduce((sum, r) => sum + (r.size || 0), 0);
|
|
91
|
+
|
|
92
|
+
// Calculate throughput
|
|
93
|
+
const throughput = duration > 0 ? this.requests.length / duration : 0;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
// Test metadata
|
|
97
|
+
totalRequests: this.requests.length,
|
|
98
|
+
duration: parseFloat(duration.toFixed(2)),
|
|
99
|
+
|
|
100
|
+
// Status code distribution
|
|
101
|
+
statusCodes: {
|
|
102
|
+
'2xx': statusCounts['2xx'] || 0,
|
|
103
|
+
'3xx': statusCounts['3xx'] || 0,
|
|
104
|
+
'4xx': statusCounts['4xx'] || 0,
|
|
105
|
+
'5xx': statusCounts['5xx'] || 0,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Error counts
|
|
109
|
+
errors: errorCounts,
|
|
110
|
+
errorTotal: Object.values(errorCounts).reduce((a, b) => a + b, 0),
|
|
111
|
+
|
|
112
|
+
// Response time statistics
|
|
113
|
+
responseTime: {
|
|
114
|
+
min: parseFloat(minResponseTime.toFixed(2)),
|
|
115
|
+
max: parseFloat(maxResponseTime.toFixed(2)),
|
|
116
|
+
avg: parseFloat(avgResponseTime.toFixed(2)),
|
|
117
|
+
median: parseFloat(calculateMedian(responseTimes).toFixed(2)),
|
|
118
|
+
p95: parseFloat(calculatePercentile(responseTimes, 95).toFixed(2)),
|
|
119
|
+
p99: parseFloat(calculatePercentile(responseTimes, 99).toFixed(2)),
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Throughput metrics
|
|
123
|
+
throughput: parseFloat(throughput.toFixed(2)),
|
|
124
|
+
dataTransferred: totalDataTransferred,
|
|
125
|
+
|
|
126
|
+
// Success rate
|
|
127
|
+
successRate: parseFloat(
|
|
128
|
+
(
|
|
129
|
+
((this.requests.length - Object.values(errorCounts).reduce((a, b) => a + b, 0)) /
|
|
130
|
+
this.requests.length) *
|
|
131
|
+
100
|
|
132
|
+
).toFixed(2)
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get empty metrics object (for when no requests were made)
|
|
139
|
+
* @private
|
|
140
|
+
* @returns {Object}
|
|
141
|
+
*/
|
|
142
|
+
_getEmptyMetrics() {
|
|
143
|
+
return {
|
|
144
|
+
totalRequests: 0,
|
|
145
|
+
duration: 0,
|
|
146
|
+
statusCodes: {
|
|
147
|
+
'2xx': 0,
|
|
148
|
+
'3xx': 0,
|
|
149
|
+
'4xx': 0,
|
|
150
|
+
'5xx': 0,
|
|
151
|
+
},
|
|
152
|
+
errors: {},
|
|
153
|
+
errorTotal: 0,
|
|
154
|
+
responseTime: {
|
|
155
|
+
min: 0,
|
|
156
|
+
max: 0,
|
|
157
|
+
avg: 0,
|
|
158
|
+
median: 0,
|
|
159
|
+
p95: 0,
|
|
160
|
+
p99: 0,
|
|
161
|
+
},
|
|
162
|
+
throughput: 0,
|
|
163
|
+
dataTransferred: 0,
|
|
164
|
+
successRate: 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = MetricsCollector;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Burst Test Mode
|
|
3
|
+
* Sends simultaneous requests to measure peak capacity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = require('../logger');
|
|
7
|
+
const { buildUrl } = require('../utils');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BurstTestMode class
|
|
11
|
+
* Launches N simultaneous requests and measures aggregate performance
|
|
12
|
+
*/
|
|
13
|
+
class BurstTestMode {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run the burst test
|
|
20
|
+
* Launches burstClients simultaneous requests
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} client - HTTP client with makeRequest() function
|
|
23
|
+
* @param {Object} metricsCollector - MetricsCollector instance
|
|
24
|
+
* @returns {Promise<Object>} Test results (MetricsCollector summary)
|
|
25
|
+
*/
|
|
26
|
+
async run(client, metricsCollector) {
|
|
27
|
+
const { burstClients, method, timeout, followRedirects } = this.config;
|
|
28
|
+
const url = buildUrl(this.config);
|
|
29
|
+
|
|
30
|
+
logger.log(`Starting burst test: ${burstClients} simultaneous requests`);
|
|
31
|
+
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
|
|
34
|
+
// Create array of N simultaneous requests
|
|
35
|
+
const requests = [];
|
|
36
|
+
for (let i = 0; i < burstClients; i++) {
|
|
37
|
+
requests.push(
|
|
38
|
+
client.makeRequest(url, {
|
|
39
|
+
method,
|
|
40
|
+
timeout,
|
|
41
|
+
followRedirects,
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Execute all requests in parallel
|
|
47
|
+
try {
|
|
48
|
+
const results = await Promise.all(requests);
|
|
49
|
+
|
|
50
|
+
// Record all results in metrics collector
|
|
51
|
+
results.forEach((result, index) => {
|
|
52
|
+
metricsCollector.recordRequest(result);
|
|
53
|
+
logger.debug(`Burst request ${index + 1}: ${result.statusCode || 'ERROR'}`);
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
logger.error(`Burst test error: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
metricsCollector.complete();
|
|
60
|
+
const elapsed = Date.now() - startTime;
|
|
61
|
+
|
|
62
|
+
logger.log(`Burst test complete after ${(elapsed / 1000).toFixed(2)}s`);
|
|
63
|
+
|
|
64
|
+
return metricsCollector.getAggregateMetrics();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = BurstTestMode;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steady-State Test Mode
|
|
3
|
+
* Runs multiple clients making periodic requests at fixed intervals
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = require('../logger');
|
|
7
|
+
const { sleep, buildUrl } = require('../utils');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SteadyStateTestMode class
|
|
11
|
+
* Creates multiple concurrent clients, each making requests at a fixed interval
|
|
12
|
+
*/
|
|
13
|
+
class SteadyStateTestMode {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run the steady-state test
|
|
20
|
+
* Spawns N clients that each make requests at the specified interval
|
|
21
|
+
* for the specified duration
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} client - HTTP client with makeRequest() function
|
|
24
|
+
* @param {Object} metricsCollector - MetricsCollector instance
|
|
25
|
+
* @returns {Promise<Object>} Test results (MetricsCollector summary)
|
|
26
|
+
*/
|
|
27
|
+
async run(client, metricsCollector) {
|
|
28
|
+
const { clients: numClients, interval, duration } = this.config;
|
|
29
|
+
|
|
30
|
+
const testDurationMs = duration * 1000;
|
|
31
|
+
const url = buildUrl(this.config);
|
|
32
|
+
|
|
33
|
+
logger.log(
|
|
34
|
+
`Starting steady-state test: ${numClients} clients, ${interval}ms interval, ${duration}s duration`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
const clientPromises = [];
|
|
39
|
+
|
|
40
|
+
// Spawn N client tasks
|
|
41
|
+
for (let i = 0; i < numClients; i++) {
|
|
42
|
+
const clientId = i + 1;
|
|
43
|
+
clientPromises.push(this._runClient(clientId, client, metricsCollector, url, testDurationMs));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Wait for all clients to finish
|
|
47
|
+
try {
|
|
48
|
+
await Promise.all(clientPromises);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.error(`Test error: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
metricsCollector.complete();
|
|
54
|
+
const elapsed = Date.now() - startTime;
|
|
55
|
+
|
|
56
|
+
logger.log(`Steady-state test complete after ${(elapsed / 1000).toFixed(2)}s`);
|
|
57
|
+
|
|
58
|
+
return metricsCollector.getAggregateMetrics();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run a single client: make requests at the specified interval until duration is reached
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
async _runClient(clientId, client, metricsCollector, url, testDurationMs) {
|
|
66
|
+
const { interval, method, timeout, followRedirects } = this.config;
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
let requestCount = 0;
|
|
69
|
+
|
|
70
|
+
while (Date.now() - startTime < testDurationMs) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await client.makeRequest(url, {
|
|
73
|
+
method,
|
|
74
|
+
timeout,
|
|
75
|
+
followRedirects,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
metricsCollector.recordRequest(result);
|
|
79
|
+
requestCount++;
|
|
80
|
+
|
|
81
|
+
logger.debug(
|
|
82
|
+
`Client ${clientId}: Request ${requestCount} - ${result.statusCode || 'ERROR'}`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Wait for the interval before next request
|
|
86
|
+
const elapsedSinceStart = Date.now() - startTime;
|
|
87
|
+
const nextRequestTime = (requestCount + 1) * interval;
|
|
88
|
+
|
|
89
|
+
if (nextRequestTime > elapsedSinceStart && nextRequestTime <= testDurationMs) {
|
|
90
|
+
const waitTime = nextRequestTime - elapsedSinceStart;
|
|
91
|
+
await sleep(waitTime);
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
logger.debug(`Client ${clientId}: Request error - ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.debug(`Client ${clientId} completed ${requestCount} requests`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = SteadyStateTestMode;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Mode Factory
|
|
3
|
+
* Instantiates the appropriate test mode based on configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SteadyStateTestMode = require('./SteadyStateTestMode');
|
|
7
|
+
const BurstTestMode = require('./BurstTestMode');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get test mode instance
|
|
11
|
+
* @param {Object} config - Configuration object
|
|
12
|
+
* @returns {Object} Test mode instance with run() method
|
|
13
|
+
* @throws {Error} if mode is not recognized
|
|
14
|
+
*/
|
|
15
|
+
function getTestMode(config) {
|
|
16
|
+
switch (config.mode.toLowerCase()) {
|
|
17
|
+
case 'steady-state':
|
|
18
|
+
return new SteadyStateTestMode(config);
|
|
19
|
+
case 'burst':
|
|
20
|
+
return new BurstTestMode(config);
|
|
21
|
+
default:
|
|
22
|
+
throw new Error(`Unknown test mode: ${config.mode}. Valid modes: steady-state, burst`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
getTestMode,
|
|
28
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Agent resolver
|
|
3
|
+
* Translates browser names to realistic user agent strings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const UserAgent = require('user-agents');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get a user agent string based on browser preference
|
|
10
|
+
*
|
|
11
|
+
* @param {string} browser - Browser name: firefox, chrome, safari, or null for default
|
|
12
|
+
* @returns {string} User agent string
|
|
13
|
+
*/
|
|
14
|
+
function getUserAgent(browser = 'chrome') {
|
|
15
|
+
const ua = new UserAgent();
|
|
16
|
+
|
|
17
|
+
switch (browser) {
|
|
18
|
+
case 'firefox':
|
|
19
|
+
return ua.toString({ ua: 'Firefox' });
|
|
20
|
+
case 'chrome':
|
|
21
|
+
return ua.toString({ ua: 'Chrome' });
|
|
22
|
+
case 'safari':
|
|
23
|
+
return ua.toString({ ua: 'Safari' });
|
|
24
|
+
default:
|
|
25
|
+
return ua.toString();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a custom user agent or default based on browser preference
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} config - Configuration object
|
|
33
|
+
* @param {string} config.customUserAgent - Custom UA string (takes precedence)
|
|
34
|
+
* @param {string} config.browser - Browser name (firefox, chrome, safari)
|
|
35
|
+
* @returns {string} User agent string
|
|
36
|
+
*/
|
|
37
|
+
function resolveUserAgent(config) {
|
|
38
|
+
// Custom UA takes precedence
|
|
39
|
+
if (config.customUserAgent) {
|
|
40
|
+
return config.customUserAgent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use browser preference, default to chrome
|
|
44
|
+
const browser = config.browser || 'chrome';
|
|
45
|
+
return getUserAgent(browser);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
getUserAgent,
|
|
50
|
+
resolveUserAgent,
|
|
51
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the application
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calculate percentile from sorted array
|
|
7
|
+
* @param {number[]} sortedArray - Array of values, sorted in ascending order
|
|
8
|
+
* @param {number} percentile - Percentile to calculate (0-100)
|
|
9
|
+
* @returns {number} Percentile value
|
|
10
|
+
*/
|
|
11
|
+
function calculatePercentile(sortedArray, percentile) {
|
|
12
|
+
if (sortedArray.length === 0) {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
if (percentile === 50) {
|
|
16
|
+
return calculateMedian(sortedArray);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const index = (percentile / 100) * (sortedArray.length - 1);
|
|
20
|
+
const lower = Math.floor(index);
|
|
21
|
+
const upper = Math.ceil(index);
|
|
22
|
+
const weight = index % 1;
|
|
23
|
+
|
|
24
|
+
if (lower === upper) {
|
|
25
|
+
return sortedArray[lower];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calculate median from sorted array
|
|
33
|
+
* @param {number[]} sortedArray - Array of values, sorted in ascending order
|
|
34
|
+
* @returns {number} Median value
|
|
35
|
+
*/
|
|
36
|
+
function calculateMedian(sortedArray) {
|
|
37
|
+
if (sortedArray.length === 0) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
const mid = Math.floor(sortedArray.length / 2);
|
|
41
|
+
if (sortedArray.length % 2 === 1) {
|
|
42
|
+
return sortedArray[mid];
|
|
43
|
+
}
|
|
44
|
+
return (sortedArray[mid - 1] + sortedArray[mid]) / 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build full URL from configuration
|
|
49
|
+
* @param {Object} config - Configuration object
|
|
50
|
+
* @returns {string} Full URL
|
|
51
|
+
*/
|
|
52
|
+
function buildUrl(config) {
|
|
53
|
+
const protocol = config.https ? 'https' : 'http';
|
|
54
|
+
const domain = config.domain.replace(/^(https?:\/\/)/, ''); // Remove protocol if present
|
|
55
|
+
const endpoint = config.endpoint.startsWith('/') ? config.endpoint : `/${config.endpoint}`;
|
|
56
|
+
return `${protocol}://${domain}${endpoint}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format bytes to human-readable format
|
|
61
|
+
* @param {number} bytes - Number of bytes
|
|
62
|
+
* @returns {string} Formatted string
|
|
63
|
+
*/
|
|
64
|
+
function formatBytes(bytes) {
|
|
65
|
+
if (bytes === 0) {
|
|
66
|
+
return '0 B';
|
|
67
|
+
}
|
|
68
|
+
const k = 1024;
|
|
69
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
70
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
71
|
+
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format milliseconds to seconds
|
|
76
|
+
* @param {number} ms - Milliseconds
|
|
77
|
+
* @returns {string} Formatted string
|
|
78
|
+
*/
|
|
79
|
+
function formatDuration(ms) {
|
|
80
|
+
return (ms / 1000).toFixed(2) + 's';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sleep for specified milliseconds
|
|
85
|
+
* @param {number} ms - Milliseconds to sleep
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
calculatePercentile,
|
|
94
|
+
calculateMedian,
|
|
95
|
+
buildUrl,
|
|
96
|
+
formatBytes,
|
|
97
|
+
formatDuration,
|
|
98
|
+
sleep,
|
|
99
|
+
};
|