As defined in the WICG User-Agent Client-Hints specification, User-Agent Client Hints (UA-CH) are available on the server side in the form of additional HTTP request headers and the client side via a JavaScript API. Confusingly, there are some slight differences in naming between the two different sources so some of our customers have asked us for guidance on obtaining UA-CH from the browser, but in a header-like format that can then be sent to a server as part of an existing payload for analysis by DeviceAtlas.
This code may be useful for customers with integrations with publishers, to share with their publisher partners to facilitate capture of client hints, or for publishers for their own use.
This library emits a JSON object containing the full set of HTTP headers to enable a DeviceAtlas API to resolve them into device properties. If the browser supports UA-CH then the the appropriate headers are included, if not only the User-Agent string is captured.
Usage
// Import module
import { clientHintHeaders } from "./clientHintHeaders.js";
// Wait for client hints promise to resolve and log the header name and value to the console
clientHintHeaders.then(res => {
for (const [header, value] of Object.entries(res)) {
console.log(`${header}: ${value}`);
}
});
Output
Chrome emulating a Pixel 5
User-Agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36
Sec-Ch-Ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"
Sec-Ch-Ua-Arch: ""
Sec-Ch-Ua-Mobile: ?1
Sec-Ch-Ua-Model: "Pixel 5"
Sec-Ch-Ua-Platform: "Android"
Sec-Ch-Ua-Platform-Version: "11"
Sec-Ch-Ua-Full-Version-List: "Not?A_Brand";v="8.0.0.0", "Chromium";v="108.0.5359.124", "Google Chrome";v="108.0.5359.124"
Sec-Ch-Ua-Bitness: "64"
Sec-Ch-Ua-Wow64: ?0
Safari on macOS Big Sur
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15
Firefox on macOS Big Sur
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:108.0) Gecko/20100101 Firefox/108.0
Code
clientHintHeaders.js
const getBrandsConcatenated = (brands) => {
return brands.map(value => {
let brandEscaped = escapeDoubleQuoteAndBackslash(value.brand);
return `"${brandEscaped}";v="${value.version}"`;
}).join(", ");
}
const doubleQuoteEscape = (value) => {
return value.replace(/"/g, '\\"');
}
const backslashEscape = (value) => {
return value.replace(/\\/, "\\\\");
}
const escapeDoubleQuoteAndBackslash = (value) => {
if (typeof value === 'string' && value){
value = backslashEscape(value);
value = doubleQuoteEscape(value);
}
return value;
}
const isValidArray = (fullVersionList) => {
return typeof fullVersionList !== 'undefined' && Array.isArray(fullVersionList);
}
const getBrowserList = (fullVersionList, brands) => {
if (isValidArray(fullVersionList) ){
return getBrandsConcatenated(fullVersionList);
} else if (isValidArray(brands) ){
return getBrandsConcatenated(brands);
}
}
const getHintsValues = () => {
return [
'brands',
'mobile',
'platform',
'architecture',
'bitness',
'fullVersionList',
'model',
'platformVersion',
'wow64',
];
}
function getPropertyQuoted(property) {
if (property !== undefined){
let valueEscaped = escapeDoubleQuoteAndBackslash(property);
return `"${valueEscaped}"`;
}
return property;
}
export const clientHintHeaders = (new Promise(resolve => {
const navUAData = navigator && navigator.userAgentData
if (navUAData) {
navUAData.getHighEntropyValues(getHintsValues()).then(ua => {
const headers = {
'User-Agent': navigator.userAgent,
'Sec-CH-UA': getBrowserList(ua.brands),
'Sec-CH-UA-Arch': getPropertyQuoted(ua.architecture),
'Sec-CH-UA-Mobile': '?' + (ua.mobile ? '1' : '0'),
'Sec-CH-UA-Model': getPropertyQuoted(ua.model),
'Sec-CH-UA-Platform': getPropertyQuoted(ua.platform),
'Sec-CH-UA-Platform-Version': getPropertyQuoted(ua.platformVersion),
'Sec-CH-UA-Full-Version-List': getBrowserList(ua.fullVersionList, ua.brands),
'Sec-CH-UA-Bitness': getPropertyQuoted(ua.bitness),
'Sec-CH-UA-Wow64': '?' + (ua.wow64 ? '1' : '0')
};
resolve(headers);
})
} else if (navigator) {
resolve({
'User-Agent': navigator.userAgent,
});
} else {
resolve({});
}
}));