viberadar 0.3.207 → 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,CAggG1G"}
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 = {}) {
@@ -2672,6 +2806,19 @@ function startServer({ data: initialData, port, projectRoot }) {
2672
2806
  return;
2673
2807
  }
2674
2808
  }
2809
+ else if (task === 'obs-feature-review') {
2810
+ const requestedFeatureKey = item.meta?.featureKey || item.featureKey;
2811
+ if (!requestedFeatureKey) {
2812
+ failBeforeStart('Фича не указана');
2813
+ return;
2814
+ }
2815
+ const builtPrompt = buildObsFeatureReviewPrompt(requestedFeatureKey, currentData);
2816
+ if (builtPrompt === null) {
2817
+ failBeforeStart(`Не удалось собрать prompt по наблюдаемости фичи ${requestedFeatureKey}`);
2818
+ return;
2819
+ }
2820
+ prompt = builtPrompt;
2821
+ }
2675
2822
  else if (task === 'actualize-docs') {
2676
2823
  if (!featureKey || !currentData.features) {
2677
2824
  failBeforeStart('Фича не найдена');
@@ -3371,6 +3518,16 @@ function startServer({ data: initialData, port, projectRoot }) {
3371
3518
  const label = meta?.fieldName ? `поле ${meta.fieldName}` : meta?.recommendationType || 'логи';
3372
3519
  title = `${agentLabel} — ${label} (${count} модулей)`;
3373
3520
  }
3521
+ else if (task === 'obs-feature-review') {
3522
+ const requestedFeatureKey = meta?.featureKey || featureKey;
3523
+ const isUnmapped = requestedFeatureKey === '__unmapped__';
3524
+ const feat = !isUnmapped ? currentData.features?.find(f => f.key === requestedFeatureKey) : null;
3525
+ if (!requestedFeatureKey || (!isUnmapped && !feat)) {
3526
+ broadcast('agent-error', { message: `Фича не найдена: ${requestedFeatureKey || 'не указана'}` });
3527
+ return null;
3528
+ }
3529
+ title = `${agentLabel} — наблюдаемость фичи "${isUnmapped ? 'Unmapped' : feat.label}"`;
3530
+ }
3374
3531
  else if (task === 'actualize-docs') {
3375
3532
  const feat = currentData.features?.find(f => f.key === featureKey);
3376
3533
  title = feat ? `${agentLabel} — документация "${feat.label}"` : `${agentLabel} — документация`;
@@ -4888,7 +5045,7 @@ a{color:var(--blue)}
4888
5045
  req.on('data', (d) => { body += d; });
4889
5046
  req.on('end', () => {
4890
5047
  try {
4891
- const { name, script } = JSON.parse(body);
5048
+ const { name, script, vus, duration, baseUrl } = JSON.parse(body);
4892
5049
  if (!name || !script) {
4893
5050
  res.writeHead(400, jsonH);
4894
5051
  res.end(JSON.stringify({ error: 'name and script required' }));
@@ -4898,7 +5055,15 @@ a{color:var(--blue)}
4898
5055
  const safeName = name.replace(/[^a-zA-Zа-яА-ЯёЁ0-9_\- ]/g, '_').slice(0, 80);
4899
5056
  const date = new Date().toISOString().slice(0, 16).replace('T', ' ');
4900
5057
  const fileName = `${Date.now()}-${safeName.replace(/\s+/g, '_')}.json`;
4901
- 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
+ };
4902
5067
  // overwrite if same name exists
4903
5068
  const existing = fs.readdirSync(scriptsDir).find(f => {
4904
5069
  try {
@@ -4988,6 +5153,31 @@ a{color:var(--blue)}
4988
5153
  res.end(JSON.stringify(loadState));
4989
5154
  return;
4990
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
+ }
4991
5181
  if (url === '/api/load/stop' && req.method === 'POST') {
4992
5182
  if (loadProc) {
4993
5183
  try {
@@ -5001,7 +5191,7 @@ a{color:var(--blue)}
5001
5191
  loadState.status = 'stopped';
5002
5192
  loadState.endTime = Date.now();
5003
5193
  }
5004
- broadcast('load-done', { status: loadState.status, summary: loadState.summary });
5194
+ broadcast('load-done', { runId: loadState.runId, status: loadState.status, summary: loadState.summary });
5005
5195
  res.writeHead(200, jsonH);
5006
5196
  res.end(JSON.stringify({ ok: true }));
5007
5197
  return;
@@ -5030,8 +5220,11 @@ a{color:var(--blue)}
5030
5220
  res.end(JSON.stringify({ error: 'No script provided' }));
5031
5221
  return;
5032
5222
  }
5223
+ const { config: loadConfig, envVars } = buildLoadConfig(cfg);
5224
+ const runId = `load-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5033
5225
  const scriptPath = path.join(os.tmpdir(), `viberadar-k6-${Date.now()}.js`);
5034
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`);
5035
5228
  try {
5036
5229
  fs.writeFileSync(scriptPath, script, 'utf-8');
5037
5230
  }
@@ -5042,22 +5235,27 @@ a{color:var(--blue)}
5042
5235
  }
5043
5236
  loadRunning = true;
5044
5237
  loadState = {
5045
- status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0,
5046
- 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,
5047
5240
  };
5048
- broadcast('load-started', { config: cfg });
5241
+ broadcast('load-started', { runId, config: loadConfig });
5049
5242
  res.writeHead(200, jsonH);
5050
- res.end(JSON.stringify({ ok: true }));
5051
- // Build --env flags from cfg.envVars (e.g. { TOKEN: 'abc', BASE_URL: '...' })
5052
- const envVars = (typeof cfg.envVars === 'object' && cfg.envVars !== null)
5053
- ? cfg.envVars
5054
- : {};
5243
+ res.end(JSON.stringify({ ok: true, runId }));
5055
5244
  const envFlags = [];
5056
5245
  for (const [k, v] of Object.entries(envVars)) {
5057
5246
  if (k && v !== undefined && v !== '')
5058
5247
  envFlags.push('--env', `${k}=${v}`);
5059
5248
  }
5060
- 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, {
5061
5259
  cwd: projectRoot, env: { ...process.env }, shell: WIN, stdio: 'pipe',
5062
5260
  });
5063
5261
  const addLog = (line) => {
@@ -5133,7 +5331,7 @@ a{color:var(--blue)}
5133
5331
  }
5134
5332
  if (changed) {
5135
5333
  const slice = loadState.buckets.slice(-30);
5136
- 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 });
5137
5335
  }
5138
5336
  }
5139
5337
  catch { }
@@ -5143,11 +5341,17 @@ a{color:var(--blue)}
5143
5341
  loadRunning = false;
5144
5342
  loadProc = null;
5145
5343
  if (loadState.status === 'running') {
5146
- loadState.status = (code === 0 || code === null) ? 'done' : 'done';
5344
+ loadState.status = code === 0 ? 'done' : 'error';
5147
5345
  }
5148
5346
  loadState.endTime = Date.now();
5149
- loadState.summary = parseK6Summary(loadState.logs.join('\n'));
5150
- 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 });
5151
5355
  try {
5152
5356
  fs.unlinkSync(scriptPath);
5153
5357
  }
@@ -5156,6 +5360,10 @@ a{color:var(--blue)}
5156
5360
  fs.unlinkSync(jsonOutPath);
5157
5361
  }
5158
5362
  catch { }
5363
+ try {
5364
+ fs.unlinkSync(summaryPath);
5365
+ }
5366
+ catch { }
5159
5367
  });
5160
5368
  loadProc.on('error', (err) => {
5161
5369
  clearInterval(watchInterval);
@@ -5164,11 +5372,17 @@ a{color:var(--blue)}
5164
5372
  loadState.status = 'error';
5165
5373
  loadState.endTime = Date.now();
5166
5374
  addLog(`❌ k6 не запустился: ${err.message}`);
5167
- 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 });
5168
5378
  try {
5169
5379
  fs.unlinkSync(scriptPath);
5170
5380
  }
5171
5381
  catch { }
5382
+ try {
5383
+ fs.unlinkSync(summaryPath);
5384
+ }
5385
+ catch { }
5172
5386
  });
5173
5387
  });
5174
5388
  return;