ESC

Type to search the knowledge base.

Error Monitoring and RUM Design

Frontend system design for error monitoring and RUM — capture, sampling, privacy, source maps, and alerting.

intermediate4 min read
  • system-design
  • interview
  • architecture
  • observability
  • rum

Scope the problem

In scope: browser error capture, performance RUM (Core Web Vitals), event pipelines, privacy, sampling, dashboards/alerts for frontend teams.

Out of scope: full APM backend storage engine design.

Goals

Goal Example
Detect spike in JS exceptions after deploy
Diagnose stack + release + user path
Performance LCP/INP/CLS by route
Privacy no PII/secrets in payloads

Architecture

Browser SDK
  ├─ window.onerror / unhandledrejection
  ├─ Framework error boundaries hook
  ├─ PerformanceObserver (vitals)
  └─ transport (batch + beacon)
        ↓
Ingest API → process (symbolicate) → store → alert + UI

Error capture

window.addEventListener("error", (e) => {
  report({
    type: "error",
    message: e.message,
    stack: e.error?.stack,
    release: process.env.RELEASE,
    route: location.pathname,
  });
});

window.addEventListener("unhandledrejection", (e) => {
  report({ type: "unhandledrejection", reason: String(e.reason) });
});

React: error boundary componentDidCatch → report + fallback UI.

Grouping

Server fingerprints by:

  • top stack frames (normalized)
  • message
  • release

Avoid grouping by full URL query strings (cardinality explosion).

Source maps

  • Upload source maps at build for each release
  • Never expose maps publicly if they reveal sensitive code (authenticated symbolication)
  • Collapse framework frames for readability

RUM / Web Vitals

import { onLCP, onINP, onCLS } from "web-vitals";

onLCP((m) => sendVital("LCP", m));
onINP((m) => sendVital("INP", m));
onCLS((m) => sendVital("CLS", m));

Dimensions: route, device class, country, connection (effectiveType), app version.

Custom marks: login_to_interactive, checkout_step_2.

Transport & reliability

Concern Approach
Batching queue N events or T ms
Unload sendBeacon / fetch(keepalive)
Failures drop oldest; never block UI
Ad blockers first-party ingest domain proxy

Sampling

  • Errors: often 100% (or rate-limit per session)
  • Traces/vitals: 1–20% depending on traffic
  • Head sampling vs tail (keep slow sessions) if supported

Rate-limit noisy errors (network offline loops).

Privacy & security

Scrub before send:

  • emails, tokens, Authorization headers
  • form field values
  • query params like ?token=

Consent mode: disable RUM until accepted where required. Document retention.

Client SDK design

init({
  dsn: "...",
  release: "web@1.2.3",
  environment: "prod",
  sampleRate: 0.1,
  beforeSend(event) {
    return scrub(event);
  },
});

Integrate with feature flags/release to mark regressions.

Alerting

  • Error rate > baseline + stddev for release
  • New fingerprint in first hour of deploy
  • LCP p75 regression on /checkout

Page owners via route ownership map.

Performance of the SDK itself

  • Tiny async load; don’t block LCP
  • Avoid heavy stack parsing on main thread
  • Prefer sampling over capturing huge DOM snapshots by default

Tradeoffs

  1. Full session replay vs privacy/cost
  2. First-party proxy vs SaaS default endpoint
  3. 100% errors vs ingest cost
  4. Verbose context vs PII risk

Interview close

SDK capture points → scrubbing/sampling → release + source maps → vitals by route → alerts on deploy regression. Mention beacon reliability and ad blockers.

Release health workflow

  1. Tag every deploy with release + git SHA
  2. Compare error rate and CWV p75 for 1–2 hours post-deploy against previous release
  3. Auto-page on new fingerprint volume or LCP regression on money routes
  4. Attach replay/trace only for sampled sessions to control cost

What “good” looks like in an interview

Draw the SDK → ingest → symbolicate → alert loop, then spend time on scrubbing, sampling, and release correlation. Mention that console.error wrapping and network instrumentation are optional layers, not the core.

Framework-specific notes

  • React: error boundaries don’t catch event-handler errors — still need global handlers
  • Vue/Svelte: equivalent app-level hooks
  • Route-aware tagging beats “one giant project bucket” for ownership

Cost control

Session replay, full request bodies, and 100% vitals will dominate invoices at scale. Default to: all fatal errors, sampled vitals, scrubbed breadcrumbs, no raw passwords/PII ever.

Further reading