How the widget runs
The widget loads in a cross-origin iframe, so the page that embeds it cannot read or patch what runs inside. Its script is obfuscated, and the collection code is bytecode for a small register machine whose opcodes and handler order change per session. Field names are randomised per build and records are shuffled per session. What cannot be renamed are the browser APIs each probe calls, such as getParameter or getHighEntropyValues, which is why public analysis can still map what is collected.
The probes group by the question they ask:
| Group | Examples |
|---|---|
| Property surface | window, navigator, document and screen, grouped by value type, read from a fresh iframe |
| Native inspection | About 150 built-ins converted to strings and timed per call |
| Automation presence | A console stack getter, a debugger statement in a worker, Node-style globals |
| Rendering | WebGL 1 and 2 parameters and extensions, WebGPU adapter info and limits, canvas, OfflineAudioContext, SVG text and element boxes |
| Host and locale | A 52-font list, a flag emoji, Intl strings, keyboard layout, client hints, historical timezone offsets |
| Network | Codec support, WebRTC ICE gathering, a worker fetch, resource timing, a WebSocket echo |
| Compute | WebAssembly kernels, the performance.now() step, a timed SHA-256 proof of work |
What the page around the widget reports
The iframe cannot read the host page, so it asks the widget's loader script, which runs in the page, and the loader answers with a summary. In the analysed widget that answer contains:
- Configuration and timing: the widget settings, the loader's own resource timing, and six elapsed-time measurements from loader start to the iframe's first message.
- A render stack:
new Error().stacktaken while the widget was rendered, which shows whether it came from the page's markup or from a script calling the render API; see stack trace inspection. - The page shape: counts of elements, scripts, stylesheets and meta tags, a short DOM-shape string, the widget's XPath and position, the title's length and hash, the referrer and
document.visibilityState. - The form: method, action origin, control counts and a keyword guess at its purpose (login, signup, checkout, contact and others).
A widget on a real login page sits in a form, on a page with dozens of elements, a title and a referrer, in a visible tab. A bare test page that loads only the widget has a handful of elements and no form, and that difference travels with the first payload.
Device facts, session values and noise
Twenty-three of the 38 probes read device facts: answers a given machine gives every time. These cannot be expected to be unique, because millions of phones of one model return the same Math output, Intl strings and WebGL parameters. What they can be checked for is possibility: do these WebGL limits exist on a GPU that fits the claimed platform, do these fonts belong to the claimed OS, does the clock step fit the claimed browser. The rest are session values such as timings, ICE candidates and the proof of work, which show the answers were produced now by a browser that really made the requests.
Noise is part of the picture. One major browser adds its own noise to canvas and audio readbacks, so its hashes change per session, while stock desktop Chrome returns identical canvas, WebGL and audio answers on one machine. Stability is a property of the claimed browser: random noise on an engine that never adds any is as inconsistent as a stable hash on one that always does.
Taken together the list is a set of questions that only make sense together: the property surface has to fit the User-Agent, WebGPU has to fit WebGL, fonts and emoji have to fit the platform, Intl and timezone history have to fit the language and the IP. That is the coherence problem a managed web data API solves by running real browsers on matching hardware and network, rather than by answering each probe separately.
