backlens 0.2.4 → 0.2.6

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 CHANGED
@@ -12,6 +12,8 @@ npx backlens
12
12
 
13
13
  One command, in your server's folder. No code changes, no account, nothing leaves your machine.
14
14
 
15
+ ![backlens finding three issues in a NestJS API, fixing one with Promise.all and proving it with a replay, then creating a missing index](https://raw.githubusercontent.com/shayandutta/backlens/main/docs/demo.gif)
16
+
15
17
  ---
16
18
 
17
19
  ## What it does
@@ -151,7 +153,7 @@ cd backlens && npm install
151
153
  node --test test/
152
154
  ```
153
155
 
154
- Bug reports and pull requests are welcome.
156
+ Bug reports and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md). To report a security problem privately, see [SECURITY.md](SECURITY.md).
155
157
 
156
158
  ## License
157
159
 
package/analyze.js CHANGED
@@ -237,8 +237,8 @@ function percentile(values, p) {
237
237
 
238
238
  /** Routes and issues across every trace kept. */
239
239
  /**
240
- * fixed: issue key -> when a fix for it was kept. Such an issue is hidden until a request after
241
- * that moment shows it again.
240
+ * fixed: issue key -> when a fix for it was kept, and 'route:<route>' -> when code behind that
241
+ * route changed. Such issues are hidden until a request after that moment shows them again.
242
242
  */
243
243
  export function summarize(traces, known = [], advice = [], fixed = new Map()) {
244
244
  const routes = new Map();
@@ -323,7 +323,8 @@ export function summarize(traces, known = [], advice = [], fixed = new Map()) {
323
323
  if (!routes.has(route)) routes.set(route, { route, calls: 0, durations: [], queries: 0, redis: 0, http: 0, errors: 0, issueKeys: new Set(), last: 0 });
324
324
  }
325
325
  for (const [key, issue] of issues) {
326
- if (fixed.has(key) && fixed.get(key) >= (issue.lastAt || 0)) {
326
+ const since = Math.max(fixed.get(key) || 0, fixed.get(`route:${issue.route}`) || 0);
327
+ if (since && since >= (issue.lastAt || 0)) {
327
328
  issues.delete(key);
328
329
  routes.get(issue.route)?.issueKeys.delete(key);
329
330
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlens",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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
@@ -35,6 +35,20 @@ export function startServer({ port, root = process.cwd(), token, manualArgv = nu
35
35
  // Issue key -> the index created for it from the dashboard, so Undo still knows it.
36
36
  const createdIndexes = new Map();
37
37
 
38
+ /** Sends a recorded read to the app without recording it, so the app opens its connections. */
39
+ function warmUp(port, payload) {
40
+ return new Promise((resolve) => {
41
+ const r = http.request({ host: '127.0.0.1', port, method: payload.method, path: payload.url, headers: { ...payload.headers, 'x-backlens': 'skip' } }, (res) => {
42
+ res.resume();
43
+ res.on('end', resolve);
44
+ });
45
+ r.setTimeout(20000, () => r.destroy());
46
+ r.on('error', resolve);
47
+ r.on('close', resolve);
48
+ r.end();
49
+ });
50
+ }
51
+
38
52
  /** Sends one command to the recorder inside the app (see register.cjs). */
39
53
  function appCommand(port, body) {
40
54
  return new Promise((resolve, reject) => {
@@ -337,14 +351,24 @@ export function startServer({ port, root = process.cwd(), token, manualArgv = nu
337
351
  if (!app || !app.port) return json(res, 409, { error: 'The app is not running. Start it first.' });
338
352
  const body = (await readJson(req)) || {};
339
353
  let index = null;
354
+ const { issues } = summarize(traces, [...known.keys()], [...advice.values()]);
355
+ const issue = issues.find((i) => i.key === body.key);
340
356
  if (action !== 'info') {
341
- const { issues } = summarize(traces, [...known.keys()], [...advice.values()]);
342
- const issue = issues.find((i) => i.key === body.key);
343
357
  index = issue && issue.index ? issue.index : createdIndexes.get(body.key);
344
358
  if (!index) return json(res, 404, { error: 'That suggestion is gone. Refresh the list.' });
345
359
  }
346
360
  try {
347
- const out = await appCommand(app.port, { action, ...(index || {}) });
361
+ let out;
362
+ try {
363
+ out = await appCommand(app.port, { action, ...(index || {}) });
364
+ } catch (err) {
365
+ // A restarted app has no database connection until a request uses one. Replay the
366
+ // request that showed the issue once, unrecorded, so it opens one, then try again.
367
+ const payload = issue && replays.get(issue.example);
368
+ if (!/not used Postgres yet/.test(err.message) || !payload || !['GET', 'HEAD'].includes(payload.method)) throw err;
369
+ await warmUp(app.port, payload);
370
+ out = await appCommand(app.port, { action, ...(index || {}) });
371
+ }
348
372
  if (action === 'create') {
349
373
  createdIndexes.set(body.key, index);
350
374
  fixedAt.set(body.key, Date.now());
@@ -425,14 +449,18 @@ export function startServer({ port, root = process.cwd(), token, manualArgv = nu
425
449
  // Recorded requests on a route since the last fix kept there (the code they ran is the
426
450
  // code being changed now), oldest first.
427
451
  routeTraces: (route) => {
428
- const since = Math.max(0, ...[...fixedAt].filter(([key]) => key.split('|')[1] === route).map(([, at]) => at));
452
+ const since = fixedAt.get(`route:${route}`) || 0;
429
453
  return traces.filter((t) => t.at > since && routeOf(t.method, t.url, t.template) === route);
430
454
  },
431
455
  markFixed: (key, after) => {
432
- fixedAt.set(key, Date.now());
456
+ const now = Date.now();
457
+ fixedAt.set(key, now);
458
+ // The code behind this route changed: issues from its older requests point at old
459
+ // lines, so they wait for a request on the new code (the after-fix one below, or later).
460
+ fixedAt.set(`route:${key.split('|')[1]}`, now);
433
461
  if (after) {
434
462
  const { replayOf, ...trace } = after;
435
- traces.push({ ...trace, afterFix: true });
463
+ traces.push({ ...trace, at: now + 1, afterFix: true });
436
464
  received++;
437
465
  if (traces.length > KEEP) replays.delete(traces.shift().id);
438
466
  }
package/ui.html CHANGED
@@ -790,7 +790,7 @@ function compareCard(j) {
790
790
  let idx = {};
791
791
  async function loadIndexInfo() {
792
792
  idx.loading = true;
793
- try { idx.db = await aiPost('/api/index/info', {}); } catch (err) { idx.dbError = err.message; }
793
+ try { idx.db = await aiPost('/api/index/info', { key: fixJob && fixJob.issueKey }); } catch (err) { idx.dbError = err.message; }
794
794
  idx.loading = false;
795
795
  renderFix();
796
796
  }