Skip to content

Our incident: the web-vitals fetch bug

2 min read · main part only; folded sections are optional

After this you can answer: How can a request return 200 and still keep a page from looking idle?

After P10, four end-to-end tests on /events timed out at waitForLoadState("networkidle") against the dev server. A 15-second network trace showed 33 requests: zero browser events.search calls, but four telemetry.report calls with status 200 that stayed pending. These are the owner’s incident measurements, not a new run.

Playwright’s wait timed out; the API had accepted the telemetry. A 200 response means headers arrived, not that the response body finished. Reach succeeded; neither an IAM Allowed failure nor a Password failure fits this trace. Inspect which requests stay open before blaming event search or hydration, the step that makes server HTML interactive.

flowchart TB
A["networkidle wait times out"] --> B{"Which traffic remains?"}
B -->|"events.search repeats"| C["inspect list query lifecycle"]
B -->|"dev tooling only"| D["use a page readiness signal"]
B -->|"telemetry 200, pending"| E{"Does the hook read the body?"}
E -->|"no"| F["consume or cancel the response"]
E -->|"yes"| G["inspect response completion"]

Ranked for this trace:

  1. Unread telemetry replies: the hook ignored each fetch response body. This was the cause.
  2. Dev-server background traffic: another reason network idle can be a poor readiness signal.
  3. A list request loop: ruled out here by zero browser search calls.

Browser DevTools → Network: group by endpoint and compare status with completion. The four telemetry replies remained pending despite their 200 headers. Check the Search events region’s data-hydrated="true" marker separately.

Consume or cancel every fetch response. The hook now drains its small acknowledgement with response.text(), with a catch for failures. Replace network-idle waits with specific assertions: the hydration marker, and the expected auth response where needed.

apps/web/src/features/telemetry/hooks/use-web-vitals.ts → report

The regression in apps/web/e2e/events.spec.ts waits for hydration and a telemetry 200, then requires all tracked app requests to finish or fail. It observes 15 seconds without interaction and rejects new app requests or pending ones. It failed before the fix and passed after, according to the owner’s incident report. This bounded observation checks a quiet-page property; it is not the page’s readiness signal.

Find the web app on the map →
Advanced: Why the two checks are separate

The fix in this incident reads the body; cancellation is an alternative when the body is unwanted. Response.text() reads a response stream to completion. This incident does not establish that every unread browser response always hangs. Playwright discourages networkidle for readiness and recommends assertions about the page instead. The regression ignores hot module replacement (HMR) and static assets, tracking only paths beginning /trpc/.

Read this book online: https://book.theversehub.in/