Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/whole-dev-cycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'agoda-devfeedback-common': minor
'agoda-devfeedback-vite2': minor
'agoda-devfeedback-rsbuild': minor
---

Extend devfeedback from single compilations to the whole local dev cycle.

- New `type: "command"` event with an `install`, `devserver` or `clientready` phase, posted to `COMMAND_ENDPOINT` (default `http://compilation-metrics/command`). Dev server ready time was previously never measured, because `closeBundle` does not fire in dev.
- Every event now carries a `sessionId`, so install → dev server ready → first HMR correlate into one timeline. Purely additive; existing payloads are unchanged.
- Install capture for npm, yarn and pnpm. Works with no repo change; a `preinstall`/`postinstall` pair in the consuming repo upgrades it to an exact span with a trustworthy `coldInstall` flag. Install events are spooled locally and delivered by the next dev server or build, so an install never waits on the network.
- Aborted runs are recorded: Ctrl-C produces a `devserver` event with `signal` set, without changing what Ctrl-C does.
- Rspack and Rsbuild events now post to `RSPACK_ENDPOINT` (`/rspack`) as the README always documented, instead of the webpack endpoint.
- Off the critical path: 1500 ms POST timeout, git metadata cached until the repo actually changes instead of three `git` spawns per HMR event, `stats.toJson()` no longer serializes the whole compilation on every Rsbuild rebuild, and startup chatter moved behind `DEVFEEDBACK_DEBUG`.
- Widened the Vite peer range to `>=4.0.0` (`rollup >=3.0.0`).
97 changes: 90 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ Welcome to agoda-devfeedback, the JavaScript/TypeScript package collection that'
## Build Time (Compilation Time): Because Life's Too Short for Slow Builds

This collection supports collecting build time (compilation time) metrics across multiple bundlers:

- Webpack (4.x or 5.x)
- Vite (4.x)
- Vite (4.x and up, including 6.x and Rolldown-based builds)
- Rspack/Rsbuild (1.x)

It's like a stopwatch for your builds, but cooler, and now with more bundlers! 🎮
Expand All @@ -15,11 +16,14 @@ It's like a stopwatch for your builds, but cooler, and now with more bundlers!

The data is sent to the following default endpoints (customizable via environment variables):

| Bundler | Default | Environment Variable Override | Post Data Example
| --- | --- | --- | --- |
| WebPack | "<http://compilation-metrics/webpack>" | WEBPACK_ENDPOINT | [click here](examples/webpack.json) |
| Vite | "<http://compilation-metrics/vite>" | VITE_ENDPOINT | [click here](examples/vite.json) |
| Rspack | "<http://compilation-metrics/rspack>" | RSPACK_ENDPOINT | [click here](examples/rspack.json) |
| Bundler | Default | Environment Variable Override | Post Data Example |
| ----------------------------- | -------------------------------------- | ----------------------------- | ----------------------------------- |
| WebPack | "<http://compilation-metrics/webpack>" | WEBPACK_ENDPOINT | [click here](examples/webpack.json) |
| Vite | "<http://compilation-metrics/vite>" | VITE_ENDPOINT | [click here](examples/vite.json) |
| Rspack | "<http://compilation-metrics/rspack>" | RSPACK_ENDPOINT | [click here](examples/rspack.json) |
| Lifecycle (`type: "command"`) | "<http://compilation-metrics/command>" | COMMAND_ENDPOINT | [click here](examples/command.json) |

> **Heads up:** Rspack and Rsbuild events used to be posted to the _webpack_ endpoint despite the table above. They now go to `/rspack` as documented. If your dashboards were reading them off the webpack endpoint, point them at `/rspack` (or set `RSPACK_ENDPOINT` back to the webpack URL during the transition).

### Basic Usage: Easy as Pie (Mmm... pie 🥧)

Expand Down Expand Up @@ -106,6 +110,85 @@ Want to track bootstrap chunk sizes? We've got you covered! Pass a size limit (i
viteBuildStatsPlugin('vite-build-extraordinaire', 1000); // 1 mega byte
```

## The Whole Dev Cycle, Not Just The Compile

A compile time is one number out of the several a developer actually waits through. Between `git pull` and a working app there is an install, a dev server start, and a browser that has to finish booting. Vite and Rspack/Rsbuild now report all of them.

These arrive as a new event type, `type: "command"`, on the `COMMAND_ENDPOINT`:

| `phase` | What it measures | Emitted by |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `install` | The package manager run, plus cold/warm and whether the lockfile changed | install hooks (npm, yarn, pnpm) |
| `devserver` | Time until the dev server is listening — the number that was previously never measured, because `closeBundle` does not fire in dev | Vite, Rsbuild, Rspack watch |
| `clientready` | Time until the app is usable in the browser, with DOMContentLoaded and first contentful paint | Vite, Rsbuild |

Aborted runs count too: Ctrl-C on a dev server produces a `devserver` event with `success: false` and `signal: "SIGINT"`. A developer who gave up waiting is the most interesting data point on the chart.

### Session correlation

Every event — including the existing `webpack`, `vite`, `vitehmr`, `rspack` and `rsbuild` payloads — now carries a `sessionId`, so install → dev server ready → first HMR stitch into one timeline. It is purely additive; nothing that existed changed shape.

The session id is resolved with zero setup: a small state file under `node_modules/.cache/devfeedback` (or a tmpdir, before `node_modules` exists) that rolls over after four idle hours. If you want exact session boundaries, set one yourself and it wins:

```bash
export DEVFEEDBACK_SESSION_ID=$(uuidgen)
```

### Install timing

Two tiers, and the first one needs nothing from you.

**Default — no repo change.** `agoda-devfeedback-common` runs its own `postinstall` hook, which infers the install duration from the package manager's process start time. The span ends when our package is linked rather than when the whole install finishes, so it undercounts a little.

**Exact — two lines in the consuming repo.** Add both hooks and you get the true install span, plus a trustworthy cold/warm flag on a fresh clone:

```json
{
"scripts": {
"preinstall": "node -e \"try{require('agoda-devfeedback-common/hooks/preinstall')}catch(e){}\"",
"postinstall": "node -e \"try{require('agoda-devfeedback-common/hooks/postinstall')}catch(e){}\""
},
"devDependencies": {
"agoda-devfeedback-common": "^2.0.0"
}
}
```

Add `agoda-devfeedback-common` as a direct devDependency for this tier — under pnpm a transitive dependency is not resolvable from the repo root. When these hooks are present the bundled one stands down, so you get one event, not 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.

Known gaps, so nobody is surprised:

- Install events are **spooled, not sent**. An install must never wait on the network, so events are written to a small local NDJSON file and delivered by the next dev server or build start. `spooledAt` tells you the delivery was deferred. Nobody watches an install dashboard in real time.
- `--ignore-scripts` skips everything here.
- **pnpm blocks dependency lifecycle scripts by default.** Allow it once during dev machine bootstrap, in `~/.config/pnpm/config.yaml`, so repos stay untouched:
```yaml
allowBuilds:
agoda-devfeedback-common: true
```
(pnpm 10 and earlier call this `onlyBuiltDependencies`.)
- npm only: with `timing=true` in `.npmrc`, npm's own per-phase timers are scraped from `~/.npm/_logs/*-timing.json` and attached as `npmTimers`. That is where you find out a Playwright browser download or a `node-gyp` rebuild is what actually costs you three minutes.

### Rollout tiers

| Tier | Repo change | What you get |
| ------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------- |
| Shared preset (e.g. `@agoda/vite-config` re-exporting `viteBuildStatsPlugin()`) | none, just a version bump | everything below except exact install spans |
| Direct install | one plugin line | same |
| Exact install timing | two `scripts` lines + a direct devDependency | true install span and cold-clone accuracy |
| npm repos, opt-in | one `.npmrc` line (`timing=true`) | per-phase and per-package install breakdown |
| Advanced | `DEVFEEDBACK_SESSION_ID` in your shell | precise session boundaries |

### Staying off the critical path

Telemetry that slows people down gets deleted from configs, so:

- Every POST has a 1500 ms timeout. Off-VPN, nothing hangs.
- Git metadata is read once and cached until the repository actually changes, instead of spawning three `git` processes per HMR event.
- The session id is resolved once per process; the HMR path performs no synchronous filesystem writes and no process spawns.
- Signal handlers write synchronously and then get out of the way, so Ctrl-C behaves exactly as it would without the plugin.
- The spool is capped at 256 KB and events older than a week are dropped rather than accumulated.
- Startup chatter is behind `DEVFEEDBACK_DEBUG=1`. At the default log level the lifecycle events print nothing at all.

## The F5 Experience: Because Waiting is So Last Year

What is the F5 Experience? Have a read [here](https://beerandserversdontmix.com/2024/08/15/an-introduction-to-the-f5-experience/)
Expand All @@ -125,4 +208,4 @@ Remember, in the world of agoda-devfeedback, there are no stupid questions, only

Remember, in JavaScript development, there are only two types of projects: those that are measuring their build times, and those that are still waiting for their builds to finish. With agoda-devfeedback, you'll always know exactly how long you're waiting. (Spoiler alert: with our help, it won't be long!)

Happy coding, and may your builds be ever faster! 🚀
Happy coding, and may your builds be ever faster! 🚀
169 changes: 169 additions & 0 deletions examples/command.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
{
"install": {
"id": "9f1c2b40-1a2e-11f0-9c3a-0242ac120002",
"sessionId": "3c9f1e64-6b1c-4a0a-9e2f-8f0f7a5d1b21",
"userName": "jane.doe",
"cpuCount": 8,
"hostname": "jane-laptop",
"platform": "Darwin",
"os": "23.5.0",
"timeTaken": 48213,
"branch": "feature/checkout-redesign",
"projectName": "my-vite-project",
"repository": "https://github.com/example/my-vite-project",
"repositoryName": "my-vite-project",
"timestamp": 1785900000000,
"builtAt": "2026-08-05T03:20:00.000Z",
"totalMemory": 34359738368,
"cpuModels": ["Apple M3 Pro"],
"cpuSpeed": [0],
"nodeVersion": "v22.14.0",
"v8Version": "12.4.254.21-node.35",
"commitSha": "2b8f1c9a6e5d4c3b2a1908f7e6d5c4b3a2918070",
"customIdentifier": "install",
"type": "command",
"phase": "install",
"command": "pnpm/9.0.0 npm/? node/v22.14.0 darwin arm64",
"exitCode": 0,
"success": true,
"packageManager": "pnpm",
"packageManagerVersion": "9.0.0",
"coldInstall": true,
"lockfileChanged": true,
"measurementSource": "preinstall",
"spooledAt": 1785900000123
},

"devserver": {
"id": "a1d3e550-1a2e-11f0-9c3a-0242ac120002",
"sessionId": "3c9f1e64-6b1c-4a0a-9e2f-8f0f7a5d1b21",
"userName": "jane.doe",
"cpuCount": 8,
"hostname": "jane-laptop",
"platform": "Darwin",
"os": "23.5.0",
"timeTaken": 4120,
"branch": "feature/checkout-redesign",
"projectName": "my-vite-project",
"repository": "https://github.com/example/my-vite-project",
"repositoryName": "my-vite-project",
"timestamp": 1785900060000,
"builtAt": "2026-08-05T03:21:00.000Z",
"totalMemory": 34359738368,
"cpuModels": ["Apple M3 Pro"],
"cpuSpeed": [0],
"nodeVersion": "v22.14.0",
"v8Version": "12.4.254.21-node.35",
"commitSha": "2b8f1c9a6e5d4c3b2a1908f7e6d5c4b3a2918070",
"customIdentifier": "dev",
"type": "command",
"phase": "devserver",
"command": "vite dev",
"exitCode": 0,
"success": true,
"prebundled": true
},

"devserverAborted": {
"id": "b7a4f660-1a2e-11f0-9c3a-0242ac120002",
"sessionId": "3c9f1e64-6b1c-4a0a-9e2f-8f0f7a5d1b21",
"userName": "jane.doe",
"cpuCount": 8,
"hostname": "jane-laptop",
"platform": "Darwin",
"os": "23.5.0",
"timeTaken": 91340,
"branch": "feature/checkout-redesign",
"projectName": "my-vite-project",
"repository": "https://github.com/example/my-vite-project",
"repositoryName": "my-vite-project",
"timestamp": 1785900160000,
"builtAt": "2026-08-05T03:22:40.000Z",
"totalMemory": 34359738368,
"cpuModels": ["Apple M3 Pro"],
"cpuSpeed": [0],
"nodeVersion": "v22.14.0",
"v8Version": "12.4.254.21-node.35",
"commitSha": "2b8f1c9a6e5d4c3b2a1908f7e6d5c4b3a2918070",
"customIdentifier": "dev",
"type": "command",
"phase": "devserver",
"command": "vite dev",
"exitCode": 130,
"success": false,
"signal": "SIGINT",
"spooledAt": 1785900160500
},

"clientready": {
"id": "c3b5a770-1a2e-11f0-9c3a-0242ac120002",
"sessionId": "3c9f1e64-6b1c-4a0a-9e2f-8f0f7a5d1b21",
"userName": "jane.doe",
"cpuCount": 8,
"hostname": "jane-laptop",
"platform": "Darwin",
"os": "23.5.0",
"timeTaken": 6890,
"branch": "feature/checkout-redesign",
"projectName": "my-vite-project",
"repository": "https://github.com/example/my-vite-project",
"repositoryName": "my-vite-project",
"timestamp": 1785900066000,
"builtAt": "2026-08-05T03:21:06.000Z",
"totalMemory": 34359738368,
"cpuModels": ["Apple M3 Pro"],
"cpuSpeed": [0],
"nodeVersion": "v22.14.0",
"v8Version": "12.4.254.21-node.35",
"commitSha": "2b8f1c9a6e5d4c3b2a1908f7e6d5c4b3a2918070",
"customIdentifier": "dev",
"type": "command",
"phase": "clientready",
"command": "vite dev",
"exitCode": 0,
"success": true,
"domContentLoadedMs": 1840,
"firstContentfulPaintMs": 2210
},

"installWithNpmTimers": {
"id": "d9c6b880-1a2e-11f0-9c3a-0242ac120002",
"sessionId": "3c9f1e64-6b1c-4a0a-9e2f-8f0f7a5d1b21",
"userName": "jane.doe",
"cpuCount": 8,
"hostname": "jane-laptop",
"platform": "Darwin",
"os": "23.5.0",
"timeTaken": 51204,
"branch": "feature/checkout-redesign",
"projectName": "my-vite-project",
"repository": "https://github.com/example/my-vite-project",
"repositoryName": "my-vite-project",
"timestamp": 1785900070000,
"builtAt": "2026-08-05T03:21:10.000Z",
"totalMemory": 34359738368,
"cpuModels": ["Apple M3 Pro"],
"cpuSpeed": [0],
"nodeVersion": "v22.14.0",
"v8Version": "12.4.254.21-node.35",
"commitSha": "2b8f1c9a6e5d4c3b2a1908f7e6d5c4b3a2918070",
"customIdentifier": "install",
"type": "command",
"phase": "install",
"command": "npm install",
"exitCode": 0,
"success": true,
"packageManager": "npm",
"packageManagerVersion": "10.9.2",
"measurementSource": "npm-timing",
"npmTimers": {
"npm": 51204,
"idealTree": 8120,
"reify": 39880,
"build:run:install:node_modules/playwright": 21430,
"build:run:postinstall:node_modules/husky": 310,
"audit": 1220
},
"spooledAt": 1785900065000
}
}
8 changes: 6 additions & 2 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"./hooks/preinstall": "./dist/hooks/preinstall.cjs",
"./hooks/postinstall": "./dist/hooks/postinstall.cjs"
},
"files": [
"dist",
Expand All @@ -20,7 +22,9 @@
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"check-types": "tsc --noEmit"
"check-types": "tsc --noEmit",
"test": "vitest",
"postinstall": "node ./dist/hooks/postinstall.cjs --self || exit 0"
},
"dependencies": {
"axios": "1.8.4",
Expand Down
Loading
Loading