viberadar 0.3.208 → 0.3.209

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B,OAAO,EAAE,UAAU,EAA4H,MAAM,YAAY,CAAC;AAOlK,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB;AA0vED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA+gG1G"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B,OAAO,EAAE,UAAU,EAA4H,MAAM,YAAY,CAAC;AAOlK,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB;AA0vED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAgvG1G"}
@@ -2095,7 +2095,7 @@ function startServer({ data: initialData, port, projectRoot }) {
2095
2095
  let loadRunning = false;
2096
2096
  let loadProc = null;
2097
2097
  let loadState = {
2098
- status: 'idle', startTime: 0, buckets: [], totalRequests: 0,
2098
+ runId: null, status: 'idle', startTime: 0, buckets: [], totalRequests: 0,
2099
2099
  totalErrors: 0, logs: [], script: '', config: null, summary: null,
2100
2100
  };
2101
2101
  let probeRunning = false;
@@ -2184,36 +2184,170 @@ function startServer({ data: initialData, port, projectRoot }) {
2184
2184
  probeRunning = false;
2185
2185
  }
2186
2186
  }
2187
- function parseK6Dur(s) {
2188
- let m;
2189
- if ((m = s.match(/^([\d.]+)µs$/)))
2190
- return parseFloat(m[1]) / 1000;
2191
- if ((m = s.match(/^([\d.]+)ms$/)))
2192
- return parseFloat(m[1]);
2193
- if ((m = s.match(/^([\d.]+)s$/)))
2194
- return parseFloat(m[1]) * 1000;
2195
- if ((m = s.match(/^(\d+)m([\d.]+)s$/)))
2196
- return parseInt(m[1]) * 60000 + parseFloat(m[2]) * 1000;
2197
- return 0;
2187
+ const loadRunsDir = path.join(projectRoot, '.viberadar', 'load-runs');
2188
+ const MAX_LOAD_RUN_HISTORY = 50;
2189
+ function sanitizeLoadScriptName(name) {
2190
+ const raw = typeof name === 'string' && name.trim() ? name.trim() : 'Без названия';
2191
+ return raw.replace(/[^a-zA-Zа-яА-ЯёЁ0-9_\- .]/g, '_').slice(0, 80);
2198
2192
  }
2199
- function parseK6Summary(text) {
2200
- const s = {};
2201
- const dur = text.match(/http_req_duration[^:]*:\s+avg=([\w.µ]+)[^\n]*p\(90\)=([\w.µ]+)[^\n]*p\(95\)=([\w.µ]+)/);
2202
- if (dur) {
2203
- s.avgDuration = parseK6Dur(dur[1]);
2204
- s.p90Duration = parseK6Dur(dur[2]);
2205
- s.p95Duration = parseK6Dur(dur[3]);
2206
- }
2207
- const reqs = text.match(/\bhttp_reqs[^:]*:\s+(\d+)\s+([\d.]+)\/s/);
2208
- if (reqs) {
2209
- s.totalRequests = parseInt(reqs[1]);
2210
- s.rps = parseFloat(reqs[2]);
2211
- }
2212
- const fail = text.match(/http_req_failed[^:]*:\s+([\d.]+)%/);
2213
- if (fail)
2214
- s.errorPct = parseFloat(fail[1]);
2215
- return s;
2193
+ function normalizeLoadDuration(value) {
2194
+ const raw = typeof value === 'string' && value.trim() ? value.trim() : '30s';
2195
+ if (!/^(\d+(ms|s|m|h))+$/.test(raw))
2196
+ return '30s';
2197
+ return raw;
2216
2198
  }
2199
+ function normalizeLoadVus(value) {
2200
+ const n = typeof value === 'number' ? value : parseInt(String(value || '10'), 10);
2201
+ if (!Number.isFinite(n) || n < 1)
2202
+ return 10;
2203
+ return Math.min(Math.floor(n), 10000);
2204
+ }
2205
+ function sanitizeLoadEnvVars(value) {
2206
+ if (!value || typeof value !== 'object')
2207
+ return {};
2208
+ const out = {};
2209
+ for (const [key, val] of Object.entries(value)) {
2210
+ const envKey = key.trim();
2211
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(envKey))
2212
+ continue;
2213
+ if (val == null)
2214
+ continue;
2215
+ out[envKey] = String(val);
2216
+ }
2217
+ return out;
2218
+ }
2219
+ function redactLoadEnvVars(envVars) {
2220
+ const out = {};
2221
+ for (const key of Object.keys(envVars)) {
2222
+ out[key] = /token|secret|password|key/i.test(key) ? '***' : envVars[key];
2223
+ }
2224
+ return out;
2225
+ }
2226
+ function buildLoadConfig(cfg) {
2227
+ const envVars = sanitizeLoadEnvVars(cfg.envVars);
2228
+ const baseUrl = typeof cfg.baseUrl === 'string' && cfg.baseUrl.trim() ? cfg.baseUrl.trim() : 'http://localhost:5000';
2229
+ envVars.BASE_URL = baseUrl;
2230
+ const config = {
2231
+ vus: normalizeLoadVus(cfg.vus),
2232
+ duration: normalizeLoadDuration(cfg.duration),
2233
+ baseUrl,
2234
+ scriptName: sanitizeLoadScriptName(cfg.scriptName),
2235
+ envVars: redactLoadEnvVars(envVars),
2236
+ };
2237
+ return { config, envVars };
2238
+ }
2239
+ function flattenK6Checks(group) {
2240
+ let passes = 0;
2241
+ let fails = 0;
2242
+ for (const check of group?.checks || []) {
2243
+ passes += Number(check.passes || 0);
2244
+ fails += Number(check.fails || 0);
2245
+ }
2246
+ for (const child of group?.groups || []) {
2247
+ const nested = flattenK6Checks(child);
2248
+ passes += nested.passes;
2249
+ fails += nested.fails;
2250
+ }
2251
+ return { passes, fails };
2252
+ }
2253
+ function normalizeK6Summary(raw, exitCode) {
2254
+ const metrics = raw?.metrics || {};
2255
+ const duration = metrics.http_req_duration?.values || {};
2256
+ const reqs = metrics.http_reqs?.values || {};
2257
+ const failed = metrics.http_req_failed?.values || {};
2258
+ const checks = flattenK6Checks(raw?.root_group);
2259
+ let thresholdsPassed = 0;
2260
+ let thresholdsFailed = 0;
2261
+ for (const metric of Object.values(metrics)) {
2262
+ for (const threshold of Object.values(metric?.thresholds || {})) {
2263
+ if (threshold?.ok === false)
2264
+ thresholdsFailed++;
2265
+ else if (threshold?.ok === true)
2266
+ thresholdsPassed++;
2267
+ }
2268
+ }
2269
+ return {
2270
+ totalRequests: typeof reqs.count === 'number' ? reqs.count : undefined,
2271
+ rps: typeof reqs.rate === 'number' ? reqs.rate : undefined,
2272
+ avgDuration: typeof duration.avg === 'number' ? duration.avg : undefined,
2273
+ p90Duration: typeof duration['p(90)'] === 'number' ? duration['p(90)'] : undefined,
2274
+ p95Duration: typeof duration['p(95)'] === 'number' ? duration['p(95)'] : undefined,
2275
+ p99Duration: typeof duration['p(99)'] === 'number' ? duration['p(99)'] : undefined,
2276
+ errorPct: typeof failed.rate === 'number' ? failed.rate * 100 : undefined,
2277
+ testRunDurationMs: typeof raw?.state?.testRunDurationMs === 'number' ? raw.state.testRunDurationMs : undefined,
2278
+ checksPassed: checks.passes,
2279
+ checksFailed: checks.fails,
2280
+ thresholdsPassed,
2281
+ thresholdsFailed,
2282
+ exitCode,
2283
+ };
2284
+ }
2285
+ function readK6Summary(summaryPath, exitCode) {
2286
+ try {
2287
+ if (!fs.existsSync(summaryPath))
2288
+ return { exitCode };
2289
+ return normalizeK6Summary(JSON.parse(fs.readFileSync(summaryPath, 'utf-8')), exitCode);
2290
+ }
2291
+ catch {
2292
+ return { exitCode };
2293
+ }
2294
+ }
2295
+ function readLoadRunIndex() {
2296
+ try {
2297
+ const p = path.join(loadRunsDir, 'index.json');
2298
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
2299
+ return Array.isArray(parsed) ? parsed : [];
2300
+ }
2301
+ catch {
2302
+ return [];
2303
+ }
2304
+ }
2305
+ function writeLoadRunIndex(items) {
2306
+ fs.mkdirSync(loadRunsDir, { recursive: true });
2307
+ fs.writeFileSync(path.join(loadRunsDir, 'index.json'), JSON.stringify(items, null, 2), 'utf-8');
2308
+ }
2309
+ function saveLoadRun() {
2310
+ if (!loadState.runId)
2311
+ return;
2312
+ fs.mkdirSync(loadRunsDir, { recursive: true });
2313
+ const record = {
2314
+ ...loadState,
2315
+ scriptName: loadState.config?.scriptName,
2316
+ createdAt: new Date(loadState.startTime || Date.now()).toISOString(),
2317
+ };
2318
+ fs.writeFileSync(path.join(loadRunsDir, `${loadState.runId}.json`), JSON.stringify(record, null, 2), 'utf-8');
2319
+ const index = readLoadRunIndex().filter(i => i.runId !== loadState.runId);
2320
+ index.unshift({
2321
+ runId: loadState.runId,
2322
+ scriptName: loadState.config?.scriptName || 'Без названия',
2323
+ createdAt: record.createdAt,
2324
+ status: loadState.status,
2325
+ startTime: loadState.startTime,
2326
+ endTime: loadState.endTime,
2327
+ config: loadState.config,
2328
+ summary: loadState.summary,
2329
+ });
2330
+ const compact = index.slice(0, MAX_LOAD_RUN_HISTORY);
2331
+ writeLoadRunIndex(compact);
2332
+ for (const old of index.slice(MAX_LOAD_RUN_HISTORY)) {
2333
+ try {
2334
+ fs.unlinkSync(path.join(loadRunsDir, `${old.runId}.json`));
2335
+ }
2336
+ catch { }
2337
+ }
2338
+ }
2339
+ function loadLastRunIntoState() {
2340
+ const latest = readLoadRunIndex()[0];
2341
+ if (!latest)
2342
+ return;
2343
+ try {
2344
+ const record = JSON.parse(fs.readFileSync(path.join(loadRunsDir, `${latest.runId}.json`), 'utf-8'));
2345
+ if (record && typeof record === 'object')
2346
+ loadState = record;
2347
+ }
2348
+ catch { }
2349
+ }
2350
+ loadLastRunIntoState();
2217
2351
  // ── SSE clients ────────────────────────────────────────────────────────────
2218
2352
  const sseClients = new Set();
2219
2353
  function broadcast(event, payload = {}) {
@@ -4911,7 +5045,7 @@ a{color:var(--blue)}
4911
5045
  req.on('data', (d) => { body += d; });
4912
5046
  req.on('end', () => {
4913
5047
  try {
4914
- const { name, script } = JSON.parse(body);
5048
+ const { name, script, vus, duration, baseUrl } = JSON.parse(body);
4915
5049
  if (!name || !script) {
4916
5050
  res.writeHead(400, jsonH);
4917
5051
  res.end(JSON.stringify({ error: 'name and script required' }));
@@ -4921,7 +5055,15 @@ a{color:var(--blue)}
4921
5055
  const safeName = name.replace(/[^a-zA-Zа-яА-ЯёЁ0-9_\- ]/g, '_').slice(0, 80);
4922
5056
  const date = new Date().toISOString().slice(0, 16).replace('T', ' ');
4923
5057
  const fileName = `${Date.now()}-${safeName.replace(/\s+/g, '_')}.json`;
4924
- const entry = { name: safeName, date, script, fileName };
5058
+ const entry = {
5059
+ name: safeName,
5060
+ date,
5061
+ script,
5062
+ fileName,
5063
+ vus: normalizeLoadVus(vus),
5064
+ duration: normalizeLoadDuration(duration),
5065
+ baseUrl: typeof baseUrl === 'string' && baseUrl.trim() ? baseUrl.trim() : 'http://localhost:5000',
5066
+ };
4925
5067
  // overwrite if same name exists
4926
5068
  const existing = fs.readdirSync(scriptsDir).find(f => {
4927
5069
  try {
@@ -5011,6 +5153,31 @@ a{color:var(--blue)}
5011
5153
  res.end(JSON.stringify(loadState));
5012
5154
  return;
5013
5155
  }
5156
+ if (url === '/api/load/runs' && req.method === 'GET') {
5157
+ res.writeHead(200, jsonH);
5158
+ res.end(JSON.stringify(readLoadRunIndex()));
5159
+ return;
5160
+ }
5161
+ const loadRunMatch = url.match(/^\/api\/load\/runs\/([^/]+)$/);
5162
+ if (loadRunMatch && req.method === 'GET') {
5163
+ try {
5164
+ const runId = decodeURIComponent(loadRunMatch[1]);
5165
+ if (!/^[a-zA-Z0-9_-]+$/.test(runId)) {
5166
+ res.writeHead(400, jsonH);
5167
+ res.end(JSON.stringify({ error: 'Bad run id' }));
5168
+ return;
5169
+ }
5170
+ const runPath = path.join(loadRunsDir, `${runId}.json`);
5171
+ const record = JSON.parse(fs.readFileSync(runPath, 'utf-8'));
5172
+ res.writeHead(200, jsonH);
5173
+ res.end(JSON.stringify(record));
5174
+ }
5175
+ catch {
5176
+ res.writeHead(404, jsonH);
5177
+ res.end(JSON.stringify({ error: 'Run not found' }));
5178
+ }
5179
+ return;
5180
+ }
5014
5181
  if (url === '/api/load/stop' && req.method === 'POST') {
5015
5182
  if (loadProc) {
5016
5183
  try {
@@ -5024,7 +5191,7 @@ a{color:var(--blue)}
5024
5191
  loadState.status = 'stopped';
5025
5192
  loadState.endTime = Date.now();
5026
5193
  }
5027
- broadcast('load-done', { status: loadState.status, summary: loadState.summary });
5194
+ broadcast('load-done', { runId: loadState.runId, status: loadState.status, summary: loadState.summary });
5028
5195
  res.writeHead(200, jsonH);
5029
5196
  res.end(JSON.stringify({ ok: true }));
5030
5197
  return;
@@ -5053,8 +5220,11 @@ a{color:var(--blue)}
5053
5220
  res.end(JSON.stringify({ error: 'No script provided' }));
5054
5221
  return;
5055
5222
  }
5223
+ const { config: loadConfig, envVars } = buildLoadConfig(cfg);
5224
+ const runId = `load-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5056
5225
  const scriptPath = path.join(os.tmpdir(), `viberadar-k6-${Date.now()}.js`);
5057
5226
  const jsonOutPath = path.join(os.tmpdir(), `viberadar-k6-out-${Date.now()}.ndjson`);
5227
+ const summaryPath = path.join(os.tmpdir(), `viberadar-k6-summary-${Date.now()}.json`);
5058
5228
  try {
5059
5229
  fs.writeFileSync(scriptPath, script, 'utf-8');
5060
5230
  }
@@ -5065,22 +5235,27 @@ a{color:var(--blue)}
5065
5235
  }
5066
5236
  loadRunning = true;
5067
5237
  loadState = {
5068
- status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0,
5069
- totalErrors: 0, logs: [], script, config: cfg, summary: null,
5238
+ runId, status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0,
5239
+ totalErrors: 0, logs: [], script, config: loadConfig, summary: null,
5070
5240
  };
5071
- broadcast('load-started', { config: cfg });
5241
+ broadcast('load-started', { runId, config: loadConfig });
5072
5242
  res.writeHead(200, jsonH);
5073
- res.end(JSON.stringify({ ok: true }));
5074
- // Build --env flags from cfg.envVars (e.g. { TOKEN: 'abc', BASE_URL: '...' })
5075
- const envVars = (typeof cfg.envVars === 'object' && cfg.envVars !== null)
5076
- ? cfg.envVars
5077
- : {};
5243
+ res.end(JSON.stringify({ ok: true, runId }));
5078
5244
  const envFlags = [];
5079
5245
  for (const [k, v] of Object.entries(envVars)) {
5080
5246
  if (k && v !== undefined && v !== '')
5081
5247
  envFlags.push('--env', `${k}=${v}`);
5082
5248
  }
5083
- loadProc = (0, child_process_1.spawn)('k6', ['run', ...envFlags, '--out', `json=${jsonOutPath}`, scriptPath], {
5249
+ const args = [
5250
+ 'run',
5251
+ '--vus', String(loadConfig.vus),
5252
+ '--duration', loadConfig.duration,
5253
+ ...envFlags,
5254
+ '--summary-export', summaryPath,
5255
+ '--out', `json=${jsonOutPath}`,
5256
+ scriptPath,
5257
+ ];
5258
+ loadProc = (0, child_process_1.spawn)('k6', args, {
5084
5259
  cwd: projectRoot, env: { ...process.env }, shell: WIN, stdio: 'pipe',
5085
5260
  });
5086
5261
  const addLog = (line) => {
@@ -5156,7 +5331,7 @@ a{color:var(--blue)}
5156
5331
  }
5157
5332
  if (changed) {
5158
5333
  const slice = loadState.buckets.slice(-30);
5159
- broadcast('load-progress', { buckets: slice, total: loadState.totalRequests, errors: loadState.totalErrors });
5334
+ broadcast('load-progress', { runId: loadState.runId, buckets: slice, total: loadState.totalRequests, errors: loadState.totalErrors });
5160
5335
  }
5161
5336
  }
5162
5337
  catch { }
@@ -5166,11 +5341,17 @@ a{color:var(--blue)}
5166
5341
  loadRunning = false;
5167
5342
  loadProc = null;
5168
5343
  if (loadState.status === 'running') {
5169
- loadState.status = (code === 0 || code === null) ? 'done' : 'done';
5344
+ loadState.status = code === 0 ? 'done' : 'error';
5170
5345
  }
5171
5346
  loadState.endTime = Date.now();
5172
- loadState.summary = parseK6Summary(loadState.logs.join('\n'));
5173
- broadcast('load-done', { status: loadState.status, summary: loadState.summary });
5347
+ loadState.summary = readK6Summary(summaryPath, code);
5348
+ if (loadState.summary?.totalRequests != null)
5349
+ loadState.totalRequests = loadState.summary.totalRequests;
5350
+ if (loadState.summary?.errorPct != null && loadState.summary.totalRequests != null) {
5351
+ loadState.totalErrors = Math.round(loadState.summary.totalRequests * (loadState.summary.errorPct / 100));
5352
+ }
5353
+ saveLoadRun();
5354
+ broadcast('load-done', { runId: loadState.runId, status: loadState.status, summary: loadState.summary });
5174
5355
  try {
5175
5356
  fs.unlinkSync(scriptPath);
5176
5357
  }
@@ -5179,6 +5360,10 @@ a{color:var(--blue)}
5179
5360
  fs.unlinkSync(jsonOutPath);
5180
5361
  }
5181
5362
  catch { }
5363
+ try {
5364
+ fs.unlinkSync(summaryPath);
5365
+ }
5366
+ catch { }
5182
5367
  });
5183
5368
  loadProc.on('error', (err) => {
5184
5369
  clearInterval(watchInterval);
@@ -5187,11 +5372,17 @@ a{color:var(--blue)}
5187
5372
  loadState.status = 'error';
5188
5373
  loadState.endTime = Date.now();
5189
5374
  addLog(`❌ k6 не запустился: ${err.message}`);
5190
- broadcast('load-done', { status: 'error', summary: null });
5375
+ loadState.summary = { exitCode: null };
5376
+ saveLoadRun();
5377
+ broadcast('load-done', { runId: loadState.runId, status: 'error', summary: loadState.summary });
5191
5378
  try {
5192
5379
  fs.unlinkSync(scriptPath);
5193
5380
  }
5194
5381
  catch { }
5382
+ try {
5383
+ fs.unlinkSync(summaryPath);
5384
+ }
5385
+ catch { }
5195
5386
  });
5196
5387
  });
5197
5388
  return;