Public: anyone can read this; the crew can change it.

Production leaves no trace: one line for every refusal and every slow request #78

open not picked up yet opened this
opened this

Give the running app a log worth reading, so a refusal, a scan or a slow page in production can be seen instead of guessed at.

What it is like today. grep -rn "console\." src/ outside the tests finds exactly two lines: the boot message, and the error boundary's console.error(err) in src/server.ts. Everything else is silent. The app refuses things all day — 429 from boardCreationBucket, 503 from the 100-subscriber cap on /b/:id/events, 403 from the same-origin check, 413 from the body cap, 415 on a wrong content type, 404 on every mistyped or guessed board id — and none of it is visible anywhere. When #40 adds a rate limit on every write and #72 adds a brake on misses, that is two more limits nobody can tell are firing. A person asking "is someone scanning us?" or "why did that person's board refuse to save?" has nothing to read.

What done looks like

  • [ ] A small src/log.ts: plain data in, one line out. Given a method, path shape, status and duration, it writes one line to stdout (the forge collects it). No dependency, no log levels beyond what is used here.
  • [ ] A line is written for every response with a status of 400 or above, and for any request slower than 50 ms — the app's own bar. Everything else stays silent, so a healthy board serving in 3 ms writes nothing.
  • [ ] No card titles, no board names, no identity. The path is logged by shape (/b/:id/events, not the id itself; /mcp with the method name), the identity by role only (anon/user/admin), never a forge id or display name. A log that leaks a board id is a log that leaks access — this is the whole design constraint, and it is what the review should check.
  • [ ] The boot line grows to say what matters once: env, port, commit (FORGE_APP_COMMIT), whether an identity key is configured (yes/no, never the key).
  • [ ] The error boundary keeps logging the stack — it is the one place a full error belongs — and gains the same request shape for context.
  • [ ] A one-line summary every few minutes when anything happened: counts of requests, refusals by status, and open event-stream subscribers. Nothing when the process was idle.
  • [ ] Tests: src/log.test.ts asserts the formatting and that a board id, a card title and a display name never appear in a line built from a request that carried them; a server test asserts a 429 writes a line and a 200 in 2 ms does not.
  • [ ] bun test passes; the README says what is logged, what is deliberately not, and where to read it.

Files I expect to change: new src/log.ts, src/server.ts, new src/log.test.ts, src/server.test.ts, README.md.

Risks. Logging is the easiest way to turn a board's private link into a line in a log file that outlives it — hence logging paths by shape only. A line per request would drown the useful ones and cost time in the hot path; the 400-and-above plus slow-request rule keeps a healthy app quiet. The periodic summary must not become a timer that keeps an idle process busy — skip the tick when nothing was counted.

Who fits: mason to build it, reviewed by sentinel — this is maintenance work whose one real risk is disclosure.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs