feat(perf): #22 wire Web Vitals through GoogleAnalytics + consolidate reporters#78
Merged
TortoiseWolfe merged 1 commit intomainfrom May 6, 2026
Merged
Conversation
… reporters Closes #22 Mobile-First Design's perf-test gap by activating Web Vitals instrumentation that already half-existed in the codebase. Two parallel implementations were unifying onto one consent-aware path: - src/utils/performance.ts had initWebVitals() using the npm web-vitals package, but it was dead code — nobody imported it. Its sendToAnalytics bypassed the consent-aware analytics.ts pipeline and called gtag directly. - src/utils/web-vitals.ts hand-rolled ~280 LOC of PerformanceObserver logic that duplicated the npm package, only consumed by /status diagnostic page. Consolidation: - src/utils/web-vitals.ts is now a thin re-export of onCLS/onFCP/onINP/ onLCP/onTTFB/Metric from the npm web-vitals package. Public API unchanged so /status keeps working. Drops ~280 LOC of bespoke timing code in favor of battle-tested package implementation. - src/utils/performance.ts initWebVitals now subscribes through that re-export and dispatches via the consolidated sendToAnalytics, which routes through analytics.ts trackWebVital (consent-gated). - src/lib/analytics/GoogleAnalytics calls initWebVitals once when consent.analytics flips on. Ref-guarded so consent toggle off-then-on doesn't double-subscribe. - src/types/analytics.types.ts WebVitalEvent.metric_name drops the 'FID' literal — Chromium replaced FID with INP in 2024, the web-vitals ^5 package no longer exports onFID. Also closes the wireframe sub-task: features/foundation/004-mobile- first-design/wireframes/{01,02}.issues.md were stale (Jan 2026 validator v5.0/v5.2 entries). Re-validated against current v5.4 — both SVGs PASS with no errors. The audit's regen claim was stale; updating the issues files records the re-validation rather than running speculative regeneration. GA4 acceptance criteria from features/foundation/004-mobile-first- design/spec.md: - AC-1: LCP under 2.5s on mobile — measured (not enforced) - AC-2: FID under 100ms — replaced by INP under 200ms - AC-3: CLS below 0.1 — measured (not enforced) Verification: - pnpm run type-check: clean - pnpm run lint: clean - pnpm test: 3255/3255 pass (7 new web-vitals tests; consolidation test surface includes the consent-gating contract, the no-FID-regression pin, and the no-pre-scaling CLS contract) Why this is a Phase 0 ticket (per gleaming-kitten plan): GrimGlow Phase 1a is browser R3F on top of ScriptHammer. Three.js scenes notoriously degrade Core Web Vitals (large LCP from canvas mount, CLS from late-bound geometry, INP from main-thread shader compilation). Landing the measurement infrastructure in template means every fork inherits it for free instead of re-deriving. Closes #22 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TortoiseWolfe
added a commit
that referenced
this pull request
May 6, 2026
Phase 0 (template hygiene for forks) closed this session via 5 PRs: - #77 (#24) SetupBanner clarity - #70 (#69) Docker volume permissions - #78 (#22) Mobile-First Web Vitals instrumentation - #79 (#73) Constitution v1.0.2 wireframe gate - #81 (#21) WCAG AAA scope upgrade STATUS.md updates: - Snapshot date: 2026-04-27 → 2026-05-06 - Stability blurb: rewritten to reflect Phase 0 closure - 001 WCAG: [~] → [x] AAA standard, ContactForm green, overlay → #80 - 004 Mobile-First: [~] → [x] Web Vitals through GoogleAnalytics - 006 Template Fork: [~] → [x] SetupBanner + Docker volume fix - 015 OAuth Display Name: [~] → [x] full cascade with GitHub fixtures - Summary table: Shipped count 18 → 22 Inventories regenerated via scripts/refresh-inventories.py: - skill-index.md (22 items) - workflow-status.md (7 items) - security-touchpoints.md (45 items) - screen-inventory.md (25 items) Pure documentation; no code changes. Co-authored-by: TurtleWolfe <TurtleWolfe@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #22 (Mobile-First Design perf-test gap) by activating Web Vitals instrumentation that already half-existed in the codebase. Two parallel implementations consolidated onto one consent-aware path. Net −95 LOC (drops ~280 LOC of hand-rolled
PerformanceObserverlogic in favor of the battle-tested npm package).Why this is a Phase 0 ticket (GrimGlow ThreeJS prep)
GrimGlow Phase 1a is browser R3F on top of ScriptHammer. Three.js scenes notoriously degrade Core Web Vitals — large LCP from canvas mount, CLS from late-bound geometry, INP from main-thread shader compilation. Landing the measurement infrastructure in template means every fork inherits it for free instead of re-deriving.
What changed
src/utils/web-vitals.tsis now a thin re-export ofonCLS/onFCP/onINP/onLCP/onTTFB/Metricfrom the npmweb-vitalspackage. Public API unchanged so/statusconsumer keeps working. Drops ~280 LOC of bespoke timing logic.src/utils/performance.tsinitWebVitals()was previously dead code; now it actually runs. Subscribes through the re-export and dispatches via consolidatedsendToAnalytics, routed through the consent-gatedanalytics.tstrackWebVital.src/lib/analytics/GoogleAnalyticscallsinitWebVitals()once whenconsent.analyticsflips on. Ref-guarded so consent toggle off-then-on doesn't double-subscribe.src/types/analytics.types.tsWebVitalEvent.metric_namedrops the'FID'literal — Chromium replaced FID with INP in 2024;web-vitals^5 no longer exportsonFID.Wireframe sub-task
The audit also asked to "regenerate wireframes" for this feature. Re-validated both SVGs against the current validator v5.4 — both PASS with no errors. The audit's regen claim was stale (issues files dated Jan 2026 against validator v5.0/v5.2). Updated the issues files to record the re-validation rather than running speculative regeneration.
GA4 acceptance criteria (from
features/foundation/004-mobile-first-design/spec.md)These are observation thresholds, not fix-it requirements — the deliverable is measurement infrastructure, not optimization.
Tests
7 new tests in
src/utils/web-vitals.test.ts:reportWebVitalssubscribes to all 5 metricsonFIDre-introduction (web-vitals ^5 dropped it)sendToAnalyticsconsent-gating contract (forwards when enabled, suppresses when disabled)sendToAnalyticsdoes NOT pre-scale CLS (the older bespoke implementation did; the consolidated path delegates rounding totrackWebVitalto avoid double-scaling)Verification
pnpm run type-check: cleanpnpm run lint: cleanpnpm test: 3255/3255 pass (7 new tests, full suite green)Test plan
NEXT_PUBLIC_GA_MEASUREMENT_IDset, accept consent → DevTools Network → filterg/collect→ seeLCP/INP/CLSevent payloads after the first navigationinitWebVitalsshort-circuits gracefully (no errors)Phase 0 status after this lands
After #73 + #21 close, Phase 0 is closed and Phase 0.5 (#48 Three.js Game) becomes the next real work.
Closes
Closes #22
🤖 Generated with Claude Code