prix-r9 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Prix-R9 — Pruebas de Estrés HTTP para Node.js
2
+
3
+ Herramienta CLI ligera para pruebas de carga y rendimiento de APIs REST. Permite realizar envíos concurrentes de alto volumen simulando usuarios simultáneos con escalado progresivo (**Ramp-Up**) e inyección de **variables dinámicas** para evitar colisiones de datos en bases de datos.
4
+
5
+ ---
6
+
7
+ ## Instalación
8
+
9
+ ```bash
10
+ npm install -g prix-r9
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Cómo Empezar
16
+
17
+ ```bash
18
+ prix-r9 --config mi-config.json
19
+ ```
20
+
21
+ Toda la configuración (URL, cabeceras, métodos y tiempos) reside dentro del archivo JSON que le pases como parámetro. Esto te permite guardar un historial de todos los servicios estresados.
22
+
23
+ ---
24
+
25
+ ## Importador Automático desde cURL
26
+
27
+ No tienes que escribir el archivo JSON a mano. Si tienes una petición que ya funciona en **Postman, Swagger o en tu Navegador**, simplemente cópiala como formato `cURL` y la herramienta creará el JSON por ti.
28
+
29
+ ```bash
30
+ # 1. Guarda tu comando cURL en un archivo de texto
31
+ # (Click derecho en Network tab del navegador → Copy as cURL)
32
+
33
+ # 2. Convierte a config JSON
34
+ prix-r9-curl -i mi-curl.txt -o casos/mi-endpoint.json
35
+
36
+ # 3. Lanza la prueba
37
+ prix-r9 --config casos/mi-endpoint.json
38
+ ```
39
+
40
+ **¿Qué hace?**
41
+ - Extrae la URL y el Método HTTP automáticamente.
42
+ - Extrae todas las cabeceras (Tokens, ApiKeys).
43
+ - Detecta si es un envío JSON regular (`--data`) o un envío de Archivo pesado (`--form`). Si es archivo, automáticamente separa la ruta física local (`file` / `filekey`) y construye el `body` con los campos extra.
44
+ - Le inyecta un **Ramp-Up predeterminado** (5 a 50 req/s en 5 segundos), listo para que lo afines.
45
+
46
+ ---
47
+
48
+ ## Organización por Casos de Prueba (Recomendado)
49
+
50
+ Para no perder la configuración de un WebService que probaste y que podrías volver a ocupar en meses, guarda los archivos JSON divididos por carpetas:
51
+
52
+ ```text
53
+ mi-proyecto/
54
+ └── casos/
55
+ ├── modulo-usuarios/
56
+ │ └── crear-usuario.json
57
+ ├── modulo-reportes/
58
+ │ └── generar-reporte.json
59
+ └── modulo-archivos/
60
+ └── upload-csv.json
61
+ ```
62
+
63
+ > **IMPORTANTE:** Tú y tu equipo deciden cómo nombrar las carpetas. Pueden ser por Producto, Módulo, Ambiente de QA o Sprint.
64
+
65
+ ---
66
+
67
+ ## Motor de Carga (Rate & Ramp-Up)
68
+
69
+ La herramienta permite moldear cómo las peticiones van entrando a tu API a través del tiempo usando cuatro valores clave:
70
+
71
+ * **`startRate`** (Ej: `5`): Peticiones por segundo al inicio de la prueba.
72
+ * **`targetRate`** (Ej: `50`): Máximo de peticiones por segundo al que se quiere llegar.
73
+ * **`rampUpTime`** (Ej: `5`): Segundos que toma escalar de `startRate` a `targetRate`.
74
+ * **`duration`** (Ej: `10`): Duración total de la prueba en segundos.
75
+
76
+ ### Fórmula de cálculo por segundo
77
+
78
+ ```
79
+ Si s < rampUpTime:
80
+ rate(s) = PISO( startRate + (targetRate - startRate) * (s / rampUpTime) )
81
+
82
+ Si s >= rampUpTime:
83
+ rate(s) = targetRate
84
+ ```
85
+
86
+ **Ejemplo con `startRate: 3, targetRate: 5, rampUpTime: 10, duration: 15`:**
87
+
88
+ | Segundo (s) | Fracción = s ÷ 10 | Incremento = (5-3) × fracción | rate = 3 + incremento | Redondeado |
89
+ |:-----------:|:-----------------:|:-----------------------------:|:---------------------:|:----------:|
90
+ | 1 | 0.1 | 0.2 | 3.2 | **3** |
91
+ | 2 | 0.2 | 0.4 | 3.4 | **3** |
92
+ | 3 | 0.3 | 0.6 | 3.6 | **3** |
93
+ | 4 | 0.4 | 0.8 | 3.8 | **3** |
94
+ | 5 | 0.5 | 1.0 | 4.0 | **4** |
95
+ | 6 | 0.6 | 1.2 | 4.2 | **4** |
96
+ | 7 | 0.7 | 1.4 | 4.4 | **4** |
97
+ | 8 | 0.8 | 1.6 | 4.6 | **4** |
98
+ | 9 | 0.9 | 1.8 | 4.8 | **4** |
99
+ | 10 | 1.0 | 2.0 | 5.0 | **5** |
100
+ | 11 – 15 | — | tope alcanzado | 5 pet/s × 5 seg | **25** |
101
+ | **TOTAL** | | | | **62** |
102
+
103
+ ---
104
+
105
+ ## Variables Dinámicas `{{ }}`
106
+
107
+ Para evitar colisiones por restricciones `UNIQUE` en base de datos, puedes usar variables dinámicas en el `body` y `headers`:
108
+
109
+ - **`{{uuid}}`**: Cadena alfanumérica única (ej: `123e4567-e89b-12d3...`).
110
+ - **`{{timestamp}}`**: Tiempo actual en milisegundos (ej: `1702939129124`).
111
+ - **`{{random_number}}`**: Número aleatorio del 0 al 9999.
112
+
113
+ ---
114
+
115
+ ## Ejemplo 1: Carga JSON
116
+
117
+ ```json
118
+ {
119
+ "url": "https://api.tuempresa.com/v1/usuarios",
120
+ "method": "POST",
121
+ "startRate": 5,
122
+ "targetRate": 50,
123
+ "rampUpTime": 5,
124
+ "duration": 10,
125
+ "headers": {
126
+ "Authorization": "Bearer tu_token",
127
+ "X-Request-ID": "{{uuid}}"
128
+ },
129
+ "body": {
130
+ "id_unico": "{{uuid}}",
131
+ "correo_usuario": "prueba_{{timestamp}}@empresa.com",
132
+ "edad": "{{random_number}}"
133
+ }
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Ejemplo 2: Subida de Archivos (Multipart)
140
+
141
+ ```json
142
+ {
143
+ "url": "https://api.tuempresa.com/v1/upload-foto",
144
+ "method": "POST",
145
+ "startRate": 2,
146
+ "targetRate": 10,
147
+ "rampUpTime": 2,
148
+ "duration": 5,
149
+ "headers": {
150
+ "Authorization": "Bearer tu_token"
151
+ },
152
+ "file": "./foto-pesada.jpg",
153
+ "filekey": "foto",
154
+ "body": {
155
+ "TypeFile": "1",
156
+ "ReasonProcess": "Carga desde V{{random_number}}"
157
+ }
158
+ }
159
+ ```
160
+
161
+ - **`file`:** Ruta del archivo a adjuntar en cada petición.
162
+ - **`filekey`:** Nombre del campo que espera el backend (el `name` del `<input type="file">`).
163
+ - **`body`:** Campos extra que acompañan al archivo en el `FormData`.
164
+
165
+ ---
166
+
167
+ ## Métricas Generadas
168
+
169
+ Al terminar la prueba se imprime un resumen y se guarda en `reporte.txt`:
170
+
171
+ ```text
172
+ ===========================================
173
+ RESUMEN DE LA PRUEBA
174
+ ===========================================
175
+ Peticiones Totales : 410
176
+ Conexiones Máximas : 50
177
+ Rendimiento / Rate : 23.50 Req/s
178
+
179
+ DESGLOSE DE RESULTADOS:
180
+ Exitosas (2xx) : 410
181
+ Errores Cliente (4xx): 0
182
+ Errores Server (5xx): 0
183
+
184
+ TIEMPOS DE RESPUESTA (Latencia en ms):
185
+ Mínimo : 60.88 ms
186
+ Máximo : 165.25 ms
187
+ Promedio: 75.93 ms
188
+ P95 : 88.99 ms
189
+
190
+ CÓDIGOS DE ESTADO HTTP:
191
+ [200] -> 410 veces
192
+ ===========================================
193
+ ```
194
+
195
+ > **P95**: El 95% de las peticiones tardó igual o menos que ese tiempo.
196
+ > **Rendimiento**: Throughput real (Req/s) que el servidor procesó durante toda la prueba.
197
+
198
+ ---
199
+
200
+ ## Template para Informe con IA
201
+
202
+ El paquete incluye el archivo `promptInforme.txt`, un template profesional para generar informes técnicos de rendimiento usando cualquier IA (ChatGPT, Claude, etc.).
203
+
204
+ **Cómo usarlo:**
205
+ 1. Corre uno o varios escenarios de estrés con `prix-r9`
206
+ 2. Copia el contenido de cada `reporte.txt` generado
207
+ 3. Abre `promptInforme.txt` y pega los reportes en las secciones marcadas
208
+ 4. Envía el prompt completo a tu IA preferida
209
+
210
+ El informe generado incluirá: resumen ejecutivo, tabla comparativa, análisis por escenario, diagnóstico del servidor, recomendaciones y semáforo de salud del sistema.
211
+
212
+ El archivo se encuentra en:
213
+ ```
214
+ node_modules/prix-r9/promptInforme.txt
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Referencia Rápida
220
+
221
+ ```bash
222
+ # Instalar globalmente
223
+ npm install -g prix-r9
224
+
225
+ # Convertir cURL a config JSON
226
+ prix-r9-curl -i mi-curl.txt -o casos/endpoint.json
227
+
228
+ # Ejecutar prueba de estrés
229
+ prix-r9 --config casos/endpoint.json
230
+
231
+ # Ver versión
232
+ prix-r9 --version
233
+ ```
package/import-curl.js ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { program } = require('commander');
5
+ const { toJsonString } = require('curlconverter');
6
+
7
+ program
8
+ .version('1.0.0')
9
+ .description('Herramienta de importación de cURL a config.json para Pruebas de Estrés')
10
+ .requiredOption('-i, --input <path>', 'Archivo de texto (.txt) que contiene el comando cURL crudo')
11
+ .requiredOption('-o, --output <path>', 'Ruta de destino para el config JSON de la prueba (ej: casos/az/nuevo-endpoint.json)')
12
+ .parse(process.argv);
13
+
14
+ const options = program.opts();
15
+
16
+ if (!fs.existsSync(options.input)) {
17
+ console.error(`Error: El archivo de entrada "${options.input}" no existe.`);
18
+ process.exit(1);
19
+ }
20
+
21
+ try {
22
+ // 1. Leer RAW cURL
23
+ const curlCommand = fs.readFileSync(options.input, 'utf8').trim();
24
+
25
+ // 2. Usar curlconverter para extraer obj (Devuelve un string JSON equivalente en fetch-obj, o Node.js puro)
26
+ // curlconverter toJsonString genera un JSON representativo del request:
27
+ // { url, method, headers, data }
28
+ const parsedCurlStr = toJsonString(curlCommand);
29
+ if (!parsedCurlStr) {
30
+ throw new Error("El comando cURL no pudo ser parseado.");
31
+ }
32
+
33
+ const parsedCurl = JSON.parse(parsedCurlStr);
34
+
35
+ // 3. Generar la Plantilla de Cargo
36
+ const outputConfig = {
37
+ url: parsedCurl.url,
38
+ method: parsedCurl.method || 'GET',
39
+ startRate: 5,
40
+ targetRate: 50,
41
+ rampUpTime: 5,
42
+ duration: 10,
43
+ headers: parsedCurl.headers || {},
44
+ };
45
+
46
+ // 4. Analizar BODY puro vs Multipart (Form-Data)
47
+ // `curlconverter` lo expone en "data" si es string literal
48
+ // Y lo expone como objeto {data:{}, files:{}} si es Form-Data
49
+
50
+ // Utilidad para limpiar strings literales escapados que trae cURL a veces
51
+ function cleanVal(str) {
52
+ if(typeof str !== 'string') return str;
53
+ let clean = str.replace(/^\\"/, '"').replace(/\\"$/, '"');
54
+ clean = clean.replace(/^"|"$/g, '');
55
+ return clean.replace(/\\/g, '');
56
+ }
57
+
58
+ if (parsedCurl.files && Object.keys(parsedCurl.files).length > 0) {
59
+ // 4A. Es una peticion con Archivo (Multipart)
60
+ const fileKeys = Object.keys(parsedCurl.files);
61
+ const firstFileKey = fileKeys[0];
62
+
63
+ outputConfig.filekey = firstFileKey;
64
+ let filePath = parsedCurl.files[firstFileKey];
65
+
66
+ // Cleanup de path local para Windows (A veces de vuelve /C:/Users)
67
+ if (filePath.match(/^\/[A-Za-z]:\//)) {
68
+ filePath = filePath.substring(1);
69
+ }
70
+ outputConfig.file = filePath;
71
+
72
+ // Tiene campos de texto extra acompañando al form?
73
+ if (parsedCurl.data && Object.keys(parsedCurl.data).length > 0) {
74
+ outputConfig.body = {};
75
+ for (const [key, val] of Object.entries(parsedCurl.data)) {
76
+ outputConfig.body[key] = cleanVal(val);
77
+ }
78
+ }
79
+ } else if (parsedCurl.data) {
80
+ // 4B. Es una peticion normal con body JSON / texto
81
+ if(typeof parsedCurl.data === 'string') {
82
+ try {
83
+ outputConfig.body = JSON.parse(parsedCurl.data);
84
+ } catch(e) {
85
+ outputConfig.body = parsedCurl.data;
86
+ }
87
+ } else {
88
+ outputConfig.body = parsedCurl.data; // Si ya es objeto
89
+ }
90
+ }
91
+
92
+ // 5. Asegurar estructura de carpeta y Escribir
93
+ const destPath = path.resolve(options.output);
94
+ const destDir = path.dirname(destPath);
95
+
96
+ if (!fs.existsSync(destDir)){
97
+ fs.mkdirSync(destDir, { recursive: true });
98
+ }
99
+
100
+ fs.writeFileSync(destPath, JSON.stringify(outputConfig, null, 2), 'utf8');
101
+
102
+ console.log(`✅ ¡Éxito!`);
103
+ console.log(`Tu comando cURL ha sido convertido a una configuración de carga.`);
104
+ console.log(`Archivo generado: ${destPath}`);
105
+ console.log(`\nPuedes ejecutar la prueba ahora usando:`);
106
+ console.log(`node index.js --config ${options.output}`);
107
+
108
+ } catch (err) {
109
+ console.error(`💥 Error al procesar el cURL:`, err.message);
110
+ }
package/index.js ADDED
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env node
2
+ const axios = require('axios');
3
+ const fs = require('fs');
4
+ const FormData = require('form-data');
5
+ const { program } = require('commander');
6
+ const cliProgress = require('cli-progress');
7
+ const { v4: uuidv4 } = require('uuid');
8
+
9
+ // -------------------------------------------------------------
10
+ // Configuración por Archivo JSON
11
+ // -------------------------------------------------------------
12
+ program
13
+ .version('2.0.0')
14
+ .description('Herramienta Avanzada HTTP/REST (Ramp-up y Datos Dinámicos)')
15
+ .requiredOption('-c, --config <path>', 'Ruta a un archivo JSON con toda la configuración (Obligatorio)')
16
+ .parse(process.argv);
17
+
18
+ const cliOpts = program.opts();
19
+
20
+ let options = {};
21
+ try {
22
+ const configData = JSON.parse(fs.readFileSync(cliOpts.config, 'utf8'));
23
+ options = { ...configData };
24
+ } catch (error) {
25
+ console.error(`Error crítico leyendo el archivo de configuración: ${error.message}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ // -------------------------------------------------------------
30
+ // Validaciones Básicas de la Configuración
31
+ // -------------------------------------------------------------
32
+ if (!options.url || !options.method) {
33
+ console.error('Error: Debes proporcionar "url" y "method" en el archivo de configuración JSON.');
34
+ process.exit(1);
35
+ }
36
+
37
+ const method = options.method.toUpperCase();
38
+ if (!['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
39
+ console.error(`Método HTTP no soportado: ${method}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ // Manejar tasas estáticas o Ramp-Up
44
+ const isRampUp = options.rampUpTime && options.targetRate;
45
+ const startRate = isRampUp ? (options.startRate || 1) : (options.rate || 10);
46
+ const targetRate = isRampUp ? options.targetRate : (options.rate || 10);
47
+ const rampUpTime = isRampUp ? options.rampUpTime : 0;
48
+ const duration = options.duration || 10;
49
+
50
+ // Función centralizada para calcular el rate en un segundo dado
51
+ function calculateRate(second) {
52
+ if (!isRampUp) return startRate;
53
+ if (second >= rampUpTime) return targetRate;
54
+ const rateDiff = targetRate - startRate;
55
+ const percentageDone = second / rampUpTime;
56
+ return Math.floor(startRate + (rateDiff * Math.min(percentageDone, 1)));
57
+ }
58
+
59
+ let totalRequestsExpected = 0;
60
+ if (isRampUp) {
61
+ // Sumar iteración exacta segundo a segundo para reflejar la realidad del while loop
62
+ for (let s = 1; s <= duration; s++) {
63
+ totalRequestsExpected += calculateRate(s);
64
+ }
65
+ } else {
66
+ totalRequestsExpected = startRate * duration;
67
+ }
68
+
69
+ let parsedHeaders = options.headers || {};
70
+ let templateBody = options.body ? JSON.stringify(options.body) : null;
71
+ if (options.file && !fs.existsSync(options.file)) {
72
+ console.error(`El archivo especificado no existe: ${options.file}`);
73
+ process.exit(1);
74
+ }
75
+
76
+ // -------------------------------------------------------------
77
+ // Utilidad: Reemplazar variables dinámicas
78
+ // -------------------------------------------------------------
79
+ function parseDynamicVariables(inputStr) {
80
+ if (!inputStr) return inputStr;
81
+ return inputStr
82
+ .replace(/\{\{timestamp\}\}/g, () => Date.now().toString())
83
+ .replace(/\{\{uuid\}\}/g, () => uuidv4())
84
+ .replace(/\{\{random_number\}\}/g, () => Math.floor(Math.random() * 10000).toString());
85
+ }
86
+
87
+ // -------------------------------------------------------------
88
+ // Variables de Métricas
89
+ // -------------------------------------------------------------
90
+ const metrics = {
91
+ total: 0,
92
+ success: 0,
93
+ errors: 0,
94
+ latencies: [],
95
+ statusCodes: {},
96
+ startTime: null,
97
+ endTime: null
98
+ };
99
+
100
+ let activeConnections = 0;
101
+ let maxConcurrentConnections = 0;
102
+
103
+ // Barra de progreso visual
104
+ const progressBar = new cliProgress.SingleBar({
105
+ format: 'Progreso | {bar} | {percentage}% || {value}/{total} Peticiones | Tasa: {throughput} req/s',
106
+ barCompleteChar: '\u2588',
107
+ barIncompleteChar: '\u2591',
108
+ hideCursor: true
109
+ });
110
+
111
+ // -------------------------------------------------------------
112
+ // Función para crear la petición HTTP
113
+ // -------------------------------------------------------------
114
+ async function makeRequest() {
115
+ activeConnections++;
116
+ if (activeConnections > maxConcurrentConnections) {
117
+ maxConcurrentConnections = activeConnections;
118
+ }
119
+
120
+ const startTime = process.hrtime();
121
+ let reqConfig = {
122
+ method: method,
123
+ url: options.url,
124
+ headers: { ...parsedHeaders },
125
+ validateStatus: () => true, // Resolver siempre la promesa para capturar todos los códigos de estado
126
+ timeout: 60000, // Dar 60 segundos a QA para responder cada hilo individual
127
+ };
128
+
129
+ // Lógica de archivo (Multipart) vs JSON Body
130
+ let resolvedUrl = parseDynamicVariables(options.url);
131
+ reqConfig.url = resolvedUrl;
132
+
133
+ // Lógica de variables dinámicas en Content/Body y Headers
134
+ let finalHeaders = { ...parsedHeaders };
135
+ for (const [key, val] of Object.entries(finalHeaders)) {
136
+ if (typeof val === 'string') finalHeaders[key] = parseDynamicVariables(val);
137
+ }
138
+ reqConfig.headers = finalHeaders;
139
+
140
+ // Construir Payload (JSON puro vs Multipart)
141
+ if (options.file && ['POST', 'PUT', 'PATCH'].includes(method)) {
142
+ const form = new FormData();
143
+ // 1. Adjuntar Archivo
144
+ form.append(options.filekey || 'file', fs.createReadStream(options.file));
145
+
146
+ // 2. Adjuntar otros campos del Body si existen
147
+ if (templateBody) {
148
+ const parsedDynamicString = parseDynamicVariables(templateBody);
149
+ const bodyObj = JSON.parse(parsedDynamicString);
150
+ for (const [key, value] of Object.entries(bodyObj)) {
151
+ // En multipart, todo lo extra va como string
152
+ form.append(key, String(value));
153
+ }
154
+ }
155
+
156
+ reqConfig.data = form;
157
+ Object.assign(reqConfig.headers, form.getHeaders());
158
+ } else if (templateBody && ['POST', 'PUT', 'PATCH'].includes(method)) {
159
+ const parsedDynamicString = parseDynamicVariables(templateBody);
160
+ reqConfig.data = JSON.parse(parsedDynamicString);
161
+ if (!reqConfig.headers['Content-Type']) {
162
+ reqConfig.headers['Content-Type'] = 'application/json';
163
+ }
164
+ }
165
+
166
+ try {
167
+ const response = await axios(reqConfig);
168
+
169
+ // Calcular tiempo
170
+ const endTime = process.hrtime(startTime);
171
+ const latencyMs = (endTime[0] * 1000) + (endTime[1] / 1000000);
172
+
173
+ metrics.latencies.push(latencyMs);
174
+ metrics.statusCodes[response.status] = (metrics.statusCodes[response.status] || 0) + 1;
175
+
176
+ // Consideramos éxito códigos 2xx y 3xx
177
+ if (response.status >= 200 && response.status < 400) {
178
+ metrics.success++;
179
+ } else {
180
+ metrics.errors++;
181
+ }
182
+
183
+ } catch (error) {
184
+ // Errores de red (Timeout, conexión rechazada, dns)
185
+ metrics.errors++;
186
+ metrics.statusCodes['NETWORK_ERROR'] = (metrics.statusCodes['NETWORK_ERROR'] || 0) + 1;
187
+ } finally {
188
+ activeConnections--;
189
+ metrics.total++;
190
+
191
+ // Calcular throughput en vivo
192
+ const elapsedSecs = Math.max(0.1, (Date.now() - metrics.startTime) / 1000);
193
+ const currentThroughput = (metrics.total / elapsedSecs).toFixed(1);
194
+
195
+ progressBar.increment(1, { throughput: currentThroughput });
196
+ }
197
+ }
198
+
199
+ // -------------------------------------------------------------
200
+ // Motor de Ejecución (Ramp-up y Límite)
201
+ // -------------------------------------------------------------
202
+ async function runTest() {
203
+ // Guardar la cabecera de configuración para incluirla en reporte.txt
204
+ metrics.configHeader = [];
205
+ metrics.configHeader.push(`URL: ${options.url}`);
206
+ metrics.configHeader.push(`Método: ${method}`);
207
+ if (isRampUp) {
208
+ metrics.configHeader.push(`Ramp-up: De ${startRate} a ${targetRate} req/s en ${rampUpTime}s`);
209
+ metrics.configHeader.push(`Duración total: ${duration} segundos`);
210
+ metrics.configHeader.push(`Total Estimado: ~${totalRequestsExpected} peticiones`);
211
+ } else {
212
+ metrics.configHeader.push(`Peticiones constantes: ${startRate} req/s | Duración: ${duration}s`);
213
+ metrics.configHeader.push(`Total Estimado: ~${totalRequestsExpected} peticiones`);
214
+ }
215
+ if (options.file) metrics.configHeader.push(`Archivo adjunto: ${options.file}`);
216
+
217
+ console.log(`\nIniciando Prueba de Estrés HTTP 🚀`);
218
+ console.log(`URL: ${options.url}`);
219
+ console.log(`Método: ${method}`);
220
+ if (isRampUp) {
221
+ console.log(`Ramp-up: De ${startRate} a ${targetRate} req/s en ${rampUpTime}s`);
222
+ console.log(`Mantenido en ${targetRate} req/s hasta alcanzar los ${duration}s de prueba.`);
223
+ } else {
224
+ console.log(`Peticiones constantes: ${startRate} peticiones/s | Duración: ${duration}s`);
225
+ }
226
+ if (options.file) console.log(`Cargando archivo: ${options.file}`);
227
+ console.log(`Total Estimado: ~${totalRequestsExpected} peticiones\n`);
228
+
229
+ metrics.startTime = Date.now();
230
+ progressBar.start(totalRequestsExpected, 0, { throughput: 0 });
231
+
232
+ let currentSecond = 0;
233
+ let sentRequests = 0;
234
+
235
+ return new Promise((resolve) => {
236
+ const interval = setInterval(async () => {
237
+ currentSecond++;
238
+ const currentRate = calculateRate(currentSecond);
239
+
240
+ const requestsForThisSecond = [];
241
+ for (let i = 0; i < currentRate; i++) {
242
+ if (sentRequests >= totalRequestsExpected) break;
243
+ requestsForThisSecond.push(makeRequest());
244
+ sentRequests++;
245
+ }
246
+
247
+ if (currentSecond >= duration) {
248
+ clearInterval(interval);
249
+
250
+ let waitTime = 0;
251
+ const checkDone = setInterval(() => {
252
+ waitTime++;
253
+ if (activeConnections === 0 || waitTime > 180) {
254
+ // 90 segundos máximos de gracia esperando que Node reciba respuesta de QA (bottleneck)
255
+ clearInterval(checkDone);
256
+ metrics.endTime = Date.now();
257
+ progressBar.stop();
258
+ resolve();
259
+ }
260
+ }, 500);
261
+ }
262
+ }, 1000);
263
+ });
264
+ }
265
+
266
+ // -------------------------------------------------------------
267
+ // Reporte de Métricas Final
268
+ // -------------------------------------------------------------
269
+ function analyzeMetrics() {
270
+ let reportText = "";
271
+ const logAndSave = (text) => {
272
+ console.log(text);
273
+ reportText += text + "\n";
274
+ };
275
+
276
+ // Encabezado de configuración al inicio del reporte
277
+ reportText += `CONFIGURACIÓN DE LA PRUEBA\n`;
278
+ reportText += `==========================================\n`;
279
+ if (metrics.configHeader) {
280
+ metrics.configHeader.forEach(line => { reportText += line + "\n"; });
281
+ }
282
+ reportText += `\n`;
283
+
284
+ logAndSave(`\n===========================================`);
285
+ logAndSave(`📊 RESUMEN DE LA PRUEBA`);
286
+ logAndSave(`===========================================`);
287
+
288
+ if (metrics.total === 0) {
289
+ logAndSave("No se completó ninguna petición.");
290
+ fs.writeFileSync('reporte.txt', reportText, 'utf8');
291
+ return;
292
+ }
293
+
294
+ // Ordenar latencias numéricamente
295
+ metrics.latencies.sort((a, b) => a - b);
296
+ const min = metrics.latencies[0] || 0;
297
+ const max = metrics.latencies[metrics.latencies.length - 1] || 0;
298
+ const sum = metrics.latencies.reduce((a, b) => a + b, 0);
299
+ const avg = metrics.latencies.length > 0 ? sum / metrics.latencies.length : 0;
300
+
301
+ // Percentil 95
302
+ const p95Index = Math.floor(metrics.latencies.length * 0.95);
303
+ const p95 = metrics.latencies[p95Index] || 0;
304
+
305
+ // Throughput Real
306
+ const totalTestDurationSecs = Math.max(0.1, (metrics.endTime - metrics.startTime) / 1000);
307
+ const avgThroughput = (metrics.total / totalTestDurationSecs).toFixed(2);
308
+
309
+ // Separar los distintos tipos de éxito/fallo
310
+ let count2xx = 0, count3xx = 0, count4xx = 0, count5xx = 0;
311
+ for (const code of Object.keys(metrics.statusCodes)) {
312
+ const numericCode = parseInt(code, 10);
313
+ if (numericCode >= 200 && numericCode < 300) count2xx += metrics.statusCodes[code];
314
+ else if (numericCode >= 300 && numericCode < 400) count3xx += metrics.statusCodes[code];
315
+ else if (numericCode >= 400 && numericCode < 500) count4xx += metrics.statusCodes[code];
316
+ else if (numericCode >= 500) count5xx += metrics.statusCodes[code];
317
+ }
318
+
319
+ logAndSave(`🎯 Peticiones Totales : ${metrics.total}`);
320
+ logAndSave(`⚡ Conexiones Máximas : ${maxConcurrentConnections}`);
321
+ logAndSave(`🚀 Rendimiento / Rate : ${avgThroughput} Req/s`);
322
+ logAndSave(`\n📊 DESGLOSE DE RESULTADOS:`);
323
+ logAndSave(`✅ Exitosas (2xx) : ${count2xx}`);
324
+ if (count3xx > 0) logAndSave(`🔀 Redirecciones (3xx) : ${count3xx}`);
325
+ logAndSave(`⚠️ Errores Cliente (4xx): ${count4xx} <- (Bloqueos, No Autorizado, Bad Request)`);
326
+ logAndSave(`❌ Errores Server (5xx): ${count5xx + (metrics.statusCodes['NETWORK_ERROR'] || 0)} <- (Caídas del Servidor, Timeout)`);
327
+
328
+ logAndSave(`\n⏱️ TIEMPOS DE RESPUESTA (Latencia en ms):`);
329
+ logAndSave(` Mínimo : ${min.toFixed(2)} ms`);
330
+ logAndSave(` Máximo : ${max.toFixed(2)} ms`);
331
+ logAndSave(` Promedio: ${avg.toFixed(2)} ms`);
332
+ logAndSave(` P95 : ${p95.toFixed(2)} ms`);
333
+
334
+ logAndSave(`\n🏷️ CÓDIGOS DE ESTADO HTTP:`);
335
+ for (const [code, count] of Object.entries(metrics.statusCodes)) {
336
+ logAndSave(` [${code}] -> ${count} veces`);
337
+ }
338
+ logAndSave(`===========================================\n`);
339
+
340
+ // Escribir el reporte completo en el archivo
341
+ try {
342
+ fs.writeFileSync('reporte.txt', reportText, 'utf8');
343
+ console.log(`\n📄 Reporte guardado exitosamente en: reporte.txt`);
344
+ } catch(e) {
345
+ console.error(`\nNo se pudo guardar el reporte.txt: ${e.message}`);
346
+ }
347
+ }
348
+
349
+ // Iniciar
350
+ runTest().then(() => {
351
+ analyzeMetrics();
352
+ }).catch(console.error);
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "prix-r9",
3
+ "version": "1.0.0",
4
+ "description": "Herramienta CLI ligera para pruebas de estrés de APIs REST con ramp-up progresivo y variables dinámicas.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "prix-r9": "index.js",
8
+ "prix-r9-curl": "import-curl.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "import-curl.js",
13
+ "promptInforme.txt",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "keywords": [
20
+ "stress",
21
+ "load",
22
+ "api",
23
+ "qa",
24
+ "testing",
25
+ "http",
26
+ "ramp-up",
27
+ "benchmark",
28
+ "performance"
29
+ ],
30
+ "author": "QA Team",
31
+ "license": "ISC",
32
+ "type": "commonjs",
33
+ "engines": {
34
+ "node": ">=16.0.0"
35
+ },
36
+ "repository": {},
37
+ "dependencies": {
38
+ "axios": "^1.13.5",
39
+ "cli-progress": "^3.12.0",
40
+ "commander": "^14.0.3",
41
+ "curlconverter": "^4.12.0",
42
+ "form-data": "^4.0.5",
43
+ "uuid": "^13.0.0"
44
+ }
45
+ }
@@ -0,0 +1,93 @@
1
+ Actúa como un Ingeniero QA Senior con experiencia en pruebas de rendimiento y carga de APIs REST.
2
+ A continuación te proporciono los resultados de MÚLTIPLES ESCENARIOS de pruebas de estrés HTTP
3
+ realizadas contra endpoint(s) de un sistema financiero en ambiente QA.
4
+ Cada escenario tiene una configuración distinta (diferente Ramp-Up, carga, duración, etc.)
5
+ y fue ejecutado de forma independiente para comparar el comportamiento del servidor bajo distintos niveles de estrés.
6
+
7
+ Necesito que redactes un INFORME TÉCNICO PROFESIONAL en español con las siguientes secciones:
8
+
9
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10
+ 1. RESUMEN EJECUTIVO
11
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12
+ Describe en 3-4 oraciones qué se probó, cuántos escenarios hubo y cuál fue la conclusión general.
13
+ Redacta sin tecnicismos, orientado a gerencia.
14
+
15
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16
+ 2. TABLA COMPARATIVA DE ESCENARIOS
17
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+ Crea una tabla comparativa con una fila por escenario y las siguientes columnas:
19
+ - Escenario (nombre o número)
20
+ - Carga configurada (startRate → targetRate)
21
+ - Peticiones totales
22
+ - Exitosas (2xx)
23
+ - Errores cliente (4xx)
24
+ - Errores servidor (5xx)
25
+ - Rendimiento (Req/s)
26
+ - Latencia Promedio (ms)
27
+ - P95 (ms)
28
+ - Conexiones Máximas
29
+ - Resultado general (✅ Estable / ⚠️ Degradado / ❌ Crítico)
30
+
31
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32
+ 3. ANÁLISIS POR ESCENARIO
33
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34
+ Para cada escenario, analiza en detalle:
35
+ - Qué nivel de carga se aplicó y por qué el Ramp-Up gradual es importante
36
+ - Comportamiento del servidor: ¿respondió bien, se degradó, o se protegió?
37
+ - Interpretación de los errores 4xx vs 5xx
38
+ - Qué significa el P95 en ese contexto y por qué difiere del Promedio
39
+ - Qué indica el número de Conexiones Máximas sobre la concurrencia soportada
40
+
41
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42
+ 4. DIAGNÓSTICO GENERAL DEL SERVIDOR
43
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44
+ Basándote en el patrón de todos los escenarios, diagnostica:
45
+ - ¿En qué nivel de carga el servidor empieza a degradarse?
46
+ - ¿Los errores 401 son por autenticación inválida o por Rate Limiting / WAF?
47
+ - ¿Hay señales de cuello de botella en base de datos, memoria o red?
48
+ - ¿El servidor se recupera o se queda degradado una vez superado el umbral?
49
+
50
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51
+ 5. CONCLUSIONES Y RECOMENDACIONES
52
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53
+ - ¿Cuál es la carga máxima segura que soporta el endpoint actualmente?
54
+ - Acciones concretas para el equipo de desarrollo e infraestructura
55
+ - ¿Qué escenario debería usarse como prueba de regresión en futuros sprints?
56
+
57
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58
+ 6. SEMÁFORO DE SALUD DEL SISTEMA
59
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60
+ Genera una tabla con los indicadores clave clasificados en:
61
+ 🟢 Verde = dentro de límites aceptables
62
+ 🟡 Amarillo = requiere atención
63
+ 🔴 Rojo = crítico, acción inmediata requerida
64
+
65
+ El tono debe ser formal y técnico, adecuado para presentar tanto al equipo de desarrollo como a gerencia de TI.
66
+
67
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68
+ RESULTADOS DE LOS ESCENARIOS:
69
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
+
71
+ [ESCENARIO 1]
72
+ [PEGA AQUÍ EL CONTENIDO DEL reporte.txt DEL ESCENARIO 1]
73
+
74
+ ---
75
+
76
+ [ESCENARIO 2]
77
+ [PEGA AQUÍ EL CONTENIDO DEL reporte.txt DEL ESCENARIO 2]
78
+
79
+ ---
80
+
81
+ [ESCENARIO 3 - Agrega tantos como necesites con el mismo formato]
82
+ [PEGA AQUÍ EL CONTENIDO DEL reporte.txt DEL ESCENARIO 3]
83
+
84
+
85
+
86
+
87
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88
+ INFORMACIÓN ADICIONAL DEL SISTEMA:
89
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
+ Endpoint evaluado: [Nombre del endpoint, ej: GetReportAlliedBusinessByCompany]
91
+ Sistema: [Nombre del sistema, ej: MDL37 - Reportes de Comercios Aliados]
92
+ Ambiente: QA
93
+ Fecha de prueba: [Fecha]