Skip to content

Extend devfeedback to the whole local dev cycle (Vite + Rspack) - #48

Merged
joeldickson merged 1 commit into
masterfrom
feat/full-dev-cycle-telemetry
Aug 5, 2026
Merged

Extend devfeedback to the whole local dev cycle (Vite + Rspack)#48
joeldickson merged 1 commit into
masterfrom
feat/full-dev-cycle-telemetry

Conversation

@joeldickson

Copy link
Copy Markdown
Contributor

Closes #46. Ingestion-side schema changes are tracked in #47.

getCommonMetadata() already captured timing, git, host and CPU beautifully, but every event described one compilation. So "how long from git pull to a working app?" was unanswerable, install time was invisible, and in dev mode closeBundle never fires — meaning time to a usable dev server was not measured at all.

packages/webpack-plugin is deliberately untouched, per the issue's non-goal.

What lands

A new type: "command" event on COMMAND_ENDPOINT (default http://compilation-metrics/command):

phase What it measures Emitted by
install Package manager run, plus cold/warm and lockfile change install hooks (npm, yarn, pnpm)
devserver Time until the dev server is listening Vite, Rsbuild, Rspack watch
clientready Time until the app is usable in the browser, with DCL and FCP Vite, Rsbuild

Plus a sessionId on every event type — including the existing webpack, vite, vitehmr, rspack and rsbuild payloads — so those phases stitch into one timeline. Purely additive; no existing payload changed shape.

Aborted runs are recorded too: Ctrl-C produces a devserver event with signal: "SIGINT". A developer who gave up waiting is the most interesting point on the chart.

Answers to the issue's open questions

1. Which package manager? All three. Every lockfile shape is hashed, all three are detected from npm_config_user_agent, and §4b (scraping npm's own --timing logs) is built since npm is in the set — that is where you find out a Playwright browser download or a node-gyp rebuild is the actual cost.

2. Option A vs B — cold coverage wanted, so A. A root preinstall/postinstall pair gives the exact span and a trustworthy coldInstall flag. Option B still ships and needs no repo change; when the root hooks are present the bundled one stands down, so one event is produced rather than two. On a genuinely cold clone the preinstall file does not exist yet, the try/catch swallows it, and the bundled hook falls back to process start time — cold clones are still measured, just less precisely. Every install event carries measurementSource (preinstall / postinstall / npm-timing), because the inferred route systematically undercounts and the two must not be aggregated together.

3. Vite range — widen. Peer is now vite >=4.0.0, rollup >=3.0.0. Prebundling detection is tri-state and simply absent under Rolldown rather than guessing, and never reaches into server._optimizedDeps.

4. Rspack endpoint — fixed. rsbuild/rspack now post to /rspack as the README always documented, instead of the webpack endpoint. ⚠️ If your dashboards read Rspack data off the webpack endpoint, they need repointing#47 covers keeping both paths working during the transition, and it is worth checking how much existing "webpack" volume is actually rsbuild, since that skews historical webpack numbers.

5. spooledAt — kept as a first-class field, and raw data preferred over precomputation. npmTimers ships the raw map rather than a rollup, and #47 argues for a child table over storing a fixed subset.

Never on the critical path

This is most of the design, because telemetry that slows people down gets deleted from configs:

  • Install and signal-handler events are spooled, not sent — a small local NDJSON file, delivered by the next dev server or build. An install never waits on the network; a dying process never loses the race with an in-flight POST. Capped at 256 KB, events older than a week dropped rather than accumulated.
  • 1500 ms timeout on every POST. Off-VPN, nothing hangs. This was mandatory before emitting anything from the install path.
  • Git metadata cached until the repository actually changes (a statSync fingerprint over HEAD/ref/packed-refs/config), replacing three synchronous git spawns per HMR event.
  • Session id resolved once per process, and its keepalive touch is deferred via setImmediate — so the HMR path performs zero synchronous filesystem writes and zero process spawns.
  • Signal handlers write synchronously and get out of the way: they remove themselves and re-raise the signal if nobody else is listening, so Ctrl-C behaves exactly as it would without the plugin. Merely attaching a SIGINT listener would otherwise suppress Node's default exit and leave the dev server hanging.
  • stats.toJson() in the Rsbuild plugin no longer serializes the entire compilation on every rebuild.
  • Startup chatter is behind DEVFEEDBACK_DEBUG. No new stdout/stderr at the default log level.

Testing

64 tests, 12/12 turbo tasks green (check-types + test). New coverage: session lifecycle and TTL rollover, spool round-trip including size cap, age cutoff and malformed lines, lockfile hashing and package-manager parsing for all three managers, git cache invalidation on commit and branch switch, and the new dev-server/client-ready/abort paths in both plugins.

Beyond unit tests, the install hooks were verified end-to-end against real temp repos: the cold path, the --self stand-down, and this repo's own pnpm install running the bundled hook without noise or failure.

One bug found and fixed during that smoke test: priming the session before reading state made coldInstall always report false on the Option B path, because getSessionId writes the very state file that was about to be checked.

Rollout notes

  • pnpm blocks dependency lifecycle scripts by default. Allow it once during dev machine bootstrap in ~/.config/pnpm/config.yaml (allowBuilds, or onlyBuiltDependencies on pnpm 10 and earlier) so repos stay untouched.
  • The exact-install tier needs agoda-devfeedback-common as a direct devDependency — under pnpm a transitive dependency is not resolvable from the repo root.
  • Install events arrive on the next dev server or build start, not immediately. Nobody watches an install dashboard in real time.
  • --ignore-scripts skips install capture entirely.

🤖 Generated with Claude Code

Every event this package produced described one compilation, so the number a
developer actually feels -- git pull to a working app -- was unanswerable, and
in dev mode time to a usable dev server was not measured at all because
closeBundle never fires.

Adds a `type: "command"` event with an install, devserver or clientready phase,
a sessionId on every event type so those phases stitch into one timeline, and
install capture for npm, yarn and pnpm. Aborted runs are recorded too: Ctrl-C
produces a devserver event with `signal` set, without changing what Ctrl-C does.

Nothing here may sit on the critical path, which is most of the design:

- install and signal-handler events are spooled to a local NDJSON file and
  delivered by the next dev server or build, so an install never waits on the
  network and a dying process never loses the race
- 1500 ms timeout on every POST, so an off-VPN developer never hangs
- git metadata is cached until the repository actually changes, replacing three
  synchronous git spawns per HMR event
- the session id is resolved once per process; its keepalive touch is deferred,
  leaving the HMR path with no synchronous filesystem writes
- stats.toJson() no longer serializes the whole compilation on every rebuild
- startup chatter moved behind DEVFEEDBACK_DEBUG

Install capture has two tiers. The default needs no repo change and infers the
duration from the package manager's process start time. A preinstall/postinstall
pair in the consuming repo upgrades it to an exact span with a trustworthy
coldInstall flag; when those are present the bundled hook stands down, so one
event is produced rather than two. measurementSource records which route was
used, because the inferred one systematically undercounts and the two must not
be aggregated together.

Also fixes rsbuild and rspack posting to the webpack endpoint, contradicting the
README, and widens the Vite peer range to >=4.0.0.

packages/webpack-plugin is deliberately untouched.

Closes #46. Ingestion-side schema changes are tracked in #47.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@rayriffy rayriffy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 🦫

@joeldickson
joeldickson merged commit 6da9ddd into master Aug 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend devfeedback to the complete local dev cycle (Vite + Rspack)

4 participants