Fingerprint Pro Internals
Guide

What the agent collects

143 wire signal ids in this build: 4 in stage1, 38 in stage2, 98 in stage3, and 3 contributed by the cm module's toRequest. The agent names them itself, in the sources table it registers, so every id

143 wire signal ids in this build: 4 in stage1, 38 in stage2, 98 in stage3, and 3 contributed by the cm module's toRequest. The agent names them itself, in the sources table it registers, so every id in the map below is the agent's own label for its own collector.

Generated inventories, all regenerated by npm run map:

fileone row per
reference/signals.mdsignal id: browser surfaces, branch tests, compared constants, status codes, value shape
reference/slices/signal id: the collector's source plus every helper only it can reach
reference/schema.mdwire key and addressable leaf: types, observed values, digest and session labels
reference/observed.mdsignal id: what Chrome, Firefox and Safari actually sent
reference/codes.mdstatus code: every site that can produce it

How a signal row is built#

For each collector the tool walks the body, follows the module-level helpers it calls up to four levels deep, and records the browser surfaces it touches, the s codes it can return, and the string literals in its own body and its direct helpers. Local aliases are resolved, so const n = navigator; n.languages is recorded as navigator.languages. Surfaces are ranked rarest-first across all 143 signals, which sinks the shared iframe plumbing every stage2 collector inherits below the API that actually distinguishes the signal.

An API list says which surfaces a collector touches, not what it does with them, so each row also carries the collector's branch structure: every if, ternary and returned comparison, plus the literals those tests compare against. That is where a threshold like >= 4 or a probe like "WebKitMediaKeys" in window becomes readable without opening the bundle.

Four rows carry truncated: s94, s219, s211 and s70, the WebRTC, media-capabilities and GPU families, whose call graphs fan out past the walk limit. Their surfaces and status codes are complete; their branch lists are not. Eighteen collectors branch on nothing at all: they read a property and report it. Four read no browser surface, only the JavaScript engine, and one reaches no recorded surface at all (s120 throws a string and calls toSource() on the caught value, a Gecko-only method on a local binding, and the walk only records reads rooted at a global).

Signals worth calling out#

WebGPU timestamp queries (s70). A full render pipeline: shader module, 7x9 RGBA texture, four rotation matrices, a GPUQuerySet with timestampWrites at pass start and end, results resolved into a mapped buffer alongside performance.now() deltas. That is GPU-side execution timing, not a pixel hash, and it reads hardware behaviour that a software fingerprint patch does not touch.

Canvas (s17). The standard geometry-plus-text pair, Cwm fjordbank gly with an emoji and the three-circle multiply composite, with a stability check: the text pass renders twice and the collector reports "unstable" if the two differ. Winding is probed separately through isPointInPath(..., "evenodd"). Per-call canvas noise fails the stability check outright rather than producing a new value.

Ad-block probes (s36). Base64-encoded CSS selectors for ad containers, injected and measured for suppression. Half of them ship as atob("..."), which the deobfuscator folds, so the selector list is readable in the slice.

Automation flags (s157). The classic botd name list in plaintext: __webdriver_evaluate, __selenium_evaluate, __fxdriver_evaluate, __driver_unwrapped, _phantom, __nightmare, _selenium, __webdriver_script_fn, __$webdriverAsyncExecutor, webdriverio, wdioElectron, awesomium, phantomas, plus navigator.webdriver read directly and reported tri-state.

Engine discriminators. eval.toString().length (s24) separates V8 from SpiderMonkey and JSC. Four collectors measure the JavaScript engine rather than any browser API: s48 reads the Math.random sequence, s81 reads which byte of a Float32Array holds an Infinity, s119 reads an Error trace, s148 reads Function.prototype.bind.toString.

The long tail. Font enumeration including the FingerprintJS-typical rare faces (Vrinda, Univers CE 55 Medium, TRAJAN PRO, ZWAdobeF), MathML metrics via mmultiscripts, speech-synthesis voices, OfflineAudioContext audio, SharedArrayBuffer presence, RTCPeerConnection, Encrypted Media Extensions with a Widevine service certificate, storage quota estimates, permission queries, media-capabilities decoding info, OPFS handles, and the full CSS media-query battery.

What varies between browsers#

npm run join reads the three captures in captures/ and joins them against the static map:

measurecount
wire ids observed144
ids registered in the map143
ids reporting a different value on Chrome, Firefox and Safari58
ids reporting a failure status somewhere38
ids observed but not registered0
static and observed disagreements0

The extra observed id is s56, which no collector produces: it is the server-issued blob the agent replays, matched against reference/envelope.json instead.

The split is the useful part. The 58 varying ids are what a browser build has to keep coherent; the 85 that are identical across all three are what it cannot betray itself with. Both counts are for one machine: everything here was collected on the same hardware behind the same address, so a signal that is constant across the three columns is only known to be constant for that machine.

The notes column records where the static read and the capture disagree: a status code the collector reported that no branch in its source produces, object keys on the wire the source never names, a 32-hex value from a collector with no call to the hash mixer on its path. Each of those is a hole in the static read rather than a bad capture. Against the captures in this tree the count is zero.