backlens 0.2.2 → 0.2.3

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/ai/fixes.js CHANGED
@@ -20,6 +20,9 @@ import { promiseAll } from './templates.js';
20
20
  */
21
21
 
22
22
  // Replaying a write sends it again, so it changes the dev database twice: reads only unless asked.
23
+ // Replays after a fix: warm-up runs first, then measured runs (reads only).
24
+ const WARMUPS = 3;
25
+ const SAMPLES = 5;
23
26
  const DEFAULTS = { provider: null, ollamaModel: null, dailyBudgetUsd: 1, replay: true, replayWrites: false };
24
27
  const EDIT_REPAIRS = 2;
25
28
  const TYPE_REPAIRS = 2;
@@ -483,6 +486,12 @@ export function createAI(deps) {
483
486
  return { ok: r.code === 0, errors: r.out };
484
487
  }
485
488
 
489
+ const median = (list) => {
490
+ const xs = [...list].sort((a, b) => a - b);
491
+ const mid = Math.floor(xs.length / 2);
492
+ return Math.round((xs.length % 2 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2) * 100) / 100;
493
+ };
494
+
486
495
  /** What a request cost: the numbers shown before and after a fix. */
487
496
  function stats(trace) {
488
497
  const count = (kind) => trace.spans.filter((x) => x.kind === kind).length;
@@ -556,20 +565,33 @@ export function createAI(deps) {
556
565
  } else {
557
566
  // The app has just restarted, so its first request pays for cold caches and new connections.
558
567
  // A read is sent once to warm up, and the second one is measured.
559
- if (!isWrite) await replay(job, payload, deps.appPort());
560
- const trace = await replay(job, payload, deps.appPort());
568
+ // A just-restarted app is slow for its first requests (cold code, new database
569
+ // connections). A read is sent a few times to warm up, then measured several times and the
570
+ // median kept. A write runs once: replaying it changes data again.
571
+ const samples = [];
572
+ if (!isWrite) for (let i = 0; i < WARMUPS; i++) await replay(job, payload, deps.appPort());
573
+ for (let i = 0; i < (isWrite ? 1 : SAMPLES); i++) {
574
+ const t = await replay(job, payload, deps.appPort());
575
+ if (!t) break;
576
+ samples.push(t);
577
+ }
578
+ const trace = samples.at(-1);
561
579
  if (!trace) {
562
580
  r.status = 'fail';
563
581
  r.detail = 'the replayed request did not finish within 20 s';
564
582
  return finish(job, false, 'The replayed request did not come back.');
565
583
  }
566
584
  replayed = true;
567
- const still = checkTrace(trace).some((i) => sameIssue(i, job.issue));
585
+ const still = samples.some((t) => checkTrace(t).some((i) => sameIssue(i, job.issue)));
568
586
  const sameClass = original ? Math.floor(trace.status / 100) === Math.floor(original.status / 100) : trace.status < 400;
569
- const before = original ? `${original.status} in ${original.duration} ms, ${original.spans.length} calls` : '';
587
+ // Before: the same route's recorded requests from before the change, not just one of them.
588
+ const earlier = (deps.routeTraces ? deps.routeTraces(job.issue.route) : []).filter((t) => t.status === (original || trace).status);
589
+ const beforeList = earlier.length ? earlier.slice(-SAMPLES) : original ? [original] : [];
590
+ const after = { ...stats(trace), ms: median(samples.map((t) => t.duration)) };
591
+ const before = beforeList.length ? { ...stats(original || beforeList.at(-1)), ms: median(beforeList.map((t) => t.duration)) } : null;
570
592
  r.status = !still && sameClass ? 'done' : 'fail';
571
- r.detail = `before: ${before}\nafter: ${trace.status} in ${trace.duration} ms, ${trace.spans.length} calls${still ? ', issue still there' : ', issue gone'}`;
572
- job.compare = { before: original ? stats(original) : null, after: stats(trace), issueGone: !still, warmed: !isWrite };
593
+ r.detail = `before: ${before ? `${before.status}, median ${before.ms} ms of ${beforeList.length} recorded, ${before.queries} queries` : 'no recorded request'}\nafter: ${after.status}, median ${after.ms} ms of ${samples.length} replays, ${after.queries} queries${still ? ', issue still there' : ', issue gone'}`;
594
+ job.compare = { before, after, beforeRuns: beforeList.length, afterRuns: samples.length, issueGone: !still, warmed: !isWrite };
573
595
  job.afterTrace = trace;
574
596
  if (trace.status === 401 && original && original.status !== 401) r.detail += '\nThe recorded login may have expired: sign in again and use the page once more.';
575
597
  if (r.status === 'fail') return finish(job, false, still ? 'The issue is still there after the change.' : `The request now answers ${trace.status}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlens",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Run your Node backend through backlens and see every request, every query, and what is wrong with them.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/server.js CHANGED
@@ -422,6 +422,12 @@ export function startServer({ port, root = process.cwd(), token, manualArgv = nu
422
422
  return issues.find((i) => i.key === key) || (testRun.report ? testRun.report.issues.find((i) => i.key === key) : null);
423
423
  },
424
424
  getTrace: (id) => traces.find((t) => t.id === id) || testRun.traces.find((t) => t.id === id),
425
+ // Recorded requests on a route since the last fix kept there (the code they ran is the
426
+ // code being changed now), oldest first.
427
+ routeTraces: (route) => {
428
+ const since = Math.max(0, ...[...fixedAt].filter(([key]) => key.split('|')[1] === route).map(([, at]) => at));
429
+ return traces.filter((t) => t.at > since && routeOf(t.method, t.url, t.template) === route);
430
+ },
425
431
  markFixed: (key, after) => {
426
432
  fixedAt.set(key, Date.now());
427
433
  if (after) {
package/ui.html CHANGED
@@ -157,7 +157,8 @@
157
157
  a.btn { text-decoration: none; display: inline-block; }
158
158
  .compare-card { border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin: 4px 0 14px; }
159
159
  .compare-card h4 { margin: 0 0 8px; font-size: 13px; }
160
- .compare-card table { width: auto; border-collapse: collapse; font: 13px var(--mono); }
160
+ .compare-card table { width: auto; max-width: 100%; border-collapse: collapse; font: 13px var(--mono); margin-bottom: 10px; }
161
+ .compare-card td, .compare-card th { white-space: nowrap; }
161
162
  .compare-card th { text-align: left; font-weight: 400; color: var(--sub); padding: 3px 18px 3px 0; }
162
163
  .compare-card td { padding: 3px 10px 3px 0; }
163
164
  .compare-card .arrow { color: var(--sub); }
@@ -769,20 +770,20 @@ function compareCard(j) {
769
770
  const c = j.compare;
770
771
  if (!c || !c.before || !['verified', 'failed', 'unverified', 'kept', 'reverted'].includes(j.stage)) return '';
771
772
  const pct = (a, b) => (a ? Math.round(((b - a) / a) * 100) : 0);
772
- const row = (label, a, b, fmt = (x) => x, lowerIsBetter = true) => {
773
+ const row = (label, a, b, fmt = (x) => x, noise = 0) => {
773
774
  const d = pct(a, b);
774
- const tone = a === b ? 'same' : (b < a) === lowerIsBetter ? 'better' : 'worse';
775
- return `<tr><th>${label}</th><td>${fmt(a)}</td><td class="arrow">→</td><td class="${tone}">${fmt(b)}</td><td class="${tone} delta">${a === b ? '' : `${d > 0 ? '+' : ''}${d}%`}</td></tr>`;
775
+ // Small time differences are noise, not a result: shown plain, not green or red.
776
+ const tone = a === b || Math.abs(b - a) <= noise ? 'same' : b < a ? 'better' : 'worse';
777
+ return `<tr><th>${label}</th><td>${fmt(a)}</td><td class="arrow">→</td><td class="${tone}">${fmt(b)}</td><td class="${tone} delta">${tone === 'same' ? '' : `${d > 0 ? '+' : ''}${d}%`}</td></tr>`;
776
778
  };
777
- const kind = esc((j.issueTitle || '').split(':')[0]);
778
779
  return `<section class="compare-card"><h4>Same request, before and after the change</h4><table>
779
- ${row('time', c.before.ms, c.after.ms, (x) => ms(x))}
780
+ ${row('time (median)', c.before.ms, c.after.ms, (x) => ms(x), Math.max(3, c.before.ms * 0.15))}
780
781
  ${row('database queries', c.before.queries, c.after.queries)}
781
782
  ${c.before.redis || c.after.redis ? row('redis calls', c.before.redis, c.after.redis) : ''}
782
783
  ${c.before.http || c.after.http ? row('outgoing http', c.before.http, c.after.http) : ''}
783
784
  <tr><th>status</th><td>${c.before.status}</td><td class="arrow">→</td><td class="${c.before.status === c.after.status ? 'same' : 'worse'}">${c.after.status}</td><td></td></tr>
784
- <tr><th>issue</th><td>${kind || 'found'}</td><td class="arrow">→</td><td class="${c.issueGone ? 'better' : 'worse'}">${c.issueGone ? 'gone' : 'still there'}</td><td></td></tr>
785
- </table><p class="note">One request each${c.warmed ? ', the after one measured once the restarted app was warm' : ''}. Query counts are exact; times vary a few ms from run to run.</p></section>`;
785
+ <tr><th>issue</th><td>found</td><td class="arrow">→</td><td class="${c.issueGone ? 'better' : 'worse'}">${c.issueGone ? 'gone' : 'still there'}</td><td></td></tr>
786
+ </table><p class="note">Before: ${c.beforeRuns} recorded request${c.beforeRuns === 1 ? '' : 's'}. After: ${c.afterRuns} replay${c.afterRuns === 1 ? '' : 's'}${c.warmed ? ', after warming the restarted app up' : ''}. Query counts are exact; a time change within a few ms is noise.</p></section>`;
786
787
  }
787
788
 
788
789
  // ---- creating a suggested index from the drawer ----------------------------------------