What it measures
The signal captures the four high-entropy User-Agent Client Hints fields that Chromium exposes only on explicit `getHighEntropyValues()` request: `architecture` (the OS-claimed CPU family, e.g. `'arm'` or `'x86'`), `bitness` (process bitness, `'64'` or `'32'`), `model` (device-marketed name, typically empty on desktop and populated on Android as e.g. `'Pixel 7'`), and `platformVersion` (OS minor version number, e.g. `'14.6.1'`).
The four strings are hashed verbatim: no normalisation, no lowercasing, no bucketing. Chromium returns deterministic values per (OS, browser build, device model), so the raw strings carry maximum entropy and stay stable across page loads on the same device. Non-string and missing fields both collapse to an empty string so the four-field shape is preserved across all Chromium variants.
The signal complements the NaN-sign-bit `architecture` signal and the `platform` signal. The NaN byte tells the JIT-level CPU family; UA-CH `architecture` tells the OS-claimed CPU family. They should agree on real hardware and diverge when a UA-CH override extension is active. UA-CH `platformVersion` recovers the OS version number that Chromium's UA reduction deliberately freezes out of `navigator.platform`.
How it's collected
The collector uses a two-stage collection pattern that fires the asynchronous getHighEntropyValues() call early so its round-trip overlaps with other concurrent work, then awaits the result in a later stage. In the first stage it reads `navigator.userAgentData.getHighEntropyValues` and, if it is a function, invokes it immediately and stashes the resulting Promise. If the API is absent or not a function, an unsupported result is recorded synchronously. Any rejection from `getHighEntropyValues()` is captured inside the Promise chain, so the pending call never rejects out of band.
The second stage awaits the pending result, reads the four hint strings, joins them in a fixed field order, and hashes the concatenation with xxHash64. If the call was unsupported or errored, or the resolved value is not a plain object, the result is absent. Because the call is fired early alongside every other collector, its round-trip cost (typically sub-millisecond on modern Chromium) is masked by concurrent work.
A one-shot convenience path runs the two stages back-to-back, producing byte-identical results with no architectural difference; both the one-shot and two-stage paths exercise the same code.
Confidence rules
| Confidence | Trigger |
|---|---|
| normal | getHighEntropyValues() resolved with an object, even if all four fields are empty strings |
| absent | navigator.userAgentData is undefined (Safari or Firefox); sentinel: 'unsupported' |
| absent | getHighEntropyValues is not a function (sentinel: 'unsupported') |
| absent | The Promise rejected (e.g. NotAllowedError under tight Permissions-Policy); sentinel: 'threw' |
| absent | The resolved payload is not a plain object (sentinel: 'unsupported') |
Why engine-bound
The `navigator.userAgentData` API is a Chromium invention. Safari has not implemented it; Firefox has not implemented it. If this signal were placed in `the hardware signal set`, Chrome and Safari on the same physical machine would produce different hardware fingerprints even though the CPU, RAM, and GPU are shared, because one browser provides a value and the other does not. That would violate the hardware-binding invariant.
The rationale is identical to `js_heap_size_limit`: data that only one engine family can supply must be engine-bound. The signal contributes entropy to the full `fingerprint` on Chromium-family browsers and contributes an absent result on all others, with no impact on `hardwareFingerprint`.
Things worth knowing
- platformVersion reflects the OS minor version (e.g. '14.6.1' for macOS Sonoma 14.6.1). When the OS updates, this value shifts and so does the hash. Treat the hash as engine-bound and OS-build-bound.
- model is empty on all desktop browsers including Chrome on macOS and Windows. On Android, Chromium returns the device-marketed name (e.g. 'Pixel 7'), a strong device-class discriminator but not a unique device identifier.
- The collector deliberately does not request wow64, fullVersionList, or formFactor hints that the UA-CH spec defines. Requesting additional hints in a future version would shift the hash, so treat any hint expansion as a one-time fingerprint migration.
- Some other fingerprinting libraries normalise these fields before hashing (for example by lowercasing the device model or bucketing Android device families). Benny hashes the four fields verbatim, which preserves maximum entropy because Chromium's output is already deterministic.
- The async roundtrip to the browser process is typically sub-millisecond but can reach ~100 ms on some Chromium implementations that serialise the call across an IPC boundary. The two-stage pattern ensures this cost is masked by concurrent work.
- Missing or non-string fields collapse to an empty string rather than being omitted. This preserves the four-segment shape so the hash structure is consistent even when a hint returns null or undefined in an older Chromium build.
Last reviewed 2026-06-04

