Skip to main content
Last updated: 10 Nov 2025

govuk_publishing_components: Real User Metrics

Real user metrics (RUM) allow a user's browser to report on how a page loaded using the Performance API. The tool that we use to do this is called LUX - short for Live User Experience - and is run by SpeedCurve.

The benefit of RUM is that it shows how pages are performing in real situations, rather than on synthetic tests. This means that we don't have to guess at what conditions might be - RUM provides insight into what conditions actually are.

Loading

The LUX scripts should only be loaded when a user has opted into usage tracking. This is done using the rum-loader script, which checks if the usage cookie is true and then loads the required scripts. This is already part of the Public Layout component.

How it works

The scripts for the real user metrics are loaded from our servers - this allows us to know what is contained within the scripts and reduce the risk of unwelcome things being added to GOV.UK. Normally LUX is loaded from SpeedCurve's servers, so our setup is unusual.

The scripts

There are three scripts involved.

  • the loader is a script that we wrote. If cookies have been accepted, it finds the LUX script, attaches it to the DOM and executes it. If cookies haven't been accepted, it sets a listener for the cookie-consent event.
  • the measurer measures performance from the start of page load, but doesn't report anything. This file shouldn't ever change. The source is from the RUM dashboard, in Settings, Edit RUM, at the end. We don't use this in the way SpeedCurve recommends, because we load the scripts ourselves.
  • the reporter is the script that the loader pulls down and attaches to the DOM. This file is compiled as a separate asset and isn't included until users consent to cookies.

The reporter is the file that SpeedCurve occasionally changes (in SpeedCurve's repo it's called lux.js). Because we load our own modified copy of this script we currently have to manually download and update it. The script is audited before being updated and then two extra lines are added to make sure it works correctly. These two lines set the customer ID and set the sampling rate.

Customer ID

The customer ID is an identifier for the site using LUX, not for the user visiting the site. It won't change from page to page, or from visitor to visitor.

When loading lux.js from SpeedCurve's CDN, the customer ID is appended to the end of the URI as a query string. The script looks for a script in the DOM with a source of lux.js, and from that extracts the customer ID.

Rails adds a fingerprint to the URI which means that lux.js becomes (for example) lux.self-7137780d5344a93190a2c698cd660619d4197420b9b1ef963b639a825a6aa5ff.js and the script can't find itself. Because of this that part of the script would fail. Instead, we modify the getCustomerId() function to simply return LUX.customerid.

Because of this the customer ID needs to be set in the lux.js file:

LUX.customerid = 47044334

Sending information

LUX needs the content security policy in govuk_app_config to be configured to allow communication to LUX servers. Documentation is provided on how to configure this.

Sample rate

LUX defaults to sending every event to SpeedCurve - this can be changed by setting LUX.samplerate to a integer:

LUX.samplerate = 1 // Whole number from 1 to 100.

This then only sends 1% of visits.

This must be set at the top of the main LUX function or it will default to 100% sample rate.

Debugging (bonus mode)

LUX.debug = true

Debug is turned off by default - setting this to true it will log what LUX is doing in the browser's console.

Usefully, running LUX.getDebug() in the browser's console will show the history of what's happened whether debug is true or false:

LUX.getDebug()

Sampling can also be forced by placing LUX.forceSample() at the end of the file:

LUX.forceSample()

How to update to a new version

Being notified

2nd line have an icinga alert set up to tell us when a new version of LUX is available. This checks the live LUX script for a variable called SCRIPT_VERSION. Since the file is minified it is actually looking for the pattern V=[version_number], where version_number is currently 302. If it does not match this, an alert is triggered.

The icinga alert was originally added in this PR, for reference.

It's worth enabling your notifications for this repo in case the icinga alert fails.

Getting the new code

Follow the steps below. You can also build the version of lux.js that's currently on GOV.UK by using the source code on their releases page. You can then use this to run a diff check of our version against the new version. If the changes are small enough, you could decide to just manually copy and paste the changed lines to our lux-reporter.js file as a simple way to update LUX.

  • locally clone the lux.js repo from the SpeedCurve github repo
  • inside the local copy, run npm install && npm run build to create a JavaScript version of the file (see repo README for details)
  • open the generated file dist/lux.js
  • open the current file lux-reporter.js
  • copy the changes across, see below for what needs to be preserved (might be easiest to make a new file, copy the generated code into it, then update with the relevant bits from the current file, before overwriting it)
  • fix the indenting to match the current file (to minimise changes when reviewing)

Make sure the new code has the following things from the current file:

  • the comment at the top
  • the comment at the end
  • the 'settings' and their associated comment, which comes immediately after the line LUX = (function () { (includes LUX.customerid and LUX.samplerate)
  • update the getCustomerId function to simply return the customerid, see this PR for related information

Testing

You can test the changes locally to ensure the file has been updated correctly.

  • temporarily set the debug option in the reporter to true for testing purposes
  • load a local application pointed at your local components gem, and accept cookies
  • paste this command into the browser console: copy(window.LUX.getDebug()) (it'll say undefined but the output will be in your copy/paste buffer)
  • visit the SpeedCurve debugging tool and paste the output into it, and check the output for errors

If you're using docker to run the application, your local server will have a dev.gov.uk domain name and it is possible to set up SpeedCurve to see the data from it. You will need to uncomment the forceSample line to make this work.

Check the changelog

It's worth checking the SpeedCurve changelog to see what has been updated with this latest release in case there's anything we want to take advantage of, anything we need to configure in our dashboards, or anything that could be an issue that we should discuss before deploying.

Protocol measuring

We've added an extra bit to the end of the measurer script to determine what protocol is in use - HTTP 1, 2 or 3. This shouldn't need any updating as the measurer shouldn't change.