| Next revision | Previous revision |
| programming:crawler:openwpm [2026/08/14 08:37] – New page: OpenWPM — instrumentation surfaces and their defaults, stateful/stateless as implemented, the release-to-Firefox pin, known pitfalls, maintenance status as of 2026-08-14, install/test results on this host, and 60 corpus papers' reporting. Proven karel.kubicek.claude | programming:crawler:openwpm [2026/08/21 08:32] (current) – [Open Questions] karelkubicek |
|---|
| OpenWPM {[englehardt2016online]} is the closest thing web privacy measurement has to a standard instrument: a Python platform that drives an **unbranded Firefox** through **Selenium**, and records what the browser did through a **privileged WebExtension** rather than through the automation protocol. It is the most widely shared specialised crawler in our corpus: **60 papers used or extended it**, against 21 each for the next two, on the mention-matching count over all 5,859 papers in [[Programming:Crawler#Which specialised crawlers actually get used|the comparison page]]. 59 of those 60 are inside the 1,120 papers that ran a crawl; the exception re-analysed someone else's OpenWPM data. | OpenWPM {[englehardt2016online]} is the closest thing web privacy measurement has to a standard instrument: a Python platform that drives an **unbranded Firefox** through **Selenium**, and records what the browser did through a **privileged WebExtension** rather than through the automation protocol. It is the most widely shared specialised crawler in our corpus: **60 papers used or extended it**, against 21 each for the next two, on the mention-matching count over all 5,859 papers in [[Programming:Crawler#Which specialised crawlers actually get used|the comparison page]]. 59 of those 60 are inside the 1,120 papers that ran a crawl; the exception re-analysed someone else's OpenWPM data. |
| |
| This page is about running it and reading papers that ran it: which instrumentation surfaces exist and which are silent by default, what stateful and stateless mean //in OpenWPM's implementation//, what the version number commits you to, and what its maintenance looks like today. For the choice between OpenWPM and Playwright, Tracker Radar Collector or a patched browser, see [[Programming:Crawler]]. For the design question of whether to keep browser state at all, see [[Programming:Stateful stateless]]. | This page is about running it and reading papers that ran it: which instrumentation surfaces exist and which are silent by default, what stateful and stateless mean //in OpenWPM's implementation//, what the version number commits you to, and what its maintenance looks like today. For the choice between OpenWPM and Playwright, Tracker Radar Collector or a patched browser, see [[Programming:Crawler]]. For the design question of whether to keep browser state at all, see [[Programming:Stateful stateless]], which also covers what a "reset" actually resets and why the browser's own partitioning defaults now decide part of the answer. |
| |
| <WRAP important> | <WRAP important> |
| Two findings from our corpus that should change how you write your methodology section: | Two findings from our corpus that should change how you write your methodology section: |
| |
| - **OpenWPM's own README asks researchers to publish the version number. 15 of the 60 papers do (25.0%).** The version is not cosmetic: each release pins one Firefox build, so ''OpenWPM v0.17.0'' ships Firefox 90 — and a 2024 paper crawled with it. But the pin is a default, not a guarantee, and only three of the fifteen papers say which browser they actually ran (see [[#An OpenWPM version is a Firefox version, by default]]). | - **OpenWPM's own README asks researchers to publish the version number. 15 of the 60 papers do (25.0%).** The version is not cosmetic: each release pins one Firefox build, so ''OpenWPM v0.17.0'' ships Firefox 90 — and a 2024 paper still crawled with that 2021 release. The pin is only a default, though: that paper is one of just three that name a browser too, and it ran Firefox 102 (see [[#An OpenWPM version is a Firefox version, by default]]). |
| - **Naming the version is still not enough.** Only **3 of the 60 papers name a single OpenWPM configuration key or output table** in their full text. Which instruments were switched on is the difference between a crawl that recorded JavaScript API calls and one that did not, and it is almost never stated. ''cookie_instrument'' is the **only** instrument enabled by default; everything else is off until you set it. | - **Naming the version is still not enough.** Only **3 of the 60 papers name a single OpenWPM configuration key or output table** in their full text. Which instruments were switched on is the difference between a crawl that recorded JavaScript API calls and one that did not, and it is almost never stated. ''cookie_instrument'' is the **only** instrument enabled by default; everything else is off until you set it. |
| </WRAP> | </WRAP> |
| * **A privileged WebExtension** using ''webRequest'', ''webNavigation'', ''cookies'' and ''dns'', plus three experimental APIs of its own (''sockets'', ''profileDirIO'', ''stackDump''). Because it uses privileged APIs, it can only be loaded by an **unbranded** or custom Firefox build with add-on security disabled — a documented requirement((''docs/Platform-Architecture.md'': "The Extension makes heavy use of privileged APIs and can only be installed on unbranded or custom builds of Firefox with add-on security disabled." Repository read at commit ''b9dd4c3'', 2026-08-14.)) and the reason ''scripts/install-firefox.sh'' downloads a specific build from Mozilla's CI rather than using whatever Firefox you have. The extension is still **Manifest V2**. | * **A privileged WebExtension** using ''webRequest'', ''webNavigation'', ''cookies'' and ''dns'', plus three experimental APIs of its own (''sockets'', ''profileDirIO'', ''stackDump''). Because it uses privileged APIs, it can only be loaded by an **unbranded** or custom Firefox build with add-on security disabled — a documented requirement((''docs/Platform-Architecture.md'': "The Extension makes heavy use of privileged APIs and can only be installed on unbranded or custom builds of Firefox with add-on security disabled." Repository read at commit ''b9dd4c3'', 2026-08-14.)) and the reason ''scripts/install-firefox.sh'' downloads a specific build from Mozilla's CI rather than using whatever Firefox you have. The extension is still **Manifest V2**. |
| * **A process architecture built for failure**: one ''TaskManager'' supervises N ''BrowserManager'' processes, each owning one Firefox and one geckodriver; a separate storage controller process serialises records. A browser that crashes is restarted and its profile restored, and the visit is recorded as incomplete rather than lost silently — in principle (see [[#Known pitfalls]]). | * **A process architecture built for failure**: one ''TaskManager'' supervises N ''BrowserManager'' processes, each owning one Firefox and one geckodriver; a separate storage controller process serialises records. A browser that crashes is restarted and its profile restored, and the visit is recorded as incomplete rather than lost silently — in principle (see [[#Known pitfalls]]). |
| * **Three output stores**: structured records to SQLite or Parquet (local, S3 or GCS), response bodies to LevelDB de-duplicated by MD5 content hash. Every record carries ''visit_id'', ''browser_id'' and ''instance_id'', which is what makes a multi-browser crawl analysable at all. | * **Three output stores**: structured records to SQLite or Parquet (local, S3 or GCS), response bodies to LevelDB de-duplicated by content hash. Instrument records carry ''visit_id'' and ''browser_id'', which is what makes a multi-browser crawl analysable at all; the Parquet path adds an ''instance_id'' per run for partitioning, and the SQLite schema has no such column. |
| * **Commands as objects**: a ''CommandSequence'' per site, built from ''GetCommand'', ''BrowseCommand'' (follow a random internal link), screenshot, page-source and profile-dump commands, with per-command timeouts and retries — plus your own ''BaseCommand'' subclasses, which is how most papers extend it. | * **Commands as objects**: a ''CommandSequence'' per site, built from ''GetCommand'', ''BrowseCommand'' (follow a random internal link), screenshot, page-source and profile-dump commands, with per-command timeouts and retries — plus your own ''BaseCommand'' subclasses, which is how most papers extend it. |
| |
| Each instrument is a boolean on ''BrowserParams'' and writes to its own table(s). The defaults matter more than the table: **a ''BrowserParams()'' with nothing set records cookies and nothing else.** | Each instrument is a boolean on ''BrowserParams'' and writes to its own table(s). The defaults matter more than the table: **a ''BrowserParams()'' with nothing set records cookies and nothing else.** |
| |
| ^ Instrument ^ What it records ^ Tables ^ Default ^ Status ^ | ^ Instrument ^ What it records ^ Tables ^ Default ^ Status(("works" here is from the code, the documentation and the papers that used it — **not** from a run on our host, where no browser ever launched. Only "broken" was confirmed by execution, which needs no browser. See [[#Installing it, and what we could and could not run]].)) ^ |
| | ''http_instrument'' | request and response headers, redirect chains, POST bodies, ''resource_type'', third-party flags, triggering and loading origin | ''http_requests'', ''http_responses'', ''http_redirects'' | off | works; headers for cached content are saved except for images((''docs/Configuration.md'', citing [[https://bugzilla.mozilla.org/show_bug.cgi?id=634073|Bugzilla 634073]]. OCSP POST bodies are also not recorded.)) | | | ''http_instrument'' | request and response headers, redirect chains, POST bodies, ''resource_type'', third-party flags, triggering and loading origin | ''http_requests'', ''http_responses'', ''http_redirects'' | off | works; headers for cached content are saved except for images((''docs/Configuration.md'', citing [[https://bugzilla.mozilla.org/show_bug.cgi?id=634073|Bugzilla 634073]]. OCSP POST bodies are also not recorded.)) | |
| | ''js_instrument'' | method calls with arguments and property accesses, for the APIs you configure | ''javascript'' | off | works; the default configuration is narrower than it looks — see below | | | ''js_instrument'' | method calls with arguments and property accesses, for the APIs you configure | ''javascript'' | off | works; the default configuration is narrower than it looks — see below | |
| ===== An OpenWPM version is a Firefox version, by default ===== | ===== An OpenWPM version is a Firefox version, by default ===== |
| |
| Every OpenWPM release pins one unbranded Firefox build, in one line of ''scripts/install-firefox.sh''. Reading that line out of every ''v*'' tag gives the mapping. The table below omits seven point releases for width (0.11.0, 0.14.1, 0.16.0, 0.19.0, 0.21.0, 0.24.0, 0.33.0); the complete map is in ''report_openwpm.mjs''. It is the reason the README asks for the version number: | Every OpenWPM release pins one unbranded Firefox build, in one line of ''scripts/install-firefox.sh''. Reading that line out of every ''v*'' tag gives the mapping. Note before you read it as an installation recipe: for every release below v0.32.0 the build that line points at **no longer exists** — see [[#Known pitfalls]]. The table below omits seven point releases for width (0.11.0, 0.14.1, 0.16.0, 0.19.0, 0.21.0, 0.24.0, 0.33.0); the complete map is in ''report_openwpm.mjs''. It is the reason the README asks for the version number: |
| |
| ^ OpenWPM ^ Tagged ^ Firefox ^ ^ OpenWPM ^ Tagged ^ Firefox ^ | ^ OpenWPM ^ Tagged ^ Firefox ^ ^ OpenWPM ^ Tagged ^ Firefox ^ |
| ===== Known pitfalls ===== | ===== Known pitfalls ===== |
| |
| | * **It is written to be forked, not imported — and a fork does not survive an upgrade.** There is no ''openwpm'' package on PyPI and no stable public API surface: the documented path clones the repository, builds a conda environment inside it, and expects your crawl script to live in that tree. The obvious thing to do next is to edit the tree, and it is the mistake. Every subsequent upgrade is then a merge against a codebase that has repeatedly rewritten the parts you edited (the whole architecture at v0.10.0, the command and storage interfaces at v0.14.0), so in practice the upgrade never happens: **a crawler built by editing OpenWPM stops being buildable roughly when its pinned Firefox stops being downloadable** (next bullet), which is why a three-year-old artefact so often cannot be run by its own authors. Contrast [[https://github.com/duckduckgo/tracker-radar-collector|DuckDuckGo's Tracker Radar Collector]], which is a library you depend on.\\ **Do this instead: treat it as a library it does not claim to be.** Pin a tagged release as a submodule or a ''git clone --branch v0.xx.0'' in your Dockerfile, keep every line you write in your own repository, and express changes as ''BaseCommand'' subclasses where the API allows and **monkey patches** where it does not — so an upgrade is a version bump plus whatever patches broke, and the patches are a short, reviewable list of exactly where you diverge from upstream. A worked example is the ALSAcnc crawler {[bouhoula2024_automated]}, whose ''cookie_crawler/utils/monkey_patches/'' is five files and 583 lines rebinding ''BrowserManager'', ''BrowserManagerHandle'', ''CommandSequence'', the storage-controller shutdown and the default screen resolution; on that footing it tracked upstream through v0.23.0 → v0.29.0 → v0.31.0 in its public history alone((''bouhoula/alsacnc'', ''docker/crawler/Dockerfile'', commits ''235a510'' (2024-06-22, v0.23.0), ''d5c449a'' (2024-09-24, v0.29.0) and ''3c6cfd1'' (2025-01-29, v0.31.0); the project began on v0.21.x before that repository was published. Read 2026-08-14.)) — while a fork of any of those releases would by now be unbuildable. Upstream agrees this is the right shape and says the project has not made it easy: "//Iirc other people keep the OpenWPM code and their code separated by using a git submodule. Tbh I always assumed that people would just check out one version and build on top of it and never upgrade. […] We should provide a better story here.//"((Maintainer ''vringar'' in [[https://github.com/openwpm/OpenWPM/issues/964|issue #964]], 2021-12-16, which also notes that PRs #743 and #753, released in v0.14.0, made keeping the two codebases separate easier. Read 2026-08-14.)) |
| | * **The pinned Firefox build is deleted after about a year — mirror it or your crawl is not reproducible.** ''scripts/install-firefox.sh'' fetches the unbranded build from Mozilla's **CI index**, not from a release archive, and those artefacts expire: the index entry for the build 0.35.0 pins reports ''expires: 2027-06-10''. We resolved the pinned URL for all 29 releases from v0.10.0 on 2026-08-14; **only the four 2026 releases (Firefox 148–152) still download. Everything up to v0.31.0 — including the Firefox 134 pinned as recently as January 2025 — returns HTTP 404.** ''install.sh'' therefore fails on any older release, which is the state most published artefacts are in; the failure has been [[https://github.com/openwpm/OpenWPM/issues/964|open since 2021]], and the current script at least names it in the error message. The branded build of the same version is still on ''archive.mozilla.org'' and is **not** a substitute — the instrumenting extension needs privileged APIs that only an unbranded build will load.\\ **Do this instead:** archive the ''target.tar.*'' you actually crawled with, alongside your data, and give its version and checksum in the paper. If you are reviving someone else's crawl, the practical escape hatch is the container image — ''docker pull openwpm/openwpm:0.17.0'' still works today (tags go back to 0.15.0, ''linux/amd64'' only) and the image bakes the Firefox binary in at ''/opt/firefox-bin''. Failing both, ask around: a handful of groups keep private copies of these tarballs and pass them between each other, which is not a reproducibility story anyone should be content with. |
| * **An empty database looks exactly like a complete one.** A crawl in which every browser failed to launch still produces a ''crawl-data.sqlite'' containing all 13 of OpenWPM's tables (14 with SQLite's own ''sqlite_sequence''), all of them empty — including ''incomplete_visits'', because a visit that never started is never recorded as incomplete. We produced exactly that file (see [[#Installing it, and what we could and could not run]]). **Print row counts per table and check ''crawl_history.command_status'' before you analyse anything.** A crawl that half-failed is worse than one that failed: it looks like a low-tracking result. | * **An empty database looks exactly like a complete one.** A crawl in which every browser failed to launch still produces a ''crawl-data.sqlite'' containing all 13 of OpenWPM's tables (14 with SQLite's own ''sqlite_sequence''), all of them empty — including ''incomplete_visits'', because a visit that never started is never recorded as incomplete. We produced exactly that file (see [[#Installing it, and what we could and could not run]]). **Print row counts per table and check ''crawl_history.command_status'' before you analyse anything.** A crawl that half-failed is worse than one that failed: it looks like a low-tracking result. |
| * **You are detectable, and specifically as OpenWPM.** Krumnow et al. {[krumnow2022_gullible]} analyse how detectable OpenWPM is in the wild, how resilient its recording is against a page that fights back, and how widespread OpenWPM-specific detection is. A tool 60 papers share is worth writing a detector for. ''bot_mitigation = True'' is not an answer to this: it performs up to ten random mouse moves, one scroll, and a random 1–7 second sleep, and its own documentation says these "aren't comprehensive and automated interaction with the site will still appear very bot-like". | * **You are detectable, and specifically as OpenWPM.** Krumnow et al. {[krumnow2022_gullible]} analyse how detectable OpenWPM is in the wild, how resilient its recording is against a page that fights back, and how widespread OpenWPM-specific detection is. A tool 60 papers share is worth writing a detector for. ''bot_mitigation = True'' is not an answer to this: it performs up to ten random mouse moves, one scroll, and a random 1–7 second sleep, and its own documentation says these "aren't comprehensive and automated interaction with the site will still appear very bot-like". |
| * **''headless'' and ''xvfb'' are not the same browser.** OpenWPM's ''display_mode'' takes ''native'', ''headless'' and ''xvfb''; ''xvfb'' runs a full Firefox inside a virtual display, while ''headless'' is Firefox's own headless mode, in which **WebGL is not supported**((''docs/Configuration.md'', which points at [[https://github.com/openwpm/OpenWPM/issues/448|issue #448]] for "additional factors to consider when picking a ''display_mode''". That issue is in fact a 2019 thread titled //Reduce the surface for bot detection//, still open and last updated in 2020, in which the mode choice is one sentence; checked 2026-08-14.)). If you are measuring fingerprinting, headless mode changes what the page can do and what your crawl looks like {[vastel2018_scanner]} — and note that the default JS collection would not have recorded WebGL calls anyway. Say which of the three you used; "headless" alone does not distinguish ''headless'' from ''xvfb'', and the extraction behind this page scores a paper that says "we ran under Xvfb" as not stating headlessness at all. | * **''headless'' and ''xvfb'' are not the same browser.** OpenWPM's ''display_mode'' takes ''native'', ''headless'' and ''xvfb''; ''xvfb'' runs a full Firefox inside a virtual display, while ''headless'' is Firefox's own headless mode, in which **WebGL is not supported**((''docs/Configuration.md'', which points at [[https://github.com/openwpm/OpenWPM/issues/448|issue #448]] for "additional factors to consider when picking a ''display_mode''". That issue is in fact a 2019 thread titled //Reduce the surface for bot detection//, still open and last updated in 2020, in which the mode choice is one sentence; checked 2026-08-14.)). If you are measuring fingerprinting, headless mode changes what the page can do and what your crawl looks like {[vastel2018_scanner]} — and note that the default JS collection would not have recorded WebGL calls anyway. Say which of the three you used; "headless" alone does not distinguish ''headless'' from ''xvfb'', and the extraction behind this page has no slot for it: the two papers of the 60 whose text mentions Xvfb were coerced to //opposite// values, headless for the 1-million-site paper and headful for {[zeber2020representativeness]}. Neither is wrong exactly; the dichotomy is. |
| * **It is Firefox, and only Firefox.** ''BrowserParams.browser'' accepts ''firefox'' and nothing else. A result about Chrome's behaviour cannot be obtained here, and a result about "the web" measured only in Firefox inherits Firefox's cookie policy, its tracking protection defaults and its API surface. The instrumenting extension is Manifest V2, which is itself part of why this is a Firefox tool. | * **It is Firefox, and only Firefox.** ''BrowserParams.browser'' accepts ''firefox'' and nothing else. A result about Chrome's behaviour cannot be obtained here, and a result about "the web" measured only in Firefox inherits Firefox's cookie policy, its tracking protection defaults and its API surface. The instrumenting extension is Manifest V2, which is itself part of why this is a Firefox tool. |
| * **Pin geckodriver, not just OpenWPM.** ''environment.yaml'' pins ''geckodriver=0.37.0''. With the current geckodriver release, **0.37.1** (2026-07-20), browser launch fails immediately: OpenWPM passes ''-remote-allow-system-access'' (it needs system access for the privileged profile APIs) and 0.37.1 refuses it — ''InvalidArgumentException: Argument --remote-allow-system-access can't be set via capabilities''. We measured this as a clean A/B: same Firefox, same Selenium 4.45.0, only the driver changed. Install from ''environment.yaml''; if you install geckodriver by hand, pin it. | * **Pin geckodriver, not just OpenWPM.** ''environment.yaml'' pins ''geckodriver=0.37.0''. With the current geckodriver release, **0.37.1** (2026-07-20), browser launch fails immediately: OpenWPM passes ''-remote-allow-system-access'' (it needs system access for the privileged profile APIs) and 0.37.1 refuses it — ''InvalidArgumentException: Argument --remote-allow-system-access can't be set via capabilities''. We measured this as a clean A/B: same Firefox, same Selenium 4.45.0, only the driver changed. Install from ''environment.yaml''; if you install geckodriver by hand, pin it. |
| * **''tracking_protection'' does not work.** It is documented as **NOT SUPPORTED** ([[https://github.com/openwpm/OpenWPM/issues/101|issue #101]]). To measure with protections on you set the underlying ''about:config'' preferences through ''BrowserParams.prefs'' and report exactly which ones. | * **''tracking_protection'' does not work.** It is documented as **NOT SUPPORTED** ([[https://github.com/openwpm/OpenWPM/issues/101|issue #101]]). To measure with protections on you set the underlying ''about:config'' preferences through ''BrowserParams.prefs'' and report exactly which ones. |
| * **Long crawls need the watchdogs, and they are off by default.** ''memory_watchdog'' and ''process_watchdog'' (which kills orphaned geckodriver and Xvfb processes) exist for cloud-scale crawls; ''maximum_profile_size'' recycles a browser whose profile has grown too large — relevant precisely in a stateful crawl, where the profile is what you are accumulating. ''failure_limit'' defaults to ''2 × num_browsers + 10'' consecutive command failures before the crawl aborts, which on a large list is a threshold you should choose deliberately. | * **Long crawls need the watchdogs, and they are off by default.** ''memory_watchdog'' and ''process_watchdog'' (which kills orphaned geckodriver and Xvfb processes) exist for cloud-scale crawls; ''maximum_profile_size'' recycles a browser whose profile has grown too large — relevant precisely in a stateful crawl, where the profile is what you are accumulating. ''failure_limit'' defaults to ''2 × num_browsers + 10'' consecutive command failures before the crawl aborts, which on a large list is a threshold you should choose deliberately. |
| * **Response bodies are de-duplicated by MD5.** ''save_content'' stores each distinct body once in LevelDB, keyed by content hash, and ''http_responses.content_hash'' is the join. Count responses in ''http_responses'', never rows in LevelDB. | * **The content hash is SHA-256, whatever the documentation says.** ''save_content'' stores each distinct body once in LevelDB keyed by that hash, and ''http_responses.content_hash'' is the join — so count responses in ''http_responses'', never rows in LevelDB. ''docs/Configuration.md'' calls it an **md5** hash in two places; the extension computes ''crypto.subtle.digest("SHA-256", …)''((''Extension/src/lib/sha256.ts'' and ''response-body-listener.ts'' at commit ''b9dd4c3''; ''docs/Configuration.md'' lines 419–420 still say md5. Hashing a body with MD5 to look it up will simply miss.)). Also: ''save_content'' does nothing unless you pass an unstructured storage provider to ''TaskManager'' — with ''None'' every body is dropped with one logged error and no other trace. |
| * **''num_browsers'' is not free parallelism.** Each browser is a full Firefox with its own profile and its own state; see the per-browser cookie jar point above, and remember that concurrency changes what the sites see (N simultaneous connections from one address) and therefore what they serve {[jueckstock2021_realistic]}. | * **''num_browsers'' is not free parallelism.** Each browser is a full Firefox with its own profile and its own state; see the per-browser cookie jar point above, and remember that concurrency changes what the sites see (N simultaneous connections from one address) and therefore what they serve {[jueckstock2021_realistic]}. |
| * **Two repetitions of the same OpenWPM crawl do not agree.** Demir et al. {[demir2022_reproducibility]} measured the variation between repetitions of the same web measurement, and Demir et al. {[demir2023_similarity]} between the same measurement under different setups. Neither is an OpenWPM defect; both mean a single OpenWPM crawl is a point estimate. | * **Two repetitions of the same OpenWPM crawl do not agree.** Demir et al. {[demir2022_reproducibility]} measured the variation between repetitions of the same web measurement, and Demir et al. {[demir2023_similarity]} between the same measurement under different setups. Neither is an OpenWPM defect; both mean a single OpenWPM crawl is a point estimate. |
| | Release cadence, 2026 | four releases: 0.32.0 (Mar), 0.33.0 (Mar), 0.34.0 (May), 0.35.0 (Jun) — tracking Firefox 148→152 | | | Release cadence, 2026 | four releases: 0.32.0 (Mar), 0.33.0 (Mar), 0.34.0 (May), 0.35.0 (Jun) — tracking Firefox 148→152 | |
| | Release cadence, 2025 | **one release** (0.31.0, 19 January), then a 13-month gap; the newest release spent 2025 pinned to Firefox 134 while Firefox reached 147 | | | Release cadence, 2025 | **one release** (0.31.0, 19 January), then a 13-month gap; the newest release spent 2025 pinned to Firefox 134 while Firefox reached 147 | |
| | Commits per year | 569 (2019), 380 (2020), 85, 28, 23, **11 (2024), 12 (2025)**, 72 (2026 to 11 July) | | | Commits per year | 569 (2019), 380 (2020), 85, 28, 23, **11 (2024), 12 (2025)**, 73 (2026, to the pinned commit ''b9dd4c3'') | |
| | Contributors | 63 all-time; **72 of the 73 commits in 2026 are by one person** (''vringar'', Stefan Zabka) | | | Contributors | 63 all-time; of the **73 commits in 2026** up to the pinned commit, **72 are by one person** (''vringar'', Stefan Zabka) | |
| | CI | "Tests and linting" runs on a two-day cron and **passed on 2026-08-13**; CodeQL passing | | | CI | "Tests and linting" runs on a two-day cron and **passed on 2026-08-13**; CodeQL passing | |
| | Container image | ''openwpm/openwpm'' on Docker Hub, rebuilt per commit (''latest'' 2026-08-02) — **''linux/amd64'' only** | | | Container image | ''openwpm/openwpm'' on Docker Hub, rebuilt per commit (''latest'' 2026-08-02) — **''linux/amd64'' only** | |
| </code> | </code> |
| |
| We did not isolate the cause and do not claim one: installing the same ''.xpi'' into the same Nightly by hand succeeds (''INSTALL OK id= openwpm@mozilla.org''), so the failure is in what the extension is allowed to //do// once loaded, not in loading it. The practical answer on ARM is the amd64 container under emulation, or an x86-64 machine. | We did not isolate the cause and do not claim one: installing the same ''.xpi'' into the same Nightly by hand succeeds (''INSTALL OK id= openwpm@mozilla.org''), so the failure is in what the extension is allowed to //do// once loaded, not in loading it. The practical answer on ARM is the amd64 container under emulation, or an x86-64 machine. Neither could be tested here — this host has no Docker and cannot execute x86-64 binaries at all — so treat the first as what it is: the project's own recommended path (its README says OpenWPM "is commonly used via the docker container that this repo builds", and is tested on Ubuntu 24.04 in CI), on an image that exists, is rebuilt per commit and is amd64-only. |
| |
| What we //could// run on this host, and what it establishes: | What we //could// run on this host, and what it establishes: |
| from openwpm.commands.browser_commands import GetCommand | from openwpm.commands.browser_commands import GetCommand |
| from openwpm.config import BrowserParams, ManagerParams | from openwpm.config import BrowserParams, ManagerParams |
| | from openwpm.storage.leveldb import LevelDbProvider |
| from openwpm.storage.sql_provider import SQLiteStorageProvider | from openwpm.storage.sql_provider import SQLiteStorageProvider |
| from openwpm.task_manager import TaskManager | from openwpm.task_manager import TaskManager |
| bp.save_content = "script" # response bodies for scripts only, into LevelDB | bp.save_content = "script" # response bodies for scripts only, into LevelDB |
| browser_params = [bp] | browser_params = [bp] |
| | |
| | # save_content needs an UNSTRUCTURED storage provider as well. Pass None and |
| | # OpenWPM logs one error per body and drops it (storage_controller.py: "Tried to |
| | # save content while not having provided any unstructured storage provider"), |
| | # which no per-table row count can see — exactly the silent-instrument failure |
| | # this script exists to catch. |
| | CONTENT = DATA / "content.ldb" |
| |
| with TaskManager( | with TaskManager( |
| browser_params, | browser_params, |
| SQLiteStorageProvider(DATA / "crawl-data.sqlite"), | SQLiteStorageProvider(DATA / "crawl-data.sqlite"), |
| None, | LevelDbProvider(CONTENT), |
| ) as manager: | ) as manager: |
| for i, site in enumerate(SITES): | for i, site in enumerate(SITES): |
| for row in db.execute("select hostname, addresses, is_TRR from dns_responses"): | for row in db.execute("select hostname, addresses, is_TRR from dns_responses"): |
| print(" %-24s %-30s trr=%s" % row) | print(" %-24s %-30s trr=%s" % row) |
| | |
| | # The bodies do not live in SQLite, so a per-table row count cannot see them. |
| | print("\nresponse bodies in LevelDB:") |
| | try: |
| | import plyvel |
| | |
| | db2 = plyvel.DB(str(CONTENT)) |
| | print(f" {sum(1 for _ in db2.iterator(include_value=False))} distinct bodies" |
| | f" (keyed by content_hash)") |
| | except Exception as e: # no plyvel, or nothing was written |
| | print(f" could not read {CONTENT}: {e}") |
| </file> | </file> |
| |
| | IEEE S&P | 1 | 1.7% | 110 | 0.9% | | | IEEE S&P | 1 | 1.7% | 110 | 0.9% | |
| |
| **OpenWPM is a PETS instrument first.** Nearly a fifth of PoPETs crawling papers use it, against 3% or less at CCS, NDSS and USENIX Security and one paper in fifteen years at IEEE S&P. If your reviewers come from the security venues, they will not treat "we used OpenWPM" as self-explanatory; if they come from PETS, they may. | **OpenWPM is a PETS instrument first.** Nearly a fifth of PoPETs crawling papers use it, against about 3% at CCS, NDSS and USENIX Security, and one paper in the whole corpus at IEEE S&P. If your reviewers come from the security venues, they will not treat "we used OpenWPM" as self-explanatory; if they come from PETS, they may. |
| |
| ^ Year ^ OpenWPM papers ^ Crawling papers ^ OpenWPM's share ^ | ^ Year ^ OpenWPM papers ^ Crawling papers ^ OpenWPM's share ^ |
| |
| * **As a reader**, you cannot tell from most papers whether the ''javascript'' table was populated. If a paper reports "no fingerprinting on X% of sites" without saying that ''js_instrument'' was on and with what settings, the absence may be the instrument's. | * **As a reader**, you cannot tell from most papers whether the ''javascript'' table was populated. If a paper reports "no fingerprinting on X% of sites" without saying that ''js_instrument'' was on and with what settings, the absence may be the instrument's. |
| * **As an author**, one sentence fixes it: //"OpenWPM 0.35.0 (Firefox 152), stateless, ''http_instrument'', ''js_instrument'' with ''collection_fingerprinting'' plus WebGL, ''cookie_instrument'', ''display_mode='xvfb''', ''num_browsers=8''."// That is fully reproducible and costs a line. One paper in the corpus does close to this and is worth copying: {[agarwal2020_stop]} writes that its "updates include enabling the ''http_instrument'' which logs HTTP responses, requests and redirects, using the selenium headless browser to perform crawling and, setting ''js_instrument'', ''cookie_instrum''[''ent'']…", and names the extensions it turned //off// as well. | * **As an author**, one sentence fixes it: //"OpenWPM 0.35.0 (Firefox 152), stateless, ''http_instrument'', ''js_instrument'' with ''collection_fingerprinting'' plus WebGL, ''cookie_instrument'', ''display_mode='xvfb''', ''num_browsers=8''."// That is fully reproducible and costs a line. One paper in the corpus does close to this and is worth copying: {[agarwal2020_stop]} writes that its "updates include enabling the ''http_instrument'' which logs HTTP responses, requests and redirects, using the selenium headless browser to perform crawling and, setting ''js_instrument'', ''cookie_instrument''…", and names the extensions it turned //off// as well. |
| |
| ==== What they use it for ==== | ==== What they use it for ==== |
| |
| Enum counts over ''classification[].target'' tuples in the 60 papers, publishable as a ranking. The catch-all ''other'' bucket (24 tuples) is omitted from the table but would rank fourth: | Enum counts over ''classification[].target'' tuples in the 60 papers, publishable as a ranking. This is the top nine; the catch-all ''other'' (24 tuples) would rank fifth and is omitted, as are ''malware'' and ''javascript'' (3 each) below the cut: |
| |
| ^ What the paper classified ^ Tuples ^ | ^ What the paper classified ^ Tuples ^ |
| ==== Forks and extensions are the norm, not the exception ==== | ==== Forks and extensions are the norm, not the exception ==== |
| |
| Three of the 60 papers //produced// something rather than merely using it, and several more describe local modifications: | Three of the 60 papers //produced// rather than used: OpenWPM itself {[englehardt2016online]}, OpenWPM-Mobile {[das2018_sixth]}, and one paper's own extended instrumentation {[acar2020_noboundaries]}. Several more describe local modifications: |
| |
| * **OpenWPM-Mobile** {[das2018_sixth]} adds mobile-focused instrumentation; OmniCrawl {[cassel2022_omnicrawl]} later ran it alongside real mobile browsers and used the comparison to show what emulation misses. | * **OpenWPM-Mobile** {[das2018_sixth]} adds mobile-focused instrumentation; OmniCrawl {[cassel2022_omnicrawl]} later ran it alongside real mobile browsers and used the comparison to show what emulation misses. |
| * **Extended JS instrumentation.** "No boundaries" {[acar2020_noboundaries]} extended OpenWPM's JavaScript instrumentation to a further set of DOM properties and function calls — the standard move when ''collection_fingerprinting'' is not enough, and the one to copy. | * **Extended JS instrumentation.** "No boundaries" {[acar2020_noboundaries]} extended OpenWPM's JavaScript instrumentation to a further set of DOM properties and function calls — the standard move when ''collection_fingerprinting'' is not enough, and the one to copy. |
| * Papers also describe "a lightly modified version of OpenWPM" without saying what was modified. If you extend it, publish the diff with your artefact; a modified instrument with no published modification is not a shared instrument. | * Papers also describe "a lightly modified version of OpenWPM" without saying what was modified. If you extend it, publish the diff with your artefact; a modified instrument with no published modification is not a shared instrument. |
| | * **How you modify it decides whether your crawler outlives the paper.** A fork you edit in place cannot practically be upgraded; the same changes written as ''BaseCommand'' subclasses and monkey patches over a pinned release can be, and have been. This is the first entry under [[#Known pitfalls]], with a worked example. |
| |
| **33 of the 60 (55.0%) release an artefact publicly**, against 51.8% of other crawling papers — no better than the field, for a tool whose whole value is shared instrumentation. | **33 of the 60 (55.0%) release an artefact publicly**, against 51.8% of other crawling papers — no better than the field, for a tool whose whole value is shared instrumentation. |
| * **Venue coverage.** Seven venues only, so a paper count is a lower bound on a tool's standing: Krumnow et al., whose OpenWPM detection study this page leans on, was published at CoNEXT, outside them. The scope and selection funnel are on [[literature:corpus]]. | * **Venue coverage.** Seven venues only, so a paper count is a lower bound on a tool's standing: Krumnow et al., whose OpenWPM detection study this page leans on, was published at CoNEXT, outside them. The scope and selection funnel are on [[literature:corpus]]. |
| * **''crawled'' is defined** as a paper whose crawl configuration was recorded or whose study types include an automated web crawl (1,120 papers, 19.1% of the corpus). 59 of the 60 OpenWPM papers are inside it; the one outside re-analysed a dataset someone else crawled. | * **''crawled'' is defined** as a paper whose crawl configuration was recorded or whose study types include an automated web crawl (1,120 papers, 19.1% of the corpus). 59 of the 60 OpenWPM papers are inside it; the one outside re-analysed a dataset someone else crawled. |
| | |
| | ===== Papers to read first ===== |
| | |
| | Six, in the order that is useful if you are about to run an OpenWPM crawl rather than write about one: |
| | |
| | - **{[englehardt2016online]} — the tool, and the census it was built for.** Read the platform section for the architecture this page describes, and the configuration table for how a real large crawl is split into stateless and stateful parts. |
| | - **{[krumnow2022_gullible]} — how detectable OpenWPM is in the wild.** Read it before you interpret any null result as absence of tracking. Published at CoNEXT, outside this corpus's seven venues. |
| | - **{[demir2022_reproducibility]} and {[demir2023_similarity]} — how far the same measurement moves when you repeat it, and when you change the setup.** Together they are the reason a single crawl needs error bars. |
| | - **{[zeber2020representativeness]} — how far any crawl is from real browsing.** The limit on what an OpenWPM number can be a number //about//. |
| | - **{[acar2020_noboundaries]} — the model for extending the JavaScript instrumentation** past ''collection_fingerprinting'', which is what you will need if your question is about a specific API. |
| | - **{[agarwal2020_stop]} — the methodology paragraph to copy.** It is the one paper in the corpus that names the instruments it enabled, by their parameter names, and the extensions it disabled. |
| |
| ===== What to report if you use it ===== | ===== What to report if you use it ===== |
| - **''tp_cookies'', ''donottrack'', and any ''prefs''** you set. The defaults are a policy choice, and ''tp_cookies'' defaults to accepting everything. | - **''tp_cookies'', ''donottrack'', and any ''prefs''** you set. The defaults are a policy choice, and ''tp_cookies'' defaults to accepting everything. |
| - **Failure accounting**: how many visits are in ''incomplete_visits'', how many commands have a non-''ok'' ''command_status'', and what you did about them. This is the number nobody publishes and every reviewer should ask for. | - **Failure accounting**: how many visits are in ''incomplete_visits'', how many commands have a non-''ok'' ''command_status'', and what you did about them. This is the number nobody publishes and every reviewer should ask for. |
| | - **The Firefox binary itself**, archived with your artefact and checksummed. Mozilla's CI deletes the unbranded build about a year after it is produced, so a version number alone does not let anyone rebuild your crawler (see [[#Known pitfalls]]). |
| - **Your diff**, if you modified the instrumentation, with the artefact (see [[:Artifacts]], a page this wiki still owes you). | - **Your diff**, if you modified the instrumentation, with the artefact (see [[:Artifacts]], a page this wiki still owes you). |
| | - **The citation the project asks for**, which is the CCS 2016 paper {[englehardt2016online]} — its README says so — in the same sentence as the version, so a reader gets the method and the build together. |
| |
| ===== Open Questions ===== | ===== Open Questions ===== |
| |
| <wrap todo> | <WRAP todo> |
| * Nobody has published what OpenWPM's default ''collection_fingerprinting'' misses on a modern fingerprinting corpus. The list has not changed since 2020 and the surface has; measuring the gap is a small paper. | * Nobody has published what OpenWPM's default ''collection_fingerprinting'' misses on a modern fingerprinting corpus. The list has not changed since 2020 and the surface has; measuring the gap is a small paper. |
| * How much the unbranded Firefox build differs from a stock Firefox of the same version, in fingerprint and in behaviour, is unmeasured — the same question [[Programming:Crawler]] raises for Playwright's patched builds. | * How much the unbranded Firefox build differs from a stock Firefox of the same version, in fingerprint and in behaviour, is unmeasured — the same question [[Programming:Crawler]] raises for Playwright's patched builds. |
| * Whether the per-browser cookie-jar partition in a stateful multi-browser crawl changes published results, and by how much. Every stateful OpenWPM crawl with ''num_browsers > 1'' has this property; no paper we found reports ''num_browsers'' alongside a stateful claim. | * Whether the per-browser cookie-jar partition in a stateful multi-browser crawl changes published results, and by how much. Every stateful OpenWPM crawl with ''num_browsers > 1'' has this property; no paper we found reports ''num_browsers'' alongside a stateful claim. |
| * Where ARM support would come from: Mozilla's ''add-on-devel'' build matrix, or an OpenWPM-side path that does not need an unbranded build. | * Where ARM support would come from: Mozilla's ''add-on-devel'' build matrix, or an OpenWPM-side path that does not need an unbranded build. |
| </wrap> | </WRAP> |
| |
| ====== References ====== | ====== References ====== |