Technical white paper · v1.0
Trace Lab: measuring browser fingerprinting, and only claiming what survives repetition
An instrumented, caged browser that shows which fingerprinting techniques a web page uses, which script used them, who received the data, and whether it happened before the visitor agreed to anything.
01Executive summary
Browser fingerprinting identifies a visitor without cookies, by reading small differences in how their browser draws graphics, plays audio, lists fonts and reports its environment. It is invisible to the visitor and largely unmoved by cookie banners. Trace Lab loads a page in a purpose-built, instrumented browser and reports what the page did, in what category, from which script and to which third parties.
Its distinguishing feature is restraint. A fingerprinting report is easy to make confident and wrong, so Trace Lab will not claim a difference between two states of a site unless the difference survives repeated loads. The tool is designed around not overstating what it measured.
02The problem
Whether a site fingerprints its visitors, and whether it does so before they have agreed to anything, is a question regulators, privacy teams and site owners increasingly need answered with evidence. Reading a site's privacy policy answers a different question. Watching what the page's code actually calls answers this one.
That raises two hard problems. First, a tool that points a browser at any address a user types is a server-side request engine by construction, so it must be built to survive hostile pages. Second, the web is noisy: advertising auctions, lazy-loaded tags and experiments mean no two loads of a busy page are identical, so naive before-and-after comparisons produce plausible fiction.
03Architecture
A scan is a job, not the return value of a request. The web application validates the target, opens a scan record, hands back an identifier and returns. A runner then drives the browser through the planned passes and saves each pass as it completes, so the interface shows progress, and a scan that fails on its third pass still has the first two on record. Running scans as jobs also keeps every request comfortably inside the time limit a content network applies to web traffic.
04The instrument
The instrument is a script injected into the page before any of the page's own code runs. It watches seven categories of technique: canvas, WebGL, audio, fonts, environment properties, WebRTC and storage. For each, it records the interface called, the script that called it and a trimmed summary of the arguments.
Wrappers that still look native
The obvious way to watch a browser function is to replace it, but a replaced function betrays itself the moment a script prints its source. Fingerprinting and anti-bot scripts check exactly that, and a script that notices it is being watched behaves differently, so the tool would report the page's reaction to the tool. Trace Lab wraps functions in proxies that forward their name, length and string form to the original, so the wrapper still reports itself as native code. It is not undetectable, and the design says so, but it survives the checks that are actually common.
Three rules the instrument must keep
| Rule | Why |
|---|---|
| Never throw into the page | If the instrument breaks the page, the scan would report "no fingerprinting" with total confidence, which is worse than reporting nothing. |
| Never grow without bound | Records are de-duplicated and counted, and every list has a hard cap, so a measurement call in a render loop cannot become a fifty megabyte report. |
| Never assume an API exists | A missing interface is a fact about the browser, not an error. |
Recovered records live in a closure, reachable through a per-scan random key. The design is explicit that this is obfuscation, not a security boundary, so that a later reader never mistakes it for one.
05Containment: a browser pointed at hostile pages
A browser that follows redirects, runs scripts and fetches sub-resources for any address a user types is more dangerous than a plain fetch. Trace Lab uses three independent layers, all required, so no layer is ever removed because another "already covers it".
| Layer | Where | What it stops |
|---|---|---|
| 1. Vet the target | The application, before queueing | Addresses that resolve to private or internal ranges, expressed as ranges rather than address lists so new internal networks are covered the day they are created. |
| 2. Vet every request | The browser, intercepting sub-resources | Redirects and embedded resources that point inside. |
| 3. Seal the network | The container network itself | Anything the first two missed, including real-time media candidates and rebinding tricks. The browser container has no gateway at all, so there is no route to forward a packet. The only door is a small, dependency-free proxy that allows ordinary web ports only and logs host and port, never the full address. |
The browser container is also built on the assumption that it will one day be compromised. Findings crossing back to the application are re-validated against the same limits and vocabularies, control characters are stripped from everything originating on a hostile page, and the user interface builds its display with text nodes only, enforced by a test that fails if unsafe markup APIs appear.
06Consent testing
The finding that matters is not "this page fingerprints you" but "this page fingerprints you before you agreed to anything". To establish it, the same page is loaded three ways: untouched, after rejecting the cookie banner, and after accepting it. Each finding is keyed by technique, interface, script and phase, because the same call before and after consent are two different claims.
Why the tool clicks with a curated list
The industry consent framework cannot be told to accept or reject. It deliberately has no such command, because consent is meant to come from a person. It is used for detection and for verifying that a click changed state. The clicking itself uses a curated table of known consent platforms, covering plain markup, shadow trees and iframes.
A text heuristic ("press the button that says Reject") would cover more sites, and would sooner or later click Accept while believing it had clicked Reject, producing a confident wrong result: "rejecting made no difference". Sites outside the table are reported as not drivable, a visible gap rather than an invisible error. Every outcome is also named honestly: no banner, clicked but unverified, click failed, render failed.
07Why one load is not a measurement
Measured on the production deployment, three identical loads of a large news site, with nothing clicked, produced 64, 59 and 55 distinct findings, of which only 55 appeared in all three. A smaller site produced 15, 15 and 12, with 12 in common. Roughly 14 to 20 percent of what a single load reports is churn from ad auctions, lazy tags and experiments.
One load per arm could not separate churn from the effect of clicking Reject, and would have reported about nine spurious finding differences as consequences of the consent choice. So each arm is loaded three times, and a difference counts only if it holds across the repeats. A repeat mode, three loads with nothing clicked, is the negative control for all of it: three passes that should differ in nothing.
08Comparing over time
The same rule extends to comparing a site against itself later. The comparison classifies each finding as started, stopped, unchanged or unstable, using only the untouched loads from each scan so the consent choice is never mistaken for a change over time. A single-load scan cannot be compared at all: the comparison refuses and explains why, rather than producing a page of fiction. It also refuses a reversed pair, because a comparison run backwards would state the exact opposite of what happened in confident prose.
09Lessons
- A check that cannot go red proves nothing. The stability filter passed every test for weeks while never having fired, because every assertion expected zero unstable findings. A fixture that deliberately churns, served from a single address so finding identities match, finally made it fail on demand. The two churn sequences also differ deliberately, because with identical sequences a broken filter produces no change.
- Protection and evidence can drift apart. The range rule was correct when internal addresses changed, but the test fixtures still named only the old addresses, so a passing suite was testing an estate that no longer existed.
- Say what a mechanism is not. The comments on the instrument state plainly that its hiding trick is obfuscation, not containment.
- Prefer a visible gap to an invisible lie. Unknown banners report as not drivable.
- Let the deployment certify itself. A probe compares file hashes against the running images rather than asserting the deployment is current.
10Appendix: facts and figures
| Item | Value |
|---|---|
| Built | 23 August 2026 (first milestone) |
| Milestones | Containment, the instrument, scan history, findings interface, consent testing, comparison over time |
| Stack | Node.js and Express, a headless browser, PostgreSQL, a dependency-free vetting proxy |
| Technique categories | Canvas, WebGL, audio, fonts, environment, WebRTC, storage |
| Scan modes | Single (1 load), repeat (3 loads), consent (3 arms of 3 loads) |
| Caps per scan | 200 distinct findings, 500 recorded sub-requests, argument and stack text truncated |
| Verification at last count | 268 unit tests; 91 live probe checks including image hash comparison |
Our work is always evolving, and so are the AI vendors and their products. Nothing described here is set in concrete. Each paper describes its system as it stood in October 2026, when it was written; the date shown is when the app was first built.