@testsmith/perfornium 0.1.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 +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.RESTHandler = void 0;
|
|
40
|
+
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const http = __importStar(require("http"));
|
|
42
|
+
const https = __importStar(require("https"));
|
|
43
|
+
const logger_1 = require("../../utils/logger");
|
|
44
|
+
class RESTHandler {
|
|
45
|
+
constructor(baseURL, defaultHeaders, timeout, debugConfig) {
|
|
46
|
+
this.connectionTimings = new Map();
|
|
47
|
+
this.debugConfig = debugConfig;
|
|
48
|
+
// Create custom HTTP agent with socket timing hooks
|
|
49
|
+
const httpAgent = new http.Agent({
|
|
50
|
+
keepAlive: true,
|
|
51
|
+
keepAliveMsecs: 30000,
|
|
52
|
+
maxSockets: 100,
|
|
53
|
+
maxFreeSockets: 10
|
|
54
|
+
});
|
|
55
|
+
// Override createConnection to capture timing
|
|
56
|
+
const originalHttpCreateConnection = httpAgent.createConnection;
|
|
57
|
+
httpAgent.createConnection = (options, callback) => {
|
|
58
|
+
const connectionStart = Date.now();
|
|
59
|
+
const socket = originalHttpCreateConnection.call(httpAgent, options, callback);
|
|
60
|
+
socket.once('connect', () => {
|
|
61
|
+
const connectTime = Date.now() - connectionStart;
|
|
62
|
+
this.connectionTimings.set(socket, connectTime);
|
|
63
|
+
});
|
|
64
|
+
return socket;
|
|
65
|
+
};
|
|
66
|
+
// Create custom HTTPS agent with socket timing hooks
|
|
67
|
+
const httpsAgent = new https.Agent({
|
|
68
|
+
keepAlive: true,
|
|
69
|
+
keepAliveMsecs: 30000,
|
|
70
|
+
maxSockets: 100,
|
|
71
|
+
maxFreeSockets: 10
|
|
72
|
+
});
|
|
73
|
+
// Override createConnection to capture timing
|
|
74
|
+
const originalHttpsCreateConnection = httpsAgent.createConnection;
|
|
75
|
+
httpsAgent.createConnection = (options, callback) => {
|
|
76
|
+
const connectionStart = Date.now();
|
|
77
|
+
const socket = originalHttpsCreateConnection.call(httpsAgent, options, callback);
|
|
78
|
+
socket.once('connect', () => {
|
|
79
|
+
const connectTime = Date.now() - connectionStart;
|
|
80
|
+
this.connectionTimings.set(socket, connectTime);
|
|
81
|
+
});
|
|
82
|
+
return socket;
|
|
83
|
+
};
|
|
84
|
+
// Create optimized axios instance with connection pooling and timing
|
|
85
|
+
this.axiosInstance = axios_1.default.create({
|
|
86
|
+
baseURL: baseURL?.replace(/\/$/, ''),
|
|
87
|
+
timeout: timeout || 30000,
|
|
88
|
+
headers: {
|
|
89
|
+
'Connection': 'keep-alive',
|
|
90
|
+
'Keep-Alive': 'timeout=30',
|
|
91
|
+
...defaultHeaders
|
|
92
|
+
},
|
|
93
|
+
// Performance optimizations
|
|
94
|
+
maxRedirects: 3,
|
|
95
|
+
validateStatus: () => true,
|
|
96
|
+
decompress: true,
|
|
97
|
+
maxContentLength: 50 * 1024 * 1024, // 50MB max
|
|
98
|
+
maxBodyLength: 10 * 1024 * 1024, // 10MB max body
|
|
99
|
+
httpAgent,
|
|
100
|
+
httpsAgent
|
|
101
|
+
});
|
|
102
|
+
// Add request interceptor to capture timing and size data
|
|
103
|
+
this.axiosInstance.interceptors.request.use((config) => {
|
|
104
|
+
const timingData = {
|
|
105
|
+
startTime: Date.now()
|
|
106
|
+
};
|
|
107
|
+
// Calculate request sizes
|
|
108
|
+
const { requestHeadersSize, requestBodySize } = this.calculateRequestSizes(config);
|
|
109
|
+
timingData.requestHeadersSize = requestHeadersSize;
|
|
110
|
+
timingData.requestBodySize = requestBodySize;
|
|
111
|
+
// Store timing data in config
|
|
112
|
+
config.timingData = timingData;
|
|
113
|
+
// Add socket event listeners to capture connection timing
|
|
114
|
+
const originalBeforeRedirect = config.beforeRedirect;
|
|
115
|
+
config.beforeRedirect = (options, responseDetails) => {
|
|
116
|
+
if (originalBeforeRedirect) {
|
|
117
|
+
originalBeforeRedirect(options, responseDetails);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// Hook into the socket to measure connection time
|
|
121
|
+
const socketCallback = (socket) => {
|
|
122
|
+
if (!timingData.socketAssigned) {
|
|
123
|
+
timingData.socketAssigned = Date.now();
|
|
124
|
+
}
|
|
125
|
+
if (!socket.connecting) {
|
|
126
|
+
// Socket is already connected (reused from pool)
|
|
127
|
+
timingData.connected = Date.now();
|
|
128
|
+
timingData.tcpConnectionTime = 0; // Reused connection
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// New connection being established
|
|
132
|
+
timingData.connectionStarted = Date.now();
|
|
133
|
+
socket.once('connect', () => {
|
|
134
|
+
timingData.connected = Date.now();
|
|
135
|
+
if (timingData.connectionStarted) {
|
|
136
|
+
timingData.tcpConnectionTime = timingData.connected - timingData.connectionStarted;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
socket.once('secureConnect', () => {
|
|
140
|
+
const secureConnected = Date.now();
|
|
141
|
+
if (timingData.connected) {
|
|
142
|
+
timingData.tlsHandshakeTime = secureConnected - timingData.connected;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
// Add socket callback to config
|
|
148
|
+
config.socketCallback = socketCallback;
|
|
149
|
+
return config;
|
|
150
|
+
});
|
|
151
|
+
// Add response interceptor to capture first byte time
|
|
152
|
+
this.axiosInstance.interceptors.response.use((response) => {
|
|
153
|
+
const timingData = response.config.timingData;
|
|
154
|
+
if (timingData && !timingData.firstByteTime) {
|
|
155
|
+
timingData.firstByteTime = Date.now();
|
|
156
|
+
}
|
|
157
|
+
return response;
|
|
158
|
+
}, (error) => {
|
|
159
|
+
// Capture timing even on errors
|
|
160
|
+
if (error.config) {
|
|
161
|
+
const timingData = error.config.timingData;
|
|
162
|
+
if (timingData && !timingData.firstByteTime) {
|
|
163
|
+
timingData.firstByteTime = Date.now();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return Promise.reject(error);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async execute(request, context) {
|
|
170
|
+
const startTime = Date.now();
|
|
171
|
+
const method = request.method.toUpperCase();
|
|
172
|
+
const url = this.buildURL(request.path);
|
|
173
|
+
// Prepare efficient request config
|
|
174
|
+
const axiosConfig = this.prepareRequestConfig(request, url);
|
|
175
|
+
try {
|
|
176
|
+
if (this.debugConfig?.log_level === 'debug') {
|
|
177
|
+
logger_1.logger.debug(`🌐 ${method} ${url}`);
|
|
178
|
+
// Log request details if debug enabled
|
|
179
|
+
this.logRequestDetails(request, url, method);
|
|
180
|
+
}
|
|
181
|
+
const response = await this.axiosInstance.request(axiosConfig);
|
|
182
|
+
const duration = Date.now() - startTime;
|
|
183
|
+
return this.createSuccessResult(response, request, url, method, duration);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const duration = Date.now() - startTime;
|
|
187
|
+
return this.handleError(error, request, url, method, duration);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
prepareRequestConfig(request, url) {
|
|
191
|
+
const config = {
|
|
192
|
+
method: request.method.toUpperCase(),
|
|
193
|
+
url,
|
|
194
|
+
timeout: request.timeout,
|
|
195
|
+
// Add callback to capture socket timing
|
|
196
|
+
onUploadProgress: (progressEvent) => {
|
|
197
|
+
// Socket timing will be captured via request/response interceptors
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
// Start with any manually specified headers - ensure proper type
|
|
201
|
+
const headers = {};
|
|
202
|
+
if (request.headers) {
|
|
203
|
+
Object.assign(headers, request.headers);
|
|
204
|
+
}
|
|
205
|
+
// Handle query parameters
|
|
206
|
+
if ('query' in request && request.query) {
|
|
207
|
+
config.params = request.query;
|
|
208
|
+
}
|
|
209
|
+
// Handle authentication
|
|
210
|
+
if ('auth' in request && request.auth) {
|
|
211
|
+
this.handleAuthentication(config, headers, request.auth);
|
|
212
|
+
}
|
|
213
|
+
// Handle different payload types with automatic Content-Type detection
|
|
214
|
+
if (request.json) {
|
|
215
|
+
// JSON payload - automatically set Content-Type
|
|
216
|
+
config.data = JSON.stringify(request.json);
|
|
217
|
+
if (!this.hasContentTypeHeader(headers)) {
|
|
218
|
+
headers['Content-Type'] = 'application/json';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if ('form' in request && request.form) {
|
|
222
|
+
// URL-encoded form data
|
|
223
|
+
config.data = new URLSearchParams(request.form).toString();
|
|
224
|
+
if (!this.hasContentTypeHeader(headers)) {
|
|
225
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if ('multipart' in request && request.multipart) {
|
|
229
|
+
// Multipart form data
|
|
230
|
+
const FormData = require('form-data');
|
|
231
|
+
const formData = new FormData();
|
|
232
|
+
Object.entries(request.multipart).forEach(([key, value]) => {
|
|
233
|
+
formData.append(key, value);
|
|
234
|
+
});
|
|
235
|
+
config.data = formData;
|
|
236
|
+
// FormData sets its own Content-Type with boundary
|
|
237
|
+
Object.assign(headers, formData.getHeaders());
|
|
238
|
+
}
|
|
239
|
+
else if (request.body) {
|
|
240
|
+
// Body payload - detect type and set appropriate Content-Type
|
|
241
|
+
const { data, contentType } = this.processBodyPayload(request.body);
|
|
242
|
+
config.data = data;
|
|
243
|
+
// Only set Content-Type if not already specified and we detected a type
|
|
244
|
+
if (contentType && !this.hasContentTypeHeader(headers)) {
|
|
245
|
+
headers['Content-Type'] = contentType;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Assign the properly typed headers to config
|
|
249
|
+
config.headers = headers;
|
|
250
|
+
return config;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Handle authentication configuration
|
|
254
|
+
*/
|
|
255
|
+
handleAuthentication(config, headers, auth) {
|
|
256
|
+
switch (auth.type) {
|
|
257
|
+
case 'basic':
|
|
258
|
+
if (auth.username && auth.password) {
|
|
259
|
+
config.auth = {
|
|
260
|
+
username: auth.username,
|
|
261
|
+
password: auth.password
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
case 'bearer':
|
|
266
|
+
if (auth.token) {
|
|
267
|
+
headers['Authorization'] = `Bearer ${auth.token}`;
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case 'digest':
|
|
271
|
+
// Axios handles digest auth with config.auth
|
|
272
|
+
if (auth.username && auth.password) {
|
|
273
|
+
config.auth = {
|
|
274
|
+
username: auth.username,
|
|
275
|
+
password: auth.password
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case 'oauth':
|
|
280
|
+
if (auth.token) {
|
|
281
|
+
headers['Authorization'] = `OAuth ${auth.token}`;
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Check if Content-Type header is already set (case-insensitive)
|
|
288
|
+
*/
|
|
289
|
+
hasContentTypeHeader(headers) {
|
|
290
|
+
if (!headers)
|
|
291
|
+
return false;
|
|
292
|
+
const headerKeys = Object.keys(headers).map(key => key.toLowerCase());
|
|
293
|
+
return headerKeys.includes('content-type');
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Process body payload and detect content type
|
|
297
|
+
*/
|
|
298
|
+
processBodyPayload(body) {
|
|
299
|
+
if (typeof body === 'string') {
|
|
300
|
+
// Detect content type from string content
|
|
301
|
+
const trimmedBody = body.trim();
|
|
302
|
+
// Check if it's JSON
|
|
303
|
+
if (this.isJsonString(trimmedBody)) {
|
|
304
|
+
return {
|
|
305
|
+
data: body,
|
|
306
|
+
contentType: 'application/json'
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Check if it's XML
|
|
310
|
+
if (this.isXmlString(trimmedBody)) {
|
|
311
|
+
return {
|
|
312
|
+
data: body,
|
|
313
|
+
contentType: 'application/xml'
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// Check if it looks like a template that will resolve to JSON/XML
|
|
317
|
+
if (this.isTemplateString(body)) {
|
|
318
|
+
const detectedType = this.detectTemplateContentType(body);
|
|
319
|
+
return {
|
|
320
|
+
data: body,
|
|
321
|
+
contentType: detectedType
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Default to plain text for other string content
|
|
325
|
+
return {
|
|
326
|
+
data: body,
|
|
327
|
+
contentType: 'text/plain'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Non-string body (object, array, etc.) - serialize as JSON
|
|
332
|
+
return {
|
|
333
|
+
data: JSON.stringify(body),
|
|
334
|
+
contentType: 'application/json'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Detect if string is valid JSON
|
|
340
|
+
*/
|
|
341
|
+
isJsonString(str) {
|
|
342
|
+
if (!str.startsWith('{') && !str.startsWith('[')) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
JSON.parse(str);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Detect if string is XML
|
|
355
|
+
*/
|
|
356
|
+
isXmlString(str) {
|
|
357
|
+
return str.startsWith('<?xml') ||
|
|
358
|
+
(str.startsWith('<') && str.includes('>') && str.endsWith('>'));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Check if body contains template expressions
|
|
362
|
+
*/
|
|
363
|
+
isTemplateString(body) {
|
|
364
|
+
return body.includes('{{template:') ||
|
|
365
|
+
(body.includes('{{') && body.includes('}}'));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Detect content type from template file extension or content
|
|
369
|
+
*/
|
|
370
|
+
detectTemplateContentType(templateBody) {
|
|
371
|
+
// Extract template file references
|
|
372
|
+
const templateMatch = templateBody.match(/\{\{template:([^}]+)\}\}/);
|
|
373
|
+
if (templateMatch) {
|
|
374
|
+
const templatePath = templateMatch[1];
|
|
375
|
+
// Detect from file extension
|
|
376
|
+
if (templatePath.endsWith('.json')) {
|
|
377
|
+
return 'application/json';
|
|
378
|
+
}
|
|
379
|
+
if (templatePath.endsWith('.xml')) {
|
|
380
|
+
return 'application/xml';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// If we can't determine from template, don't assume
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
createSuccessResult(response, request, url, method, duration) {
|
|
387
|
+
const isSuccess = response.status >= 200 && response.status < 300;
|
|
388
|
+
// Extract timing data from request config
|
|
389
|
+
const timingData = response.config.timingData || {};
|
|
390
|
+
const startTime = timingData.startTime || Date.now() - duration;
|
|
391
|
+
const firstByteTime = timingData.firstByteTime || Date.now();
|
|
392
|
+
// Calculate timing breakdown
|
|
393
|
+
const latency = firstByteTime - startTime; // Time to first byte (TTFB)
|
|
394
|
+
// Get actual TCP connection time if measured, otherwise estimate
|
|
395
|
+
// For new connections: connectTime = actual measured time
|
|
396
|
+
// For keep-alive reused connections: connectTime = 0
|
|
397
|
+
// If not measured: estimate as latency/3 (rough approximation: DNS + connect + server processing)
|
|
398
|
+
let connectTime = timingData.tcpConnectionTime;
|
|
399
|
+
if (connectTime === undefined) {
|
|
400
|
+
// Estimate: assume latency includes connect time + server processing
|
|
401
|
+
// Typical breakdown: ~30% connect, ~70% server processing
|
|
402
|
+
connectTime = Math.round(latency * 0.3);
|
|
403
|
+
}
|
|
404
|
+
// Calculate response sizes
|
|
405
|
+
const { headersSize, bodySize } = this.calculateResponseSizes(response);
|
|
406
|
+
// Detect data type
|
|
407
|
+
const dataType = this.detectDataType(response);
|
|
408
|
+
const result = {
|
|
409
|
+
success: isSuccess,
|
|
410
|
+
status: response.status,
|
|
411
|
+
status_text: response.statusText,
|
|
412
|
+
data: response.data,
|
|
413
|
+
duration,
|
|
414
|
+
request_url: url,
|
|
415
|
+
request_method: method,
|
|
416
|
+
// JMeter-style timing breakdown
|
|
417
|
+
sample_start: startTime,
|
|
418
|
+
latency, // Time to first byte
|
|
419
|
+
connect_time: connectTime,
|
|
420
|
+
// JMeter-style size breakdown
|
|
421
|
+
sent_bytes: (timingData.requestHeadersSize || 0) + (timingData.requestBodySize || 0),
|
|
422
|
+
headers_size_sent: timingData.requestHeadersSize || 0,
|
|
423
|
+
body_size_sent: timingData.requestBodySize || 0,
|
|
424
|
+
headers_size_received: headersSize,
|
|
425
|
+
body_size_received: bodySize,
|
|
426
|
+
response_size: headersSize + bodySize,
|
|
427
|
+
data_type: dataType,
|
|
428
|
+
};
|
|
429
|
+
// Add detailed info based on debug configuration
|
|
430
|
+
this.addDetailedInfo(result, response, request, isSuccess);
|
|
431
|
+
// Log response details if debug enabled
|
|
432
|
+
if (this.debugConfig?.log_level === 'debug') {
|
|
433
|
+
this.logResponseDetails(response, isSuccess, duration, method, url);
|
|
434
|
+
}
|
|
435
|
+
// Log failures efficiently
|
|
436
|
+
if (!isSuccess) {
|
|
437
|
+
logger_1.logger.warn(`❌ Request failed: ${method} ${url} - Status: ${response.status} ${response.statusText}`);
|
|
438
|
+
}
|
|
439
|
+
// Run checks efficiently
|
|
440
|
+
if (request.checks) {
|
|
441
|
+
this.runChecksOptimized(request.checks, response, duration, result);
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
handleError(error, request, url, method, duration) {
|
|
446
|
+
// Extract timing data from error config if available
|
|
447
|
+
let startTime = Date.now() - duration;
|
|
448
|
+
let requestHeadersSize = 0;
|
|
449
|
+
let requestBodySize = 0;
|
|
450
|
+
let timingData;
|
|
451
|
+
if (axios_1.default.isAxiosError(error) && error.config) {
|
|
452
|
+
timingData = error.config.timingData;
|
|
453
|
+
if (timingData) {
|
|
454
|
+
startTime = timingData.startTime || startTime;
|
|
455
|
+
requestHeadersSize = timingData.requestHeadersSize || 0;
|
|
456
|
+
requestBodySize = timingData.requestBodySize || 0;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const result = {
|
|
460
|
+
success: false,
|
|
461
|
+
duration,
|
|
462
|
+
request_url: url,
|
|
463
|
+
request_method: method,
|
|
464
|
+
response_size: 0,
|
|
465
|
+
// JMeter-style timing
|
|
466
|
+
sample_start: startTime,
|
|
467
|
+
latency: 0,
|
|
468
|
+
connect_time: 0,
|
|
469
|
+
// JMeter-style size breakdown (request sent, but no response)
|
|
470
|
+
sent_bytes: requestHeadersSize + requestBodySize,
|
|
471
|
+
headers_size_sent: requestHeadersSize,
|
|
472
|
+
body_size_sent: requestBodySize,
|
|
473
|
+
headers_size_received: 0,
|
|
474
|
+
body_size_received: 0,
|
|
475
|
+
data_type: '',
|
|
476
|
+
};
|
|
477
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
478
|
+
const axiosError = error;
|
|
479
|
+
if (axiosError.response) {
|
|
480
|
+
// Server error with response
|
|
481
|
+
result.status = axiosError.response.status;
|
|
482
|
+
result.status_text = axiosError.response.statusText;
|
|
483
|
+
result.error = `HTTP ${axiosError.response.status}: ${axiosError.response.statusText}`;
|
|
484
|
+
result.data = axiosError.response.data;
|
|
485
|
+
// Calculate response sizes for error responses
|
|
486
|
+
const { headersSize, bodySize } = this.calculateResponseSizes(axiosError.response);
|
|
487
|
+
result.headers_size_received = headersSize;
|
|
488
|
+
result.body_size_received = bodySize;
|
|
489
|
+
result.response_size = headersSize + bodySize;
|
|
490
|
+
// Detect data type
|
|
491
|
+
result.data_type = this.detectDataType(axiosError.response);
|
|
492
|
+
// Calculate latency if we got a response
|
|
493
|
+
if (timingData) {
|
|
494
|
+
const firstByteTime = timingData.firstByteTime || Date.now();
|
|
495
|
+
result.latency = firstByteTime - startTime;
|
|
496
|
+
}
|
|
497
|
+
// Add error details based on debug configuration
|
|
498
|
+
this.addDetailedInfo(result, axiosError.response, request, false);
|
|
499
|
+
// Log error response details if debug enabled
|
|
500
|
+
if (this.debugConfig?.log_level === 'debug') {
|
|
501
|
+
this.logResponseDetails(axiosError.response, false, duration, method, url);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else if (axiosError.request) {
|
|
505
|
+
// Network error
|
|
506
|
+
result.error = `Network error: ${axiosError.message}`;
|
|
507
|
+
result.error_code = axiosError.code || 'NETWORK_ERROR';
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// Request setup error
|
|
511
|
+
result.error = `Request error: ${axiosError.message}`;
|
|
512
|
+
result.error_code = 'REQUEST_ERROR';
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// Unknown error
|
|
517
|
+
result.error = error.message || 'Unknown error';
|
|
518
|
+
result.error_code = 'UNKNOWN_ERROR';
|
|
519
|
+
}
|
|
520
|
+
// Log error efficiently
|
|
521
|
+
logger_1.logger.warn(`❌ ${result.error_code || 'ERROR'}: ${method} ${url} - ${result.error}`);
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
addDetailedInfo(result, response, request, isSuccess) {
|
|
525
|
+
// Check if we should capture based on failure-only setting
|
|
526
|
+
const shouldCapture = this.shouldCaptureDetails(isSuccess);
|
|
527
|
+
if (!shouldCapture)
|
|
528
|
+
return;
|
|
529
|
+
// Capture response body if enabled
|
|
530
|
+
if (this.debugConfig?.capture_response_body) {
|
|
531
|
+
const responseText = this.getResponseText(response.data);
|
|
532
|
+
result.response_size = Buffer.byteLength(responseText, 'utf8');
|
|
533
|
+
result.response_body = this.truncateIfNeeded(responseText);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// Always calculate response size for metrics
|
|
537
|
+
const responseText = this.getResponseText(response.data);
|
|
538
|
+
result.response_size = Buffer.byteLength(responseText, 'utf8');
|
|
539
|
+
}
|
|
540
|
+
// Capture response headers if enabled
|
|
541
|
+
if (this.debugConfig?.capture_response_headers) {
|
|
542
|
+
result.response_headers = this.flattenHeaders(response.headers);
|
|
543
|
+
}
|
|
544
|
+
// Capture request headers if enabled
|
|
545
|
+
if (this.debugConfig?.capture_request_headers && request.headers) {
|
|
546
|
+
result.request_headers = request.headers;
|
|
547
|
+
}
|
|
548
|
+
// Capture request body if enabled
|
|
549
|
+
if (this.debugConfig?.capture_request_body && request.body) {
|
|
550
|
+
result.request_body = typeof request.body === 'string'
|
|
551
|
+
? request.body
|
|
552
|
+
: JSON.stringify(request.body);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
logRequestDetails(request, url, method) {
|
|
556
|
+
if (!this.debugConfig || this.debugConfig.log_level !== 'debug')
|
|
557
|
+
return;
|
|
558
|
+
logger_1.logger.debug(`📤 Request Details:`);
|
|
559
|
+
logger_1.logger.debug(` Method: ${method}`);
|
|
560
|
+
logger_1.logger.debug(` URL: ${url}`);
|
|
561
|
+
if (this.debugConfig.capture_request_headers && request.headers) {
|
|
562
|
+
logger_1.logger.debug(` Headers:`, JSON.stringify(request.headers, null, 2));
|
|
563
|
+
}
|
|
564
|
+
if (this.debugConfig.capture_request_body && request.body) {
|
|
565
|
+
const bodyStr = typeof request.body === 'string'
|
|
566
|
+
? request.body
|
|
567
|
+
: JSON.stringify(request.body, null, 2);
|
|
568
|
+
logger_1.logger.debug(` Body:`, this.truncateIfNeeded(bodyStr));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
logResponseDetails(response, isSuccess, duration, method, url) {
|
|
572
|
+
if (!this.debugConfig || this.debugConfig.log_level !== 'debug')
|
|
573
|
+
return;
|
|
574
|
+
// Check if we should log based on failure-only setting
|
|
575
|
+
if (!this.shouldCaptureDetails(isSuccess))
|
|
576
|
+
return;
|
|
577
|
+
const statusIcon = isSuccess ? '✅' : '❌';
|
|
578
|
+
logger_1.logger.debug(`📥 Response Details ${statusIcon}:`);
|
|
579
|
+
logger_1.logger.debug(` Status: ${response.status} ${response.statusText}`);
|
|
580
|
+
logger_1.logger.debug(` Duration: ${duration}ms`);
|
|
581
|
+
if (this.debugConfig.capture_response_headers) {
|
|
582
|
+
logger_1.logger.debug(` Headers:`, JSON.stringify(this.flattenHeaders(response.headers), null, 2));
|
|
583
|
+
}
|
|
584
|
+
if (this.debugConfig.capture_response_body) {
|
|
585
|
+
const responseText = this.getResponseText(response.data);
|
|
586
|
+
const truncated = this.truncateIfNeeded(responseText);
|
|
587
|
+
logger_1.logger.debug(` Body (${responseText.length} chars):`, truncated);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
runChecksOptimized(checks, response, duration, result) {
|
|
591
|
+
let hasFailure = false;
|
|
592
|
+
const errors = [];
|
|
593
|
+
for (const check of checks) {
|
|
594
|
+
try {
|
|
595
|
+
let passed = true;
|
|
596
|
+
switch (check.type) {
|
|
597
|
+
case 'status':
|
|
598
|
+
passed = response.status === check.value;
|
|
599
|
+
if (!passed)
|
|
600
|
+
errors.push(`Expected status ${check.value}, got ${response.status}`);
|
|
601
|
+
break;
|
|
602
|
+
case 'response_time':
|
|
603
|
+
const expectedTime = typeof check.value === 'string'
|
|
604
|
+
? parseInt(check.value.replace(/[<>]/g, ''))
|
|
605
|
+
: check.value;
|
|
606
|
+
if (check.operator === 'lt' || check.value.toString().startsWith('<')) {
|
|
607
|
+
passed = duration < expectedTime;
|
|
608
|
+
if (!passed)
|
|
609
|
+
errors.push(`Response time ${duration}ms exceeded ${expectedTime}ms`);
|
|
610
|
+
}
|
|
611
|
+
break;
|
|
612
|
+
case 'json_path':
|
|
613
|
+
const value = this.getJsonPathOptimized(response.data, check.value);
|
|
614
|
+
passed = value !== undefined && value !== null;
|
|
615
|
+
if (!passed)
|
|
616
|
+
errors.push(`JSON path ${check.value} not found`);
|
|
617
|
+
break;
|
|
618
|
+
case 'text_contains':
|
|
619
|
+
// Only stringify when needed
|
|
620
|
+
const text = typeof response.data === 'string'
|
|
621
|
+
? response.data
|
|
622
|
+
: JSON.stringify(response.data);
|
|
623
|
+
passed = text.includes(check.value);
|
|
624
|
+
if (!passed)
|
|
625
|
+
errors.push(`Response does not contain "${check.value}"`);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
if (!passed)
|
|
629
|
+
hasFailure = true;
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
errors.push(`Check error: ${error}`);
|
|
633
|
+
hasFailure = true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (hasFailure) {
|
|
637
|
+
result.success = false;
|
|
638
|
+
result.error = `Checks failed: ${errors.join(', ')}`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Utility methods optimized for performance
|
|
642
|
+
buildURL(path) {
|
|
643
|
+
// Fast path for absolute URLs
|
|
644
|
+
if (path.startsWith('http'))
|
|
645
|
+
return path;
|
|
646
|
+
// Remove leading slash if baseURL is set (axios will handle it)
|
|
647
|
+
return path.startsWith('/') ? path.slice(1) : path;
|
|
648
|
+
}
|
|
649
|
+
getResponseText(data) {
|
|
650
|
+
if (typeof data === 'string')
|
|
651
|
+
return data;
|
|
652
|
+
if (!data)
|
|
653
|
+
return '';
|
|
654
|
+
try {
|
|
655
|
+
return JSON.stringify(data);
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return String(data);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
getJsonPathOptimized(obj, path) {
|
|
662
|
+
if (!obj)
|
|
663
|
+
return undefined;
|
|
664
|
+
const keys = path.replace(/^\$\./, '').split('.');
|
|
665
|
+
let current = obj;
|
|
666
|
+
for (const key of keys) {
|
|
667
|
+
if (current && typeof current === 'object' && key in current) {
|
|
668
|
+
current = current[key];
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return current;
|
|
675
|
+
}
|
|
676
|
+
flattenHeaders(headers) {
|
|
677
|
+
const result = {};
|
|
678
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
679
|
+
result[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value);
|
|
680
|
+
}
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
truncateIfNeeded(text) {
|
|
684
|
+
const maxSize = this.debugConfig?.max_response_body_size || 10000;
|
|
685
|
+
return text.length > maxSize
|
|
686
|
+
? text.substring(0, maxSize) + `...(+${text.length - maxSize} chars)`
|
|
687
|
+
: text;
|
|
688
|
+
}
|
|
689
|
+
shouldCaptureDetails(isSuccess) {
|
|
690
|
+
// If capture_only_failures is true, only capture on failures
|
|
691
|
+
// If capture_only_failures is false, capture everything
|
|
692
|
+
return !this.debugConfig?.capture_only_failures || !isSuccess;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Calculate request header and body sizes
|
|
696
|
+
*/
|
|
697
|
+
calculateRequestSizes(config) {
|
|
698
|
+
let requestHeadersSize = 0;
|
|
699
|
+
let requestBodySize = 0;
|
|
700
|
+
// Calculate headers size
|
|
701
|
+
if (config.headers) {
|
|
702
|
+
const headersString = Object.entries(config.headers)
|
|
703
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
704
|
+
.join('\r\n');
|
|
705
|
+
requestHeadersSize = Buffer.byteLength(headersString + '\r\n\r\n', 'utf8');
|
|
706
|
+
}
|
|
707
|
+
// Calculate body size
|
|
708
|
+
if (config.data) {
|
|
709
|
+
if (typeof config.data === 'string') {
|
|
710
|
+
requestBodySize = Buffer.byteLength(config.data, 'utf8');
|
|
711
|
+
}
|
|
712
|
+
else if (Buffer.isBuffer(config.data)) {
|
|
713
|
+
requestBodySize = config.data.length;
|
|
714
|
+
}
|
|
715
|
+
else if (typeof config.data === 'object') {
|
|
716
|
+
// For FormData or URLSearchParams, approximate the size
|
|
717
|
+
try {
|
|
718
|
+
const dataString = JSON.stringify(config.data);
|
|
719
|
+
requestBodySize = Buffer.byteLength(dataString, 'utf8');
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
requestBodySize = 0;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { requestHeadersSize, requestBodySize };
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Calculate response header and body sizes
|
|
730
|
+
*/
|
|
731
|
+
calculateResponseSizes(response) {
|
|
732
|
+
let headersSize = 0;
|
|
733
|
+
let bodySize = 0;
|
|
734
|
+
// Calculate headers size
|
|
735
|
+
if (response.headers) {
|
|
736
|
+
const headersString = Object.entries(response.headers)
|
|
737
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
738
|
+
.join('\r\n');
|
|
739
|
+
headersSize = Buffer.byteLength(headersString + '\r\n\r\n', 'utf8');
|
|
740
|
+
}
|
|
741
|
+
// Calculate body size
|
|
742
|
+
if (response.data) {
|
|
743
|
+
const responseText = this.getResponseText(response.data);
|
|
744
|
+
bodySize = Buffer.byteLength(responseText, 'utf8');
|
|
745
|
+
}
|
|
746
|
+
return { headersSize, bodySize };
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Detect data type from response
|
|
750
|
+
*/
|
|
751
|
+
detectDataType(response) {
|
|
752
|
+
const contentType = response.headers['content-type'] || '';
|
|
753
|
+
if (contentType.includes('text/') ||
|
|
754
|
+
contentType.includes('application/json') ||
|
|
755
|
+
contentType.includes('application/xml') ||
|
|
756
|
+
contentType.includes('application/javascript')) {
|
|
757
|
+
return 'text';
|
|
758
|
+
}
|
|
759
|
+
else if (contentType.includes('image/') ||
|
|
760
|
+
contentType.includes('application/octet-stream') ||
|
|
761
|
+
contentType.includes('application/pdf')) {
|
|
762
|
+
return 'bin';
|
|
763
|
+
}
|
|
764
|
+
return '';
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Generate JMeter-style thread name
|
|
768
|
+
* Format: "iteration. step_name vu_id-iteration"
|
|
769
|
+
*/
|
|
770
|
+
generateThreadName(context, stepName) {
|
|
771
|
+
const iteration = context.iteration || 1;
|
|
772
|
+
const vuId = context.vu_id;
|
|
773
|
+
return `${iteration}. ${stepName} ${vuId}-${iteration}`;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
exports.RESTHandler = RESTHandler;
|