@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.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. 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;