Skip to content

PoC: CrUX field data — Core Web Vitals dashboard card + get_core_web_vitals MCP tool - #243

Open
GambitIsHere wants to merge 1 commit into
every-app:mainfrom
GambitIsHere:feature/crux-field-data
Open

PoC: CrUX field data — Core Web Vitals dashboard card + get_core_web_vitals MCP tool#243
GambitIsHere wants to merge 1 commit into
every-app:mainfrom
GambitIsHere:feature/crux-field-data

Conversation

@GambitIsHere

Copy link
Copy Markdown

Reference implementation for #242 — real-user Core Web Vitals from the Chrome UX Report API, gated on an optional CRUX_API_KEY (hidden when unset, mirroring the OPENROUTER_API_KEY/SAM pattern).

Per CONTRIBUTING this is a proof of concept, not a merge request — it exists to make #242 concrete and to be taken apart for inspiration.

What's inside

  • lib: cruxClient (records:queryRecord + records:queryHistoryRecord; 404 modeled as an expected no_data outcome, not an error) + leaf cruxErrors module (gscErrors pattern)
  • feature: CruxService (project domain → origin, R2-cached 24h with zod-validated reads) + pure cruxShaping helpers; shared thresholds/ratings in src/shared/cwv.ts so the client never value-imports server code
  • serverFunctions: getCruxApiKeyStatus + getCruxSnapshot
  • UI: CruxCard on the project dashboard — p75 LCP/INP/CLS tiles (lower-is-better tones), 28-day stamp, weekly-LCP trendline via the exported useChartWidth + formatDateTick (no ResponsiveContainer); four states (key missing / loading / no field data / data)
  • MCP: get_core_web_vitals (withMcpProjectAuth, defaults to the project domain, optional url + formFactor) — registered in server.ts and appended to the hardcoded /ai Available-tools list
  • self-host: crux preflight/health check (info when unset) + the four env touches (package.json bindings, compose.yaml, .env.example, env.d.ts)

Stateless: the History API supplies ~40 weeks of weekly p75s, so no tables or migrations.

Testing

  • 11 new vitest tests (lib fetch behavior incl. retry/404/403, pure shaping, MCP tool with mocked service + one outputSchema round-trip); full suite 1081 passing
  • types:check, lint (oxlint), knip, prettier --check all clean
  • Verified against the live CrUX API on a running self-host: health shows crux: ok, the card renders real data + trendline, the MCP tool returns the table below
Core Web Vitals for checkin.my-trip-online.com (PHONE, 28-day rolling to 2026-08-22):
metric | p75 | rating | good | needs improvement | poor
LCP | 2031 ms | good | 82% | 11% | 7%
INP | 207 ms | poor | 73% | 23% | 3%
CLS | 0.10 | good | 74% | 24% | 2%

Notes / open questions

  • worker-configuration.d.ts deliberately not regenerated — wrangler types output is machine/.env-dependent; the optional declaration follows the OPENROUTER_API_KEY precedent in env.d.ts.
  • Apex-domain projects that 301 to www report "no field data" (CrUX records are per-origin). A www-fallback on 404 is the obvious follow-up — happy to add if wanted.
  • Disclosure: built with AI assistance under human direction, then tested end-to-end on a live deployment.

🤖 Generated with Claude Code

… check

Real-user Core Web Vitals from the Chrome UX Report API, gated on an
optional CRUX_API_KEY (hidden when unset, like OPENROUTER_API_KEY/SAM):

- lib: cruxClient (queryRecord + queryHistoryRecord, 404 = expected
  no-data outcome) + leaf cruxErrors module
- feature: CruxService (project domain -> origin, R2-cached 24h) +
  pure cruxShaping helpers; shared CWV thresholds in src/shared/cwv.ts
- serverFunctions: getCruxApiKeyStatus + getCruxSnapshot
- UI: CruxCard on the project dashboard - LCP/INP/CLS p75 tiles
  (lower-is-better tones), 28-day stamp, weekly-LCP trendline from the
  CrUX History API (useChartWidth, no ResponsiveContainer)
- MCP: get_core_web_vitals (withMcpProjectAuth, defaults to the
  project domain) + /ai Available-tools entry
- self-host preflight/health: crux check (info when key unset)

Stateless by design: the History API supplies ~40 weeks of weekly p75s,
so no DB tables or migrations. 11 new vitest tests; test/types:check/
lint/knip/prettier all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant