RousselTMLEARNING

Observability

Case study: this site's minimal RUM, cookie-free, exposed to Prometheus

How this site measures its Core Web Vitals with about forty lines of JavaScript, an anonymous beacon, Redis histograms and a /metrics endpoint for Prometheus, and when to switch to Faro.

1. The need #

Measuring without trackers or cookies.

Constraints #

This site wanted to know its real Core Web Vitals without cookies, identifiers or third-party services, so that no consent would be needed for this measurement. The choice: a minimal measurement, aggregated on the server side and exposed to Prometheus.

The site RUM chain
No individual data is kept: only aggregations per page type.

2. Browser side #

PerformanceObserver and sendBeacon.

The script #

The script observes the browser's performance entries, keeps the latest value of each indicator, and sends a single beacon when the page goes to the background:

var m = {};
new PerformanceObserver(function (list) {
  list.getEntries().forEach(function (e) { m.lcp = Math.round(e.startTime); });
}).observe({ type: 'largest-contentful-paint', buffered: true });
// ... idem pour paint (FCP), layout-shift (CLS), event (INP), navigation (TTFB)

document.addEventListener('visibilitychange', function () {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/rum.php', new Blob([JSON.stringify({ p: pageType(), m: m })], { type: 'application/json' }));
  }
});
Good practice: sendBeacon is reliable even when the user closes the tab, unlike a regular fetch. The page type (home, article, lesson...) replaces the URL: no individual browsing data.

3. Server side #

Histograms for Prometheus.

Aggregation and exposure #

The endpoint validates the beacon (types and bounds), then increments histogram counters in Redis (one counter per duration bucket, per indicator and per page type). /metrics exposes them in Prometheus format:

rtm_rum_lcp_bucket{page="article",le="2500"} 1834
rtm_rum_lcp_bucket{page="article",le="4000"} 1977
rtm_rum_lcp_bucket{page="article",le="+Inf"} 2010
rtm_rum_lcp_count{page="article"} 2010
# LCP au 75e percentile par type de page
histogram_quantile(0.75, sum by (le, page) (rate(rtm_rum_lcp_bucket[1h])))
Good to know: The buckets follow the Core Web Vitals thresholds (for example 2,500 and 4,000 ms for LCP). CLS, being unitless, is multiplied by 1,000 to be stored as an integer: a 0.1 threshold becomes 100.

Faro or minimal RUM? #

NeedMinimal RUMFaro
Aggregated Web VitalsYesYes
JavaScript errors with stackNoYes
Sessions, business eventsNoYes
Front-to-back tracesNoYes
No individual dataYesConfigurable
Components to operateNone newAlloy, Loki, Tempo

For a content site, minimal RUM is enough. For an application (checkout funnel, customer area), Faro brings error diagnostics and correlation with the back end.

Related articles

Conversion funnels in RUM: measuring where users drop off

Building and reading a conversion funnel with RUM data: steps, rates, order, pitfalls, and implementation in …

Website monitoring: availability, HTTPS monitoring, certificates and performance

Set up website monitoring: availability and HTTPS checks, certificate expiry, response time, Core Web Vitals …

Real User Monitoring (RUM) and Core Web Vitals: measuring what users experience

Why observe the front end, what RUM measures, Core Web Vitals (LCP, INP, CLS) and their thresholds, and the d…

Keep learning

Support us

♥ SUPPORT US

Support RousselTM Learning

Courses, articles, labs and certificates stay free with no intrusive ads. A voluntary contribution, of any amount you choose, pays for hosting, lab environments and new content.

Contribute with PayPal Any amount · card or PayPal accountLearn more
QR code PayPal
Or scan this QR code with your phone

Thank you, every contribution counts!