Recipe: Real-user monitoring (RUM) design

A lightweight, privacy-first RUM pipeline that captures Core Web Vitals, custom timings, and error telemetry from real user sessions without third-party bloat.

Architecture

A tiny (<2 kB) inline script bootstraps the PerformanceObserver and Long Animation Frames API. Metrics are batched every 5 s or on visibilitychange and POSTed to a Vercel Edge Function that writes to Upstash KV. The dashboard queries KV with percentile rollups.

Ingestion endpoint

POST /api/rum accepts a JSON array of events. Each event carries a session id, metric name, value, and optional tags. The edge handler validates the schema, drops duplicates via a bloom filter in KV, and appends to a per-metric sorted set.

Client snippet

const o = new PerformanceObserver((list) => {
  list.getEntries().forEach((e) => buffer.push({
    n: e.name, v: e.startTime, t: performance.now()
  }));
});
o.observe({ type: 'largest-contentful-paint', buffered: true });
o.observe({ type: 'layout-shift', buffered: true });
o.observe({ type: 'long-animation-frame', buffered: true });

Dashboard queries

The dashboard calls /api/rum/stats with a time range and percentile (p50, p75, p95). The edge function runs ZRANGEBYSCORE with a Lua script that computes percentiles in-place, returning a single JSON payload under 50 ms cold start.

Privacy & compliance

  • No cookies, no fingerprinting, no IP storage.
  • Session id is a random UUID generated on first visit, stored in sessionStorage.
  • All data expires from KV after 30 days via TTL.
  • GDPR-friendly — no PII ever leaves the browser.