Migrate to User-Agent Client Hints

Strategies to migrate your site from relying on the user-agent string to the new User-Agent Client Hints.

The User-Agent string is a significant passive fingerprinting surface in browsers, as well as being difficult to process. However, there are all kinds of valid reasons for collecting and processing user-agent data, so what's needed is a path to a better solution. User-Agent Client Hints provide both an explicit way to declare your need for user-agent data and methods to return the data in an easy-to-use format.

This article will take you through auditing your access to user-agent data and migrating user-agent string usage to User-Agent Client Hints.

Audit collection and use of user-agent data

As with any form of data collection, you should always understand why you are collecting it. The first step, regardless of whether or not you will be taking any action, is to understand where and why you are using user-agent data.

If you don't know if or where user-agent data is being used, consider searching your front-end code for use of navigator.userAgent and your back-end code for use of the User-Agent HTTP header. You should also check your front-end code for use of already deprecated features, such as navigator.platform and navigator.appVersion.

From a functional point of view, think about anywhere in your code where you are recording or processing:

  • Browser name or version
  • Operating system name or version
  • Device make or model
  • CPU type, architecture, or bitness (for example, 64-bit)

It's also likely that you may be using a third-party library or service to process the user-agent. In this case, check to see if they are updating to support User-Agent Client Hints.

Are you only using basic user-agent data?

The default set of User-Agent Client Hints includes:

  • Sec-CH-UA: browser name and major/significant version
  • Sec-CH-UA-Mobile: boolean value indicating a mobile device
  • Sec-CH-UA-Platform: operating system name
    • Note that this has been updated in the spec and will be reflected in Chrome and other Chromium-based browsers shortly.

The reduced version of the user-agent string that is proposed will also retain this basic information in a backwards-compatible way. For example, instead of Chrome/90.0.4430.85 the string would include Chrome/90.0.0.0.

If you are only checking the user-agent string for browser name, major version, or operating system, then your code will continue to work though you are likely to see deprecation warnings.

While you can and should migrate to User-Agent Client Hints, you may have legacy code or resource constraints that prevent this. The reduction of information in the user-agent string in this backwards-compatible way is intended to ensure that while existing code will receive less detailed information, it should still retain basic functionality.

Strategy: On-demand client-side JavaScript API

If you are currently using navigator.userAgent you should transition to preferring navigator.userAgentData before falling back to parsing the user-agent string.

if (navigator.userAgentData) {
  // use new hints
} else {
  // fall back to user-agent string parsing
}

If you are checking for mobile or desktop, use the boolean mobile value:

const isMobile = navigator.userAgentData.mobile;

userAgentData.brands is an array of objects with brand and version properties where the browser is able to list its compatibility with those brands. You can access it directly as an array or you may want to use a some() call to check if a specific entry is present:

function isCompatible(item) {
  // In real life you most likely have more complex rules here
  return ['Chromium', 'Google Chrome', 'NewBrowser'].includes(item.brand);
}
if (navigator.userAgentData.brands.some(isCompatible)) {
  // browser reports as compatible
}

If you need one of the more detailed, high-entropy user-agent values, you will need to specify it and check for the result in the returned Promise:

navigator.userAgentData.getHighEntropyValues(['model'])
  .then(ua => {
    // requested hints available as attributes
    const model = ua.model
  });

You may also want to use this strategy if you would like to move from server-side processing to client-side processing. The JavaScript API does not require access to HTTP request headers, so user-agent values can be requested at any point.

Strategy: Static server-side header

If you are using the User-Agent request header on the server and your needs for that data are relatively consistent across your entire site, then you can specify the desired client hints as a static set in your responses. This is a relatively simple approach since you generally only need to configure it in one location. For example, it may be in your web server configuration if you already add headers there, your hosting configuration, or top-level configuration of the framework or platform you use for your site.

Consider this strategy if you are transforming or customizing the responses served based on the user-agent data.

Browsers or other clients may choose to supply different default hints, so it's good practice to specify everything you need, even if it's generally provided by default.

For example, the current defaults for Chrome would be represented as:

⬇️ Response headers

Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA

If you also wanted to receive the device model in responses, then you would send:

⬇️ Response headers

Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA

When processing this on the server-side you should first check if the desired Sec-CH-UA header has been sent and then fallback to the User-Agent header parsing if it is not available.

Strategy: Delegating hints to cross-origin requests

If you are requesting cross-origin or cross-site subresources that require User-Agent Client Hints to be sent on their requests then you will need to explicitly specify the desired hints using a Permissions Policy.

For example, let's say that https://blog.site hosts resources on https://cdn.site which can return resources optimized for a specific device. https://blog.site can ask for the Sec-CH-UA-Model hint, but needs to explicitly delegate it to https://cdn.site using the Permissions-Policy header. The list of policy-controlled hints is available in the Clients Hints Infrastructure draft

⬇️ Response from blog.site delegating the hint

Accept-CH: Sec-CH-UA-Model
Permissions-Policy: ch-ua-model=(self "https://cdn.site")

⬆️ Request to subresources on cdn.site include the delegated hint

Sec-CH-UA-Model: "Pixel 5"

You can specify multiple hints for multiple origins, and not just from the ch-ua range:

⬇️ Response from blog.site delegating multiple hints to multiple origins

Accept-CH: Sec-CH-UA-Model, DPR
Permissions-Policy: ch-ua-model=(self "https://cdn.site"),
                    ch-dpr=(self "https://cdn.site" "https://img.site")

Strategy: Delegating hints to iframes

Cross-origin iframes work in a similar way to cross-origin resources, but you specify the hints you would like to delegate in the allow attribute.

⬇️ Response from blog.site

Accept-CH: Sec-CH-UA-Model

↪️ HTML for blog.site

<iframe src="https://widget.site" allow="ch-ua-model"></iframe>

⬆️ Request to widget.site

Sec-CH-UA-Model: "Pixel 5"

The allow attribute in the iframe will override any Accept-CH header that widget.site may send itself, so make sure you've specified everything the iframe'd site will need.

Strategy: Dynamic server-side hints

If you have specific parts of the user journey where you need a larger selection of hints than across the rest of the site, you may choose to request those hints on demand rather than statically across the entire site. This is more complex to manage, but if you already set different headers on a per route basis it may be feasible.

The important thing to remember here is that each instance of the Accept-CH header will effectively overwrite the existing set. So, if you are dynamically setting the header then each page must request the full set of hints required.

For example, you may have one section on your site where you want to provide icons and controls that match the user's operating system. For this, you may want to additionally pull in Sec-CH-UA-Platform-Version to serve appropriate subresources.

⬇️ Response headers for /blog

Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA

⬇️ Response headers for /app

Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, Sec-CH-UA

Strategy: Server-side hints required on first request

There may be cases where you require more than the default set of hints on the very first request, however this is likely to be rare so make sure you've reviewed the reasoning.

The first request really means the very first top-level request for that origin sent in that browsing session. The default set of hints includes the browser name with major version, the platform, and the mobile indicator. So the question to ask here is, do you require extended data on the initial page load?

For additional hints on the first request there are two options. First, you can make use of the Critical-CH header. This takes the same format as Accept-CH but tells the browser that it should immediately retry the request if the first one was sent without the critical hint.

⬆️ Initial request

[With default headers]

⬇️ Response headers

Accept-CH: Sec-CH-UA-Model
Critical-CH: Sec-CH-UA-Model

🔃 Browser retries initial request with the extra header

[With default headers + …]
Sec-CH-UA-Model: Pixel 5

This will incur the overhead of the retry on the very first request, but the implementation cost is relatively low. Send the extra header and the browser will do the rest.

For situations where you require really do require additional hints on the very first page load, the Client Hints Reliability proposal is laying out a route to specify hints in the connection-level settings. This makes use of the Application-Layer Protocol Settings(ALPS) extension to TLS 1.3 to enable this early passing of hints on HTTP/2 and HTTP/3 connections. This is still at a very early stage, but if you actively manage your own TLS and connection settings then this is an ideal time to contribute.

Strategy: Legacy support

You may have legacy or third-party code on your site that depends on navigator.userAgent, including portions of the user-agent string that will be reduced. Long-term you should plan to move to the equivalent navigator.userAgentData calls, but there is an interim solution.

UA-CH retrofill is a small library that allows you to overwrite navigator.userAgent with a new string built from the requested navigator.userAgentData values.

For example, this code will generate a user-agent string that additionally includes the "model" hint:

import { overrideUserAgentUsingClientHints } from './uach-retrofill.js';
overrideUserAgentUsingClientHints(['model'])
  .then(() => { console.log(navigator.userAgent); });

The resulting string would show the Pixel 5 model, but still shows the reduced 92.0.0.0 as the uaFullVersion hint was not requested:

Mozilla/5.0 (Linux; Android 10.0; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.0.0 Mobile Safari/537.36

Further support

If these strategies do not cover your use case, please start a Discussion in privacy-sandbox-dev-support repo and we can explore your issue together.

Photo by Ricardo Rocha on Unsplash