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:
| file | one row per |
|---|---|
reference/signals.md | signal 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.md | wire key and addressable leaf: types, observed values, digest and session labels |
reference/observed.md | signal id: what Chrome, Firefox and Safari actually sent |
reference/codes.md | status 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:
| measure | count |
|---|---|
| wire ids observed | 144 |
| ids registered in the map | 143 |
| ids reporting a different value on Chrome, Firefox and Safari | 58 |
| ids reporting a failure status somewhere | 38 |
| ids observed but not registered | 0 |
| static and observed disagreements | 0 |
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.