DocsRecipes

Recipes

Real-user monitoring and Web Vitals

5 min readReviewed July 2026

Real-user monitoring measures performance as people actually experience it across devices, networks, routes, releases, and account contexts. Laboratory tests remain useful, but they cannot represent every real session.

GraphJSON does not provide a browser SDK, automatic performance observer, session replay, distributed tracing, or a managed RUM product. Collect approved browser measurements through your own first-party endpoint, then use GraphJSON for event analysis, dashboards, and compatible aggregate alerts.

Start with Browser and mobile collection for endpoint security and consent.

Define the navigation grain#

One measurement should identify one page navigation or route transition:

{
  "event": "web_navigation_measured",
  "navigation_id": "nav_01J...",
  "session_id": "sess_42",
  "route_template": "/reports/:report_id",
  "navigation_type": "spa_route",
  "release": "web-2026.07.26.1",
  "device_class": "mobile",
  "network_class": "4g",
  "visibility_state": "visible",
  "measurement_version": 3
}

Use a route template, not the full URL. Paths and query strings can contain customer IDs, search text, tokens, and personal data.

Generate a stable navigation_id before collecting metrics so several metric events can be joined without using a URL.

Collect the user-centric metrics#

Core Web Vitals currently focus on:

  • Largest Contentful Paint (LCP)
  • Interaction to Next Paint (INP)
  • Cumulative Layout Shift (CLS)

Use the browser’s supported performance APIs or the maintained web-vitals library. Preserve the metric’s rating and library or policy version rather than hard-coding thresholds in every query.

{
  "event": "web_vital_measured",
  "navigation_id": "nav_01J...",
  "session_id": "sess_42",
  "route_template": "/reports/:report_id",
  "metric": "INP",
  "value": 184,
  "unit": "ms",
  "rating": "good",
  "metric_id": "v3-173...",
  "metric_definition_version": "web-vitals-5",
  "release": "web-2026.07.26.1",
  "device_class": "mobile"
}

Do not combine milliseconds and unitless CLS values in one numeric series.

Send metrics at the right lifecycle point#

Some metrics can change during the page lifetime. Send the final value when the page becomes hidden, the route changes, or the library reports a finalized measurement.

Browsers can terminate ordinary requests during navigation. Send to your first-party endpoint with sendBeacon or an appropriate keepalive request, and persist a bounded retry locally only when consent and data policy allow it.

Do not send directly to GraphJSON from the browser; that would expose the workspace API key.

Distinguish navigation outcomes#

Performance of successful pages alone is survivorship-biased. Model:

  • navigation started
  • content became usable
  • navigation failed
  • JavaScript error prevented completion
  • request timed out
  • user abandoned before readiness
{
  "event": "web_navigation_completed",
  "navigation_id": "nav_01J...",
  "route_template": "/reports/:report_id",
  "outcome": "failed",
  "failure_class": "data_request_timeout",
  "milliseconds_to_terminal": 8000,
  "release": "web-2026.07.26.1"
}

Do not assign a fast performance value to a page that never rendered.

Handle SPAs deliberately#

For a client-rendered route:

  1. create a new navigation ID when route work begins
  2. record the normalized route template
  3. observe route-specific loading and rendering
  4. finalize the prior route’s pending metrics
  5. attach downstream product outcomes to the active navigation

Browser navigation timing describes the document load, not every SPA transition. Define an application-specific “route usable” terminal event for client-side navigations.

Avoid counting hash changes, tab state, or background prefetches as page navigations unless they represent a user-visible transition.

Account for visibility and lifecycle#

Background tabs can produce misleading durations. Capture visibility state and exclude or separate:

  • pages loaded in the background
  • prerendered pages never activated
  • restored back-forward cache navigations
  • sessions suspended on mobile
  • long tasks after the user left

Do not discard these automatically; some are useful populations. Keep their semantics explicit.

Sample deterministically#

High-volume diagnostic events may be sampled, but keep navigation failures and rare critical outcomes exact.

Hash a stable unit:

  • session_id to retain complete session performance
  • navigation_id for navigation-level estimates
  • account_id for full account histories

Include the sample rate, method, and version in every sampled event. Follow Sampling and statistical uncertainty.

Do not sample each Web Vital independently; that can create incompatible metric populations.

Calculate percentiles at the correct grain#

First select one final value per metric and navigation:

WITH final_values AS (
  SELECT
    JSONExtractString(json, 'navigation_id') AS navigation_id,
    JSONExtractString(json, 'metric') AS metric,
    argMax(JSONExtractFloat(json, 'value'), timestamp) AS value,
    argMax(JSONExtractString(json, 'route_template'), timestamp) AS route_template,
    argMax(JSONExtractString(json, 'device_class'), timestamp) AS device_class
  FROM web_performance
  WHERE JSONExtractString(json, 'event') = 'web_vital_measured'
  GROUP BY navigation_id, metric
)
SELECT
  route_template,
  device_class,
  metric,
  count() AS measured_navigations,
  quantileExact(0.75)(value) AS p75_value
FROM final_values
GROUP BY route_template, device_class, metric
ORDER BY measured_navigations DESC

Do not average per-route percentiles to create a site percentile. Calculate the percentile over the underlying navigation population.

Show coverage beside performance. Missing metrics can vary by browser support, page lifetime, consent, and instrumentation failure.

Correlate with product outcomes carefully#

Attach navigation_id or session_id to later facts:

{
  "event": "report_exported",
  "account_id": "acct_7",
  "navigation_id": "nav_01J...",
  "result": "success"
}

Then compare eligible conversion or completion by a prior performance band. Slow experiences may reduce success, but slow routes can also be inherently more complex. Control for route, device, geography, account plan, and release.

Call the result an association unless a controlled intervention tests the performance change.

Compare releases#

Attach the deployed release to every navigation and metric. Compare:

  • identical route templates
  • equivalent device and network populations
  • mature post-release windows
  • failure and measurement coverage
  • p50, p75, and upper-tail values
  • product guardrails

Use Release-impact analytics for deployment facts, rollback, and fixed before-and-after windows.

Protect privacy and cardinality#

Exclude:

  • full URLs and query strings
  • DOM text and element contents
  • selectors containing customer data
  • raw IP addresses
  • detailed device fingerprints
  • request or response bodies
  • stack traces with secrets

Use bounded route templates, error classes, device classes, connection classes, and release IDs. High-cardinality resource names and individual element selectors rarely belong on dashboard legends.

Build the RUM dashboard#

Show:

  1. eligible and measured navigations
  2. LCP, INP, and CLS distributions
  3. navigation failure and abandonment rates
  4. route usability time for SPA transitions
  5. results by route, device, network, and release
  6. measurement coverage and sampling policy
  7. performance-associated product outcomes
  8. regressions after deployment
  9. pipeline freshness

Keep laboratory and real-user measurements on separate series.

Launch checklist#

  • Browser data flows through a secured first-party endpoint.
  • Navigation IDs and route templates contain no sensitive URL data.
  • Each metric preserves unit and definition version.
  • Final values deduplicate by navigation and metric.
  • Failed and abandoned navigations remain visible.
  • SPA transitions have an application-defined usability boundary.
  • Visibility, background, and cache-restoration states are explicit.
  • Sampling is deterministic and documented.
  • Percentiles use the underlying navigation population.
  • Release comparisons use equivalent traffic.
  • Product associations are not presented as causal proof.
Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support