Back to path
ExpertPulse · Project 11 of 12 ~11h· 5 milestones

Instrument it: Web Vitals, errors, and budget gates

Continues from the last build: an installable app with no production visibility

Pulse installs like a native app now, works offline, and the icon sits on people's home screens.

web-vitals field measurementINP attribution debuggingSentry SDK setup and release taggingsource map upload pipelinesReact error boundariesLighthouse CI budgetsGitHub Actions status checksRUM data interpretation

What you'll build

Pulse beacons INP, LCP, and CLS from real sessions with attribution detail, reports every client error to Sentry with de-minified stacks and release tags, catches render crashes in a reporting error boundary with a recoverable fallback, and fails CI automatically when a PR regresses the performance budget.

See how we teach, before you sign up

You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:

src/lib/vitals.tsts
import { onCLS, onLCP, onINP, type Metric } from 'web-vitals';

const VITALS_ENDPOINT = '/api/vitals';

function sendToAnalytics(metric: Metric): void {
  const payload = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
    rating: metric.rating,
  });

  fetch(VITALS_ENDPOINT, {
    method: 'POST',
    body: payload,
    keepalive: true,
  });
}

// TODO: this fires once on window 'load', which misses INP entirely
// because interaction latency is only known after the user acts.
window.addEventListener('load', () => {
  onCLS(sendToAnalytics);
  onLCP(sendToAnalytics);
  onINP(sendToAnalytics);
});

Reading this file

  • const VITALS_ENDPOINT = '/api/vitals';The mocked beacon target. In MSW this resolves to a handler that just 200s and logs, so you can see beacons land in dev.
  • fetch(VITALS_ENDPOINT, { method: 'POST', body: payload, keepalive: true, });keepalive lets the request survive the page unloading, but sendBeacon is more reliable for this exact case and does not need a keepalive flag.
  • window.addEventListener('load', () => { onCLS(sendToAnalytics); onLCP(sendToAnalytics); onINP(sendToAnalytics); });This is the bug you are here to fix: registering the vitals callbacks inside 'load' means any interaction the user makes before or shortly after load can be missed or under-attributed for INP.
  • function sendToAnalytics(metric: Metric): void {One callback shared by all three metrics keeps the wire format consistent, which matters once a backend actually parses these.

Starter beacons vitals but reports on the load event, which under-measures INP because interactions after load never get attributed.

That's 1 of 7 explained code blocks in this single project.

The build, milestone by milestone

  1. 1

    Beacon field Web Vitals with correct INP attribution

    5 guided steps

    INP is defined by the worst interaction over the page's lifetime, which can happen seconds or minutes after load. If you only register the callback once 'load' fires, you can miss early interactions and you conflate 'page finished loading' with 'user finished interacting', giving you a number that looks great in your dashboard and matches nothing users actually feel.

  2. 2

    Wire Sentry with release tagging and real source maps

    6 guided steps

    A minified stack trace without a matching source map tells you a function threw somewhere in a 400KB bundle. A release tag is what lets Sentry pick the exact source map for the exact build that shipped, so instead you get a file name, a line number, and the actual variable names.

  3. 3

    Build a React error boundary that actually reports

    5 guided steps

    An error boundary without reporting just hides the crash from you while still showing it to the user as a blank panel. The point of the boundary is not just 'do not show a white screen', it is 'catch it, tell me about it, and give the user a way back'.

  4. 4

    Fail a PR when it regresses the performance budget

    5 guided steps

    A performance budget that only produces a report nobody reads is not a budget, it is a dashboard. The gate only works if a regression fails the build the same way a broken test does, which means someone has to actually configure assert rules with real thresholds and required: true.

  5. 5

    Verify the whole instrumentation chain end to end

    5 guided steps

    Each of the last four milestones can look done in isolation while still failing in combination, for example the boundary reports fine but the release tag is wrong so the stack is still minified. This milestone is the integration check that catches that.

What's inside when you start

3 starter files, ready to clone
5 guided milestones
4 full reference solutions
7 code blocks explained line-by-line
5 "is it working?" checks
4 interview questions it prepares you for

You'll walk away with

web-vitals wired at module init (not on load) beaconing INP/LCP/CLS with attribution via sendBeacon
Sentry initialized with release tagging and source maps uploaded on every build
A reporting React error boundary wrapping the app root and the segment builder, with a working retry action
A Lighthouse CI workflow with real assert thresholds set as a required GitHub branch protection check
A short internal note documenting the three new endpoints (vitals, Sentry, Lighthouse CI) for teammates

This is portfolio-grade. Build it free.

Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.

Start building