Methodology
How a scan works
Scanner-generated reports contain one measured run per condition: one run for a single report and two for a comparison. Every number is an observation from those runs, not a general claim about the site. This page describes what a run does, what is recorded, and where the honest limits are. Terms are defined in the glossary; how scan data is handled is on the privacy page.
A measured run is not a promise of exactly one network navigation. Consent verification may perform one disclosed post-choice reload; the input probe may navigate to a blank page at the end to flush unload beacons; and policy analysis may open one separate, SSRF-guarded policy page after the primary request log is frozen. Reload- and policy-phase traffic is excluded from the main request counts, while unload beacons sent by the measured page during the active input probe remain included.
A local PageGraph import is the disclosed exception: it adapts a paired GraphML capture and metadata sidecar produced outside this service. That path records request evidence from a self-reported headful Brave Nightly crawl; cookie, storage, fingerprinting, detector, and consent evidence are explicitly marked unsupported rather than shown as observed zeroes. The GraphML binds its schema, root URL, capture date, and duration, while browser, environment, tool, sanitizer, and quality declarations come from the sidecar and are not cryptographically attested.
The visit
The controlled scanner loads the page in a headless Chromium browser with a fixed profile: en-US locale, UTC timezone, a desktop or mobile viewport, and a disclosed egress network. It does not scroll, click, or log in, with exactly two bounded exceptions described below. The visit ends after a capped duration, and the current Node report records the scan conditions (Playwright and browser versions, viewport, timezone, locale, GPC state, catalog version, egress) so the result is reproducible for that configuration. Historical reports that predate exact Playwright provenance show it as not recorded. Sites can behave differently for real users, regions, accounts, or network locations, so results are evidence to check, not a verdict.
The scanner never disguises itself: it is an honest automated browser. Sites that block automation are reported as failed loads rather than being scanned through evasion, because a report gathered under a disguised identity would misdescribe its own conditions.
Optional restart-safe execution uses a fenced lease with at most two attempts. If scanner execution, publication, or status coordination is lost, the first attempt can be abandoned and the same admitted job retried once. The site may therefore receive an extra automated visit that was partial or that completed before its result was lost. A report is built from one completed attempt per condition and never combines requests or other evidence across attempts. If a complete report had already reached storage before status was lost, the scanner reconciles that exact report instead of visiting the site again.
What is recorded
The controlled scanner can record the following families; each report discloses any family it did not capture.
- Network requests (URL, domain, method, resource type, status), classified first/third party.
- Curated service labels from a hand-maintained, US-biased catalog of recognizable services.
- Cookies and local/session storage keys as privacy-filtered snapshots. Current reports also retain changes between visit-phase boundaries; those are snapshot differences, not direct observations of browser write events. Unreviewed names are hidden and values are omitted.
- High-entropy browser API calls and behavioral fingerprinting heuristics (canvas, WebGL, audio, WebRTC, listener coverage).
- Advertising-pixel events (Meta, TikTok, X) and whether their identifier fields carried values.
- DNS CNAME-uncloaking of first-party subdomains that alias to known trackers.
- A privacy-policy cross-check: the site's own policy text compared against the observed evidence.
Counts are lower bounds. Requests made by a dedicated Web Worker are recorded, but Service Workers are blocked before the page loads, so none runs; a SharedWorker's own traffic beyond the script that starts it is not recorded, and no WebSocket activity is recorded at all, neither the connection nor its messages. Storage keys are read from the top frame only, and trackers that load only after interaction or consent are not seen by a passive visit.
A visit that requests Global Privacy Control runs every worker the site asks for, whatever its script URL, and puts the signal inside each dedicated worker's own execution context: the worker is paused before its first statement over a DevTools session scoped to the measured page, the property is installed inside the worker, and the same evaluation reads it back. Only that readback, testimony from inside the worker itself, marks the worker as carrying the signal. A worker the scanner could not attest this way still runs untouched; the visit then says so in its warnings and marks its request evidence incomplete. SharedWorker is the standing case: the browser does not expose shared workers to a page-scoped session, so their execution context never carries the signal even though their network requests carry the Sec-GPC header, and every such construction is disclosed. For attested workers a GPC comparison differs between its two visits in the signal alone; an unattested worker is the remaining one-visit asymmetry, and it is always disclosed rather than silently passed.
The two bounded interactions
First, the input probe: the scanner types a synthetic, non-personal test value into up to a handful of visible form fields, never submits, and watches whether that value leaves to a third party in plain, encoded, or hashed form. Second, in consent comparison mode only, the scanner clicks one accept-all or reject-all control on the cookie banner's first layer (known consent-platform controls first, then a conservative whole-label match). Whenever at least one field accepts the synthetic value, the report says how many did; a visit where no field accepted it carries no typed-field count. Consent-comparison reports separately disclose which consent platform or labeled control was clicked (naming the control itself when that platform's control may not express the whole choice), or that nothing was clicked.
Legacy v1 reports record only that a consent click was dispatched. R2 reports also record bounded consent-platform readbacks and distinguish a verified registered choice from a contradiction, weak signal, unavailable state, or failed check; one disclosed reload can re-read the state, and requests observed during that reload phase are excluded from the counts. Every visit's recorded requests still span before and after its click, so even verified r2 wording does not attribute every request to the choice.
Comparisons
A comparison is two sequential visits that differ in one declared condition: Global Privacy Control off versus on, no blocking versus Brave-list block simulation, or an accept-all versus reject-all consent click. From the July 13, 2026 randomization release onward, the two visits run in randomized order so time-ordered site behavior is not systematically assigned to the same arm across scans. Post-release v1 report warnings name the visit that ran first; post-release v2 JSON records AB for baseline first or BA for variant first. Comparisons captured before that release used a fixed baseline-then-variant order and carry no randomized-order disclosure. A single two-visit report is not counterbalanced; only an aggregate containing independent AB and BA pairs can make that claim. Before any comparative wording is used, an eligibility gate checks that both visits completed, hit no recording caps, and held the non-compared conditions constant; ineligible pairs render as two independent visits with the reasons stated. Differences between two visits can still reflect timing, experiments, caching, consent state, or bot detection, so comparison wording stays descriptive: it reports what differed between the visits, never that the compared setting caused the difference.
The Brave-list blocking simulation
Blocking evidence uses Brave's own ad-block engine (the open-source adblock-rust crate compiled to WebAssembly) with the default-enabled Brave Shields filter lists, vendored as a pinned snapshot. Matching requests are aborted in this scanner's browser: a simulation of Brave's default list blocking, not a live Brave-browser visit. Each request is matched with its actual HTTP method against the document that initiated it, network rules only (no cosmetic rules). Blocked counts are a close lower-bound approximation of Brave's default Shields for that page load, and the report separately states filter-list matches, engine-blocked requests, and the total third-party reduction, which are three different measurements.
Publication and redaction
Reports cross a default-deny sanitizer before anything is stored or shared: query strings and fragments are stripped in the browser before a scan is even submitted, and stored reports keep only reviewed, exact literals for paths, query keys, subdomain labels, cookie names, and storage keys, generalizing everything else. Report warnings come from a closed scanner vocabulary, so page-controlled text cannot impersonate the scanner. Shared reports live behind unguessable IDs and expire; reports published into the versioned public corpus are retained under disclosed age, count, and cohort rules. Reports cited by the corrections ledger are pinned against automated corpus pruning.
Restart-safe queue data, when explicitly enabled, is infrastructure state rather than report evidence. The application encrypts only the normalized scheme, host, and path plus scan options before committing the job; the active ciphertext is bounded to 75 minutes and deleted on every terminal outcome. It excludes client identifiers, verification and access tokens, headers, cookies, screenshots, observations, and results. Unencrypted scheduling metadata contains no target or client identity. Cloudflare recovery snapshots may retain application-encrypted copies after active deletion until their own retention window expires. This path stays disabled until its flag, encryption key, private coordinator authentication, privacy disclosure, and live lease-expiry test are all in place.
An optional encrypted watch schedules one immediate single-mode visit and then independent visits every seven days, with a five-attempt/30-day ceiling. Failed pre-admission attempts still consume that bound. Every due attempt freshly resolves and validates the target in Node before the connect-time public-address guard performs the visit; an old public DNS decision is never reused. Each result is an ordinary r2 report with the conditions observed on that run. Because live report links follow the ordinary seven-day and count retention policy, a watch is a scheduled rescan convenience, not a permanent time series or a claim that an observed difference was caused by the passage of time.
The corpus and percentiles
Findings like "at or above the 90th-percentile mark for third-party domains" use one exact measurement cohort: report schema and revision, methodology, tracker catalog, read-time ServiceRole taxonomy, metric contract, producer, and requested-GPC condition must all match. Each cohort keeps one newest eligible passive lead run per distinct site. Failed or no-response loads, request-incomplete runs, accept/reject consent arms, and reserved domains are excluded from statistical measurement; v1 and v2 runs can contribute only inside their own exact cohorts. The v4 artifact separately publishes all catalog-matched request rows and the third-party tracking-role subset defined by metric-contract-v1. Corpus coverage counts distinct sites with a successful single run or primary comparison arm, including capped recordings; two successful primary arms still count the site once. A site represented only by failed or block-page visits was attempted but is outside that loaded coverage count.
Nearly all committed corpus reports today are frozen schema v1: they predate or fall back from the controlled v2 collection lane, carry no recorded producer build commit and no consent-registration readback, and support descriptive claims only, never causal ones. Live scans from this site's scan form emit the current v2 wire with those recorded facts. The corpus shifts toward v2 only as controlled collection replaces the disclosed v1 lane, and each report page states its own schema generation, so no reader needs this paragraph to know which kind they are looking at. Each metric's measured sample can be narrower still when that evidence family was censored. Percentile wording activates only after both the exact methodology cohort and that metric's own distribution contain at least 50 eligible sites; each percentile card names the metric-specific denominator. Any report whose exact cohort or metric sample remains below that gate uses fixed reference thresholds. The wording is anchored to the stored percentile mark, not the percentage of sites strictly below a value, because ties can make those different. The corpus is curated, not a random sample of the web. Site history pages compare a site only against its own earlier reports with a compatible schema, method, browser, device, filter-list engine/source/count, and known snapshot dates. The dates may differ so the history can describe a list refresh; retention alone never makes two reports comparable.
Reproducibility
The scanner, catalog, eligibility gates, and report UI are open source (AGPL-3.0-or-later), every report embeds its scan conditions and methodology identity, and the evidence is exportable as sanitized JSON and CSV. The public corpus, its percentile statistics, and the researcher export are regenerated from the same committed report files this site renders, so the numbers cannot disagree with the evidence behind them.
What you must trust, and what you can check
Approved use boundary. This report is investigative evidence that requires independent corroboration. It is not a standalone legal determination and not the sole exhibit in a legal proceeding.That is this project's own recorded decision about its output, approved on and carried in the release manifest rather than asserted here. It holds regardless of how much of the chain below verifies: a report whose bytes are perfectly attested still reports one automated visit, through detectors whose error rates this release does not publish.
Independently checkable, with no trust in this operator: every committed corpus report's exact bytes are hashed into a per-commit evidence manifest that a separate, checkout-free CI job attests with Sigstore, so a third party can verify a committed report back to a public git commit (step-by-step guide). Also checkable: a report's structural validity against the frozen published schemas, its internal consistency (the validators are open source), that the corpus statistics regenerate from the same committed files this site renders, and every publication's place in the append-only transparency log, whose chain heads carry OpenTimestamps anchors: once an anchor's Bitcoin attestation completes, every entry beneath that head provably existed before the confirming block, on a clock nobody involved controls. A single command, npm run verify:report, replays the byte and digest checks for any committed report.
Taken on trust in this operator today: that the deployed scanner ran the attested code for any individual live scan (the deployment's self-reported build is checked hourly, but no per-scan record binds a specific visit to an attested image), the timestamps recorded inside reports (scanner clocks; the transparency log's anchors bound publication times externally, but a fresh anchor is a calendar promise until its Bitcoin attestation completes, and per-entry inclusion timing is coarser than per-report clocks), and the retention of ephemeral share reports, which expire and join no log. The methodology-identity digests in each report bind declared vocabularies and contracts, not the implementation code itself; the implementation is bound by the commit-level attestation above, at the covering git SHA. Closing the per-scan binding gap and anchoring ephemeral reports is tracked release work, not a disclosed aspiration of the current system.
How well the detectors work, and why no rate is published yet
A detector match is an observation, not a verdict. “Behavioral fingerprinting heuristics matched” means the recorded evidence met a documented rule; it does not tell you how often that rule is right. Answering that needs a calibration study: a set of sites labelled independently of the detector, so its answers can be counted against reference answers and turned into a precision and a recall. No detector in this release has a published precision or recall. The repository holds 1 committed study; the evidence register lists each one and the exact reason its re-analysis withholds a rate.
The bar is deliberately high, because a number nobody can check is worse than no number. A study must be declared before it collects: the plan, the frame, and the sealed reference labels are all committed and timestamped before the first scan runs, so nobody can choose a population or a label after seeing how the detector did. Labels come from independent reviewers who never see the detector's answer, with a separate blind tiebreaker precommitted for disagreements. Sampling must be simple-random with independent units. Every case that failed to capture makes the whole study ineligible rather than quietly becoming a negative, because a page that never loaded cannot test anything. Each of the four class denominators must reach 100, and every Wilson 95% interval must be no wider than plus or minus 10 points.
Rates are also perishable, and that is intentional. A study binds the exact build, detector implementation, registry, methodology, normalization, tracker catalog, Brave lists, Node, Playwright, Chromium, operating system, architecture and egress it measured. When any of those moves, the study stops applying and its rate is withdrawn automatically rather than being carried forward onto code it never measured. Eligibility is recomputed on every build, so this page and every report state the current answer, not a remembered one. A study also never becomes a reason to adjust the detector it measured: findings open the next study.
Two things that would otherwise read as detector error are declared as limits instead. Coverage boundaries name the surfaces never instrumented, so their absence is never evidence of absence. And the operator ceremonies that make a study releasable, sealing labels under separate authenticated identities, a protected reveal environment, a controlled single-use runner, and isolated attestation, are human steps outside this codebase; until they run, a study can be complete and still not publish.
Published schema errata
ScanReport v2 revisions r1 and r2 are immutable, so two wording corrections are published here instead of silently changing their JSON Schema bytes. E1: an advanced-matching identifier is recorded only when its parameter carries a non-empty value; the scanner inspects that value transiently for emptiness but never persists, exposes, interprets, or hash-validates it. E2: AB and BA describe which arm ran first in one randomized pair; one pair is not counterbalanced. The complete, versioned errata log is in the ScanReport v2 RFC.