Everyone has an opinion about what fingerprinting sites read. Almost nobody has the receipts. So we took an instrumented Chromium that logs every Web IDL binding access at the engine level, pointed it at 32 detection and anti-bot sites one at a time, and recorded exactly which properties and methods each site touched, along with the script URL that did the touching and the decoded WebGL enum arguments. This is what came back on 2026-07-14. It is a snapshot. The game keeps moving, and the method has real blind spots we will be blunt about.
The method
The logger sits below JavaScript, down in the C++ bindings layer, so it never has to hook navigator or patch prototypes. It sees the access at the moment V8 crosses into the engine. We set the mode to record all Web IDL exposure rather than a fingerprint whitelist, so the raw logs hold the full API surface each site touched and we bucketed afterward.
The drive loop was deliberately boring and identical per site:
for each site:
fresh user-data-dir # no cross-site state, log truncated per session
navigate(url)
wait(load)
synthesize mouse move + scroll # trip behavior-gated probes
dispatch mousemove in-page
dwell ~14s # let detection JS finish
collect fp-api-log.jsonl
One consistent persona everywhere (US / Win11 / Chrome 149 / Intel UHD 620 / en-US / LA timezone / colorDepth 24 / DPR 1.5), chosen so that a self-inconsistent persona would not pollute the observation. Everything driven over CDP. WebGL getParameter calls were decoded from their GLenum arguments (37445 is UNMASKED_VENDOR_WEBGL, 37446 is UNMASKED_RENDERER_WEBGL), so a bare numeric probe becomes a named intent. chrome:// and new-tab noise got filtered at analysis time.
The synthetic mousemove matters more than it looks. Several detectors gate their heaviest probes behind a real interaction event, and without dispatching one you record an empty page and conclude, wrongly, that the site is light.
What the sites actually weighed
The per-site heaviest dimensions are the fastest way to read each vendor’s personality. “fp accesses” counts the accesses we bucketed into a fingerprint dimension. It is a relative volume indicator, not a verdict on detection strength. A site can be devastating with a handful of well-chosen reads.
| Site | Class | fp accesses | Heaviest JS dimensions |
|---|---|---|---|
| CreepJS | Consistency | 1056 | canvas 247 · webgl 189 · domrect 144 · navigator 143 · fonts 65 · audio 62 · permissions 60 · webgpu 44 |
| BrowserScan | Consistency | 12375 | webgl 4982 · layout-measure 4952 · navigator 927 · webgpu 682 · domrect 482 |
| AmIUnique | Academic (INRIA) | 7524 | layout-measure 6166 · screen 488 · webgl 412 · navigator 363 |
| Iphey | Consistency | 2740 | fonts 930 · layout-measure 622 · canvas 367 · webgl 246 · webrtc 173 · webgpu 168 |
| deviceinfo.me | Consistency | 24310 | audio 24073 · navigator 127 · screen 45 |
| DataDome test-bot | Commercial | 5691 | domrect 1888 · layout-measure 2401 · navigator 690 · screen 203 · webgl 151 |
| BrowserLeaks Fonts | Single-dimension | 7661 | layout-measure 7658 (offsetWidth/Height ×3829 each) |
| Sannysoft | bot/headless | 277 | navigator 143 (tests webdriver) · storage 42 · screen 36 · canvas 24 |
| Cloudflare Turnstile | Commercial | 629 | webgl 189 · navigator 117 · webgpu 100 · fonts 56 · audio 30 · canvas 26 |
| fingerprint.com bot demo | bot/headless | 1143 | navigator 355 · layout-measure 307 · webgl 167 · webgpu 112 · screen 95 |
A few of these deserve their own sentence. deviceinfo.me is almost pure audio: AudioBuffer.getChannelData swept across the whole buffer roughly 24,000 times. BrowserLeaks Fonts is pure layout measurement, with offsetWidth and offsetHeight read 3,829 times each and no font API touched at all. BrowserScan ships FingerprintJS V5 and runs a real WebGPU createTexture rather than a bare adapter query. AmIUnique, the academic outlier from INRIA, does massive <span> font enumeration (6,166 layout-measure accesses) and still loads the venerable PluginDetectAll.js. CreepJS is the broadest single script: one creep.js covering every dimension plus its lie/consistency layer, which is the part that matters.
The ranked signal set
Dedup the 32 sites, rank by breadth times volume, and you get the set that modern detection actually runs on. This is the useful output: the probe union any comprehensive detector or self-test kernel should implement.
- Fonts, the number-one signal, and mostly not through a font API. The dominant technique is layout measurement: inject spans styled
font-family: <candidate>, monospace, readoffsetWidth/offsetHeight, and diff the candidate against the fallback to decide whether the font is installed. BrowserLeaks Fonts, AmIUnique, BrowserScan, Iphey, DataDome and the Vastel family all do this.FontFaceSet.check()/FontFace.load()andmeasureTextare secondary paths. Cloak fonts by only touching the enumeration API and the measurement path catches you the moment a blocked font’s span width fails to collapse to the fallback. - WebGL, universal and deep.
UNMASKED_VENDOR/RENDERERon nearly every site, dozens ofgetParameterenum reads,getShaderPrecisionFormat,getSupportedExtensions, and real rendering:shaderSource→compileShader→drawArrays→readPixels. It reads the strings, then draws a frame and hashes the pixels. - WebGPU, a genuinely new dimension. Many sites now call
navigator.gpu.requestAdapter(), then readGPUAdapter.limitsitem by item andGPUAdapter.info(vendor / architecture / description). BrowserScan goes further withGPUDevice.createTexture. This is the axis most likely to expose a masking browser today, becauseadapter.info.architectureleaks the real GPU generation while WebGL reports the faked one. - Canvas / Audio, classic but watch the read-back path.
getImageData/toDataURLfor canvas;OfflineAudioContext→startRendering→getChannelDatafor audio. The tell is determinism: read the same buffer twice and it must come back identical. - DOMRect / subpixel, consistency and behavior at once.
getBoundingClientRectandgetClientRects. DataDome leans on these hardest (≈965 + ≈923 calls), using them for both layout fingerprint and element-visibility/coordinate sampling. CreepJS uses them for lie detection: the same rect read twice must be identical. - Navigator surface, broad identity.
plugins/mimeTypesenumeration,userAgentData.getHighEntropyValues()(the UA-CH high-entropy set is the value most likely to disagree with the UA string),webdriver,deviceMemory,hardwareConcurrency,connection(NetworkInformation), languages, and EME capability viarequestMediaKeySystemAccess. - Consistency / lie detection, the killer app.
Permissions.query({name:'notifications'}).stateversusNotification.permission; main thread versus Workernavigator.*; the GPU triad (WebGL vs WebGPU vs UA platform); native-functiontoStringintegrity; and plain value-range plausibility (audio magnitude, DPR × resolution, colorDepth). - Worker / OffscreenCanvas, rising.
WorkerNavigator.userAgentre-read inside a Web Worker and compared to the main thread;OffscreenCanvas2D fingerprinting to bypass masking that only hooks the main thread.
Then a long tail that is real but lower-priority: multi-monitor screen offsets (availLeft/screenX/isExtended), WebRTC ICE candidate IP leakage, getBattery(), device sensors, speechSynthesis.getVoices(), mediaDevices.enumerateDevices(), and storage/OPFS via StorageManager.getDirectory().
One theme runs across the top of that list. The highest-value work is cross-checking, not collection. Anyone can read hardwareConcurrency. The detectors that win read it twice from two contexts and compare.
“The lie-detection logic is worth more than the collection. Any script can read a value; the ones that matter read it from two places and check whether the browser told the same story both times.”
Vendor signatures
Because the logger records the triggering script URL, the recon doubles as a fingerprint of the detectors themselves. These signatures are reusable for passively identifying which vendor a target site runs.
| Vendor | Signature script URL | JS probe profile |
|---|---|---|
| DataDome | js.datadome.co/tags.js |
Massive DOMRect (getClientRects + getBoundingClientRect ≈1900) + font layout-measure + plugins/UA-CH + WebGL getParameter + audio + FontFaceSet |
| Cloudflare Turnstile | challenges.cloudflare.com/turnstile/v0/*/api.js (+ challenge-platform/*) |
WebGL renderbuffer real render + WebGPU adapter.info + OffscreenCanvas getImageData in worker + FontFace.load + OfflineAudioContext |
| reCAPTCHA v2/Ent | gstatic.com/recaptcha/.../recaptcha__en.js + api2/webworker.js |
Light JS fingerprint: navigator (incl. WorkerNavigator.userAgent ×26) + battery + storage + mediaDevices; weight on behavior/cookie/token |
| Arkose / FunCaptcha | client-api.arkoselabs.com/v2//api.js |
Loader only reads navigator.connection/network info; real puzzle in the post-trigger enforcement iframe |
| FingerprintJS Pro | first-party-proxied <host>/<random>/web/v4/<random> |
Full spectrum: WebGL params, WebGPU limits ×71, font measurement, canvas, audio, webrtc, battery, permissions, OPFS getDirectory, WorkerNavigator |
| Castle.io | @castleio/castle-js/dist/castle.browser.js |
canvas measureText/fillText + OffscreenCanvas getImageData + WebGPU + WebGL |
Open-source libraries seen in the wild and worth reusing as ready-made probes: fpCollect.min.js (Sannysoft, Incolumitas), FingerprintJS V5 (BrowserScan), creep.js (the whole CreepJS consistency-and-lie suite in one file), and the Vastel family cstlxp.js / device_info.min.js plus playwright.js, which specifically hunts Playwright traces.
Two operational notes. reCAPTCHA and Arkose keep a deliberately light first-party JS surface; the real work is behavioral, token-based, or deferred into an iframe that only loads after you engage the widget. FingerprintJS Pro is served through a first-party proxy path with randomized segments, precisely so that blocking it by URL is hard. Match the /web/v4/ shape rather than a fixed host.
The blind spots, read this before you trust the columns
This is the part most write-ups skip, and it changes how you read every table above.
The logger sees Web IDL bindings only. It cannot see V8 built-ins. So Date.getTimezoneOffset, Intl.DateTimeFormat().resolvedOptions(), Math.*, Function.prototype.toString and Error.stack never appear in the logs, even though timezone probing, toString tamper checks and Math fingerprints are certainly happening. They just travel a path this method cannot observe.
It also cannot see the network layer. TLS (JA3/JA4) and HTTP/2 frame fingerprints are computed in the transport layer, before any JavaScript runs. That is why tls.peet.ws and BrowserLeaks TLS look nearly empty on the JS side: their JS only fetches and renders a result the server already computed. For anti-bot stacks that look at the network layer first (Akamai, Cloudflare, DataDome), a perfect JS persona can still be classified at the handshake.
And some sites were simply not fully exercised that day. EFF’s Cover Your Tracks needs a button click. infosimples detect-headless gates everything on a body mousemove and had subresources blocked. areyouheadless returned 502. Those “empty” rows are measurement artifacts.
“The logs can’t see V8 built-ins or the TLS layer, so an empty column means the probe is invisible to this method, not that the site skipped it.”, the recon notes
The corollary is the thing to keep: “not observed” is not “not tested.” Every zero in the coverage table means one of a few things. Maybe the site is genuinely light. Maybe it is behavior-gated and we failed to trip it. Maybe the probe lives in a layer this instrument is deaf to. Treat the tables as a lower bound on what detectors do, never an upper bound.
Takeaway
The measured signal set is unambiguous at the top: fonts (by layout measurement, not the font API), WebGL (params plus real render), the newly-serious WebGPU axis, and the classic canvas/audio pair, all wrapped in the cross-context consistency checks that are the actual state of the art. Build a self-test kernel or a comprehensive detector against the probe union above and you will cover what the market runs today. But budget for the two things this method is blind to (V8 built-ins and the network layer), because that is exactly where a JS-perfect persona still gets caught.