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.