HarperFast / HarperFast/studio

[RUM] Core Web Vitals collection is dead — LCP/FCP dropped 55% → 0% of initial loads around 2026-07-04, blinding #1405

Open
#1,570 3 comments 0 reactions 1 assignee Claimed by @dawsontoth View on GitHub
rum
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.