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 fields and missing fields both collapse to `''` via the `readUachField` helper so the four-column 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 the two-stage Source pattern introduced in 1.20.0. `dispatchUserAgentData()` (Phase A) reads `navigator.userAgentData.getHighEntropyValues` and immediately fires the async call if it is available, stashing the resulting Promise on the `DispatchHandle`. If the API is absent or not a function, the handle carries a resolved `{ kind: 'unsupported' }` payload synchronously. Any rejection from `getHighEntropyValues()` is captured inside the Promise chain as `{ kind: 'error' }`, so the handle never rejects.
`resolveUserAgentData()` (Phase B) awaits the handle's payload, reads the four hint strings via `readUachField`, joins them in the fixed order `architecture|bitness|model|platformVersion`, and hashes the concatenation with xxHash64. If the payload kind is `'unsupported'` or `'error'`, or the resolved object is not a plain object, the result is absent. Because dispatch fires the Promise in Phase A alongside every other collector, the async API roundtrip (typically sub-millisecond on modern Chromium) overlaps entirely with concurrent Phase B work.
The canonical one-shot path `collectUserAgentData()` calls `resolveUserAgentData(dispatchUserAgentData())` back-to-back, producing byte-identical results with no architectural difference; both the one-stage and two-stage paths exercise the same code.
const HIGH_ENTROPY_HINTS = [ 'architecture', 'bitness', 'model', 'platformVersion',
] as const; // Phase A: fires the async call immediately
const payload = getHints.call(uad, HIGH_ENTROPY_HINTS).then( (data) => ({ kind: 'data' as const, data }), (error: unknown) => ({ kind: 'error' as const, error })
); // Phase B: awaits, reads, hashes
const architecture = readUachField(data, 'architecture'); // 'arm' | 'x86' | ''
const bitness = readUachField(data, 'bitness'); // '64' | '32' | ''
const model = readUachField(data, 'model'); // 'Pixel 7' | ''
const platformVersion = readUachField(data, 'platformVersion'); // '14.6.1' | '' const hash = hash64([architecture, bitness, model, platformVersion].join('|'));The four hints requested from getHighEntropyValues() and the hash construction. Field order is fixed and is part of the hash contract.
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 `__absent__` 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 similar to the canvas RGBA migration in 1.3.0.
- FingerprintJS v4 and Thumbmark both collect UA-CH high-entropy values. This signal differs in that the four fields are hashed verbatim; Thumbmark lowercases model and FingerprintJS bucketizes Android device families. Verbatim hashing 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 Phase B work.
- Missing or non-string fields collapse to '' in readUachField 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

