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.
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:sendBeaconis reliable even when the user closes the tab, unlike a regularfetch. 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? #
| Need | Minimal RUM | Faro |
|---|---|---|
| Aggregated Web Vitals | Yes | Yes |
| JavaScript errors with stack | No | Yes |
| Sessions, business events | No | Yes |
| Front-to-back traces | No | Yes |
| No individual data | Yes | Configurable |
| Components to operate | None new | Alloy, 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.
