HarperFast / HarperFast/studio
[RUM] Core Web Vitals collection is dead — LCP/FCP dropped 55% → 0% of initial loads around 2026-07-04, blinding #1405
- Dominant language
- TypeScript
- Stars
- 5
- Forks
- 4
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 40
Description
## Summary
**Studio has not reported a single LCP or FCP measurement in over three weeks.** Coverage of `initial_load` views collapsed from **~55%** to **exactly 0%** around **2026-07-04** and has stayed there.
This means #1405 (open — "LCP performance regression: '/' view degraded +61.7%, 4320ms p75") can no longer be measured, confirmed, or closed, and no future LCP/FCP regression can be detected at all.
Surfaced by the automated daily RUM review (app `f590deee-…`).
## Datadog findings
Coverage = share of `@type:view @view.loading_type:initial_load` events carrying the metric:
| Metric | ~2026-06-26 → 06-30 | ~2026-07-04 → 07-06 | last 24h |
| --- | --- | --- | --- |
| `@view.largest_contentful_paint` | **85/156 = 54.5%** | 0/61 = **0.0%** | 0/160 = **0.0%** |
| `@view.first_contentful_paint` | **89/156 = 57.1%** | 0/61 = **0.0%** | 0/160 = **0.0%** |
| `@view.dom_complete` | 98/156 = 62.8% | 5/61 = 8.2% | 39/160 = **24.4%** |
| `@view.loading_time` | 0/156 = 0.0% | 0/61 = 0.0% | 0/160 = 0.0% |
`@view.first_input_delay` and `@view.interaction_to_next_paint` also return no data.
### The signature is precise
**`dom_complete` still works. LCP and FCP are exactly zero.**
`dom_complete` comes from `PerformanceNavigationTiming`; LCP and FCP come from `PerformanceObserver` entries. Views are still being created and still carry navigation timings — so this is not a sampling, ingestion, or view-tracking outage. Specifically the **paint-observer metrics** stopped reaching the view.
A "0 out of 160" is not sparsity: at the old ~55% rate we'd expect ~88.
### Narrowing the break
Two-day buckets put the transition between **~2026-07-02→07-04 (39.8% coverage)** and **~2026-07-04→07-06 (0.0%)**:
| window (rel. to 2026-07-28) | initial loads | with LCP | pct |
| --- | --- | --- | --- |
| 32d → 28d | 156 | 85 | 54.5% |
| 28d → 26d | 287 | 152 | 53.0% |
| 26d → 24d | 98 | 39 | 39.8% |
| **24d → 22d** | 60 | 0 | **0.0%** |
| 22d → 20d | 298 | 6 | 2.0% |
| 18d → 16d | 112 | 1 | 0.9% |
| 14d → 12d | 263 | 6 | 2.3% |
| 8d → 6d | 252 | 3 | 1.2% |
(The residual 1–2% after the break is a handful of stragglers, presumably long-lived tabs on older bundles.)
**Not the SDK.** `@datadog/browser-rum` resolved to 7.4.0 on both sides of that boundary (7.2.0 Jun 19 → 7.3.0 Jun 22 → 7.4.0 Jun 29 → 7.5.0 Jul 13 → 7.6.1 Jul 27). Prod is currently on `sdk_version:7.5.0`. The break predates the 7.5.0 bump by ~10 days.
## Likely mechanism
`src/integrations/datadog/datadog.ts` initializes with **`trackViewsManually: true`**, then starts a view twice during startup:
1. `useDatadog` (called from `src/App.tsx`) inits RUM **inside a `useEffect`** — i.e. after React mounts — and on `onReady` calls `startView({ name: window.location.pathname || 'initial' })`.
2. `useOnRouteLoadTracker` (called from `src/components/StudioCloud.tsx`) then calls `startView` again with the translated route name, on an effect keyed `[location.href, router]`.
With `trackViewsManually`, Datadog attributes LCP/FCP to the **initial view**, and stops collecting once that view ends. If the second `startView` replaces the initial view before the browser settles the largest paint, both metrics are dropped — which matches the data exactly (paint observers zeroed, navigation timings unaffected).
Two commits in the bisect window plausibly changed that startup timing:
- **`4f660718` (2026-07-03) `fix(router): create the TanStack router once, not on every AppRouted render`** — the closest date match, and it directly changes the `router` identity that `useOnRouteLoadTracker`'s effect is keyed on, hence when the second `startView` fires. (Context in `CLAUDE.md` under "the router must be created exactly once".)
- **`7c5e9141` (2026-07-06) `fix(auth): subscribe useRootAuthenticationContext once, not per change`** — also changes render/effect cadence around startup.
I want to be clear this is a **hypothesis, not a proven cause** — the correlation is by date and mechanism, and I could not reproduce paint-metric collection locally (it needs a production build and real page loads).
## Suggested next steps
1. **Confirm the mechanism** with a production-mode build: init RUM, log `datadogRum.getInternalContext()` view ids across the two `startView` calls, and check whether the initial view ends before LCP settles.
2. **Don't replace the initial view.** Either drop the `startView` in `useDatadog` and let `useOnRouteLoadTracker` name the first view, or keep the initial view and only `startView` on *subsequent* route changes. One view per page load at startup, not two.
3. **Consider initializing RUM outside a `useEffect`** — at module scope in the entry, before React mounts — so the SDK's observers attach as early as possible.
4. **Re-evaluate `trackViewsManually: true`.** If it's only there for the friendly route names, `@datadog/browser-rum-react`'s router instrumentation may give both naming and correct vitals.
5. **Add a guard against silent loss:** a Datadog monitor on LCP sample count per day would have caught this three weeks ago. Right now nothing alerts when a metric simply stops arriving.
6. Once fixed, revisit **#1405** — its p75 baseline predates the break, so it needs fresh data before it can be judged.
## Related
- **#1405** (open) — the LCP regression this blinds. Depends on this being fixed first.
- **#1377** (closed) — earlier page-load-time spike investigation, same metric family.
_Filed by the automated daily Datadog RUM review. No PII or customer identifiers included._
Contributor guide
Assessment
This issue has not been assessed yet.