Maximize what the real detectors exercise on CI:
- BotD: assert the aggregate detect().bot==false AND every individual detector
via getDetections() (webDriver/userAgent/appVersion/plugins/process/...). This
restores the per-detector granularity of the deleted hand-rolled test_botd_*,
but from the real library, with diagnostics on which detector flagged.
- FpJS: visitorId present + stable across two launches, AND a rich component
surface (>=15 signals — a suppressed/thin surface is itself a tell). We don't
hard-assert zero errored components (some are legitimately unsupported per
browser); visitorId stability is the authoritative aggregate check.
All 3 green locally against firefox-9.
These mirrored BotD's individual detectors (webdriver/appVersion/userAgent/
functionBind/productSub/process/evalLength/languages/plugins/mimeTypes/
distinctive-props/documentAttributes/windowSize/webGL) — our reverse-engineering
of BotD from before we ran the real library. Now that test_detectors_e2e.py runs
the actual @fingerprintjs/botd against the binary on CI (asserts bot===false),
they're redundant. Kept the complementary/unique tests: fpjs-surface, pinning,
sannysoft + fpscanner (which mimic detectors we do NOT run, so not covered by
BotD/FpJS), and the determinism/consistency suite.
Instead of only our hand-rolled signal checks, load the actual MIT detection
libraries against the patched binary and assert it isn't flagged:
- BotD (the client-side bot detector FingerprintJS Pro itself uses): detect()
must return bot=false (no automation/headless tell).
- FingerprintJS OSS: visitorId present and stable across two fresh launches
with the same seed (drift = per-session entropy = a bot tell).
Hermetic: the libs are vendored (tests/vendor/, pinned, MIT) and served from a
localhost server — no external CDN (Firefox tracking-protection blocks it
anyway), no IP/network dependency, runs identically on a dev box and the GitHub
runner. Both green locally against firefox-9.
Proves the patched nsProtocolProxyService end to end: the binary performs the
RFC1929 user/pass handshake with the configured socks_username/password and
relays the page through the proxy — something Playwright's own proxy= can't do,
and which test_proxy only unit-checks at the pref level.
Fully hermetic so it runs identically locally and on the GitHub runner: a local
SOCKS5 server (requires auth, records the creds it saw) + a local HTTP target,
with the localhost target forced through the proxy via allow_hijacking_localhost
+ no_proxies_on="". No external site, no secrets. 3/3 local.
Running the full e2e on GitHub (xvfb) surfaced 2 env-sensitive failures, neither
a binary bug:
- test_hover_triggers_mouseenter read window.__h immediately after hover(); the
mouseenter can land a beat later on a virtual display. Use wait_for_function
(still fails if the event genuinely never fires). 5/5 locally now.
- test_not_blocked_behind_tcp_only_socks needs a remote origin loaded fully
through the proxy to inject the synthetic srflx; that path is environment-
sensitive on a datacenter CI box. Keep the hard "zero candidates = blocked =
FAIL" check, but skip (not fail) if the srflx didn't engage — validated locally.
Maximize what's tested on CI: a new e2e job fetches the public firefox-9 binary
and runs all 138 @pytest.mark.e2e via scripts/run_e2e.py under xvfb-run. The
wrapper launches Firefox HEADED on a (virtual) display, which is stable on the
hosted runners — unlike the true-headless drive-gate that flaked. Secret-free
(public binary + local fake-SOCKS for webrtc), so safe in public CI; the proxy
realness gate (fppro) stays local. Trial-running now; will tune if xvfb/timeouts
need it.
firefox-9 is published (build 5/5, drive-gate 5/5, full e2e 137/1-skip, fppro
ALL CRITICAL CLEAN, WebRTC real, sha256-verified download path). Point the
wrapper at it.
Guard: ensure_binary() now refuses any version in BROKEN_VERSIONS (={firefox-8})
with a clear error instead of handing over an undrivable binary — a cached
firefox-8 from before this bump would otherwise keep being used silently. Tests:
the current BINARY_VERSION can never be in BROKEN_VERSIONS; firefox-8 stays
flagged; ensure_binary("firefox-8") raises.
B6: pin every third-party action in the build/publish path to an immutable
commit SHA (a retagged actions/checkout or action-gh-release would otherwise
inject code into the binary users download). The other workflows (tests, webrtc,
launch-matrix) handle no secrets, so they're left on tags.
B4: the playwright pin lived in two workflow files with no shared source. Move
it to scripts/playwright_pin.txt that both read, so they can't drift. The drive
gate already ENFORCES playwright<->juggler compatibility (an incompatible pin
fails the launch/drive and nothing publishes); the file is the single bump point
when the juggler is re-synced.
The 138 @pytest.mark.e2e tests were doubly inactive: deselected by addopts AND
skipped without a cached binary — and 3 of the 6 per-file firefox_binary
fixtures silently ignored INVPW_BINARY_PATH, so they'd test whatever was cached
even when you pointed the suite elsewhere (a false-confidence trap).
- Centralize firefox_binary into conftest.py (env INVPW_BINARY_PATH → cache →
skip); delete the 6 duplicates. Unify test_webrtc_realness onto the same env.
- scripts/run_e2e.py: one command that runs ALL e2e against a chosen binary,
with reruns so an under-load interaction flake (dblclick/hover pass 3/3 in
isolation) self-heals while a real break fails every attempt. The webrtc e2e
fake a TCP-only SOCKS locally, so the suite is offline. This is the MANDATORY
pre-release browser gate (local — hosted runners are too interaction-flaky).
- Running the suite against firefox-9 surfaced a real gap: `invisible_playwright
fetch --force` was unrecognized (the subparser took no args) though the e2e
test + docstring expect it. Implement it: drop the cached version dir, refetch.
- Add pytest-rerunfailures + playwright to the dev extras.
Baseline against firefox-9: 136 passed, 1 skipped (linux_only on win host),
1 was the --force gap now fixed.
The free hosted runners (windows-latest worst) are content-process unstable
under a heavy headless interaction sequence: clicks/moves cascade into
context-destroyed / selector-timeout / eval-CSP, even across 3 retries, even on
linux-arm64. That's an environment limit, not a binary defect (the binaries
drive 20/20 locally and the stable legs pass).
So: SMOKE (launch + http page + UA + webdriver + DOM read) runs on all 5 legs —
the firefox-8/juggler catcher, robust everywhere. FULL (+ mouse/keyboard/canvas/
navsurface, the firefox-2 class) runs only on linux-x86_64; the interaction code
is platform-identical JS (omni.ja), so one reliable full run covers every
platform, and win interaction stays covered by local pre-release testing.
windows-latest headless kept flaking on special-scheme pages: data: URLs get
re-normalized (re-nav), about:blank + redundant goto destroys the context, and
both can carry a CSP that blocks eval(). Serve the test page over a real
http://127.0.0.1 instead — none of those quirks, and it adds real-navigation
coverage (await a response). Evaluates stay arrow-functions (no eval), listeners
are inline-script (no on* attrs), and transient context-destroyed/detached/timeout
gets up to 2 retries. A genuinely broken binary fails all 3 attempts.
windows-latest failed both attempts with "Page.evaluate: call to eval() blocked
by CSP". Bare-string evaluates (page.evaluate("navigator.userAgent")) make
Playwright fall back to eval(), which a page CSP blocks. Pass arrow functions
instead (called via callFunction, never eval'd), and wire the click listener
with addEventListener instead of an inline onclick (also CSP-sensitive). Both
are Playwright best practice and platform-agnostic.
linux+macOS drive went green but windows-latest kept throwing "execution
context destroyed by navigation" at a wandering evaluate (passed 20/20 win-local,
no browser crash logged). Root cause: the unencoded data: URL gets re-normalized
(re-navigated to its percent-encoded form) by Firefox; the slower win runner
races that re-nav against the evaluates. about:blank is canonical and never
re-navigates, so the DOM is now built there via innerHTML. Also add one logged
retry on transient context-destroyed/detached (a broken binary fails both).
Re-running the enriched gate on the real binaries exposed a ~1-in-5 flake: the
iframe probe (nested data: / srcdoc) re-navigates, so Juggler's frame id changes
mid-check ("execution context destroyed" / "Frame was detached"). set_content
isn't an option either — this build rejects its document.write ("operation is
insecure").
Drop the iframe from the gate: a same-origin srcdoc iframe is a weak proxy for
the cross-origin issue #20 anyway. The page is now a plain subframe-free
goto(data:), and the mouse/keyboard/canvas-determinism/navigator-surface checks
(the firefox-2 class + stealth smoke) stay. 20/20 clean locally. The faithful
#20 sentinel (tests/test_cross_origin_iframe.py, two localhost origins) should
be wired as its own e2e gate job.
An adversarial audit of the pipeline found the drive gate only did goto+evaluate,
so several historically-shipped breakages would still pass it green:
- firefox-2 (jugglerSendMouseEvent missing) — no mouse/keyboard was tested
- issue #20 (cross-origin iframe content_frame() None) — no iframe was tested
- canvas non-determinism (stealth seed) and headless navigator tells
ci_drive_gate.py now clicks a button, moves the mouse, types into an input,
reaches into an iframe, checks an identical canvas draw is byte-stable, and
checks navigator.languages/plugins — all offline (data: URLs), GPU-free, no
proxy. Validated against the real build.
Pipeline hardening from the same audit:
- Windows: stop swallowing `mach package` failure and never fall back to the
dev tree dist/bin (that masked the firefox-7/8 packaging bugs)
- macOS: plutil -lint Info.plist + required-key checks (a malformed plist ships
fine through a headless drive but Finder calls the .app "damaged")
- publish: assert all 5 archives present + fail_on_unmatched_files (no silent
partial release if a build leg drops out)
gh release download 404s ("release not found") on a draft tag when the token
is contents:read — GitHub only shows drafts to tokens with push access. The
workflow still only reads assets; the scope bump is purely for draft visibility.
The old gate ran firefox --headless --screenshot, which renders fine even
when the juggler automation layer is missing from the package — so a binary
Playwright can't actually drive (firefox-8) passed and shipped broken.
Replace it with a real drive gate: a 5-leg matrix that launches each binary
over the juggler pipe on its native runner, loads a page, and round-trips JS
(also asserts navigator.webdriver stays hidden). Headless and no screenshot,
so it stays GPU-free on the hosted runners and needs no proxy or secrets.
Same logic is reusable standalone via verify-assets.yml to drive-test an
existing release's assets without a rebuild.
release.yml builds linux-x64/arm64 + win-x64 (cross) on free Linux runners and
macos-arm64/x64 on native Mac runners; packages per the wrapper contract
(juggler-gated so binaries are Playwright-drivable, issue-#14 symlink-safe via
cp -aL), validate_release.py gate, ad-hoc macOS codesign, DRAFT publish.
constants.py: arm64 + darwin ARCHIVE_NAME + BINARY_ENTRY_REL (Firefox.app).
download.py: macOS post-extract xattr quarantine strip.
BINARY_VERSION unchanged (firefox-8); the juggler-fixed firefox-9 is a separate
release cut + pin bump.
firefox-8 carries the WebRTC fixes: behind a proxy, ICE now completes with an
mDNS .local host and a server-reflexive candidate on the proxy IP (genuine
nICEr priority/foundation) instead of coming up blocked, and IPv6 host
candidates are suppressed. Binary published on the releases page; validated on
both Windows and Linux via scripts/validate_release.py.
Unit sentinels (run in CI) assert the rules a real WebRTC profile must meet:
host candidate is mDNS .local (never a raw LAN IP), the synthetic srflx carries
the egress IP with a genuine nICEr priority (rejecting the old local_pref
0xFFFF) and a foundation distinct from the host one, and CreepJS's resolver
returns the egress (and reads a host-only SDP as blocked).
e2e tests launch the binary and check the live gather. "Behind a proxy" is
reproduced without any external proxy: an in-process SOCKS5 server relays TCP
CONNECT but refuses UDP ASSOCIATE (a TCP-only residential proxy), and the
egress IP is injected as an RFC 5737 TEST-NET address.
test_not_blocked_behind_tcp_only_socks guards the gather-fails-behind-proxy bug.
webrtc-e2e.yml runs the e2e on demand (needs a binary that carries the fixes).
Refine timezone="auto" so it ALWAYS resolves (drop the "host" sentinel):
- ""/"auto" resolve from the proxy egress when a proxy is set, else from the
host own public IP (direct lookup); an explicit zone is the only opt-out.
- on failure: with a proxy raise; without a proxy fall back to the host TZ.
GeoIP DB now auto-updates against daijro/geoip-all-in-one weekly rebuild:
cache the latest, re-check after GEOIP_REFRESH_DAYS (7), prune old tags,
reuse a stale cache offline; GEOIP_MMDB_VERSION is only the cold fallback.
tests: test_geo.py (37) + test_geoip_update.py; full unit suite 429 green
plus 8 live combinations (proxy / no-proxy / explicit / failing / freshness).
Follow-up to the timezone="auto" feature. The launcher/config timezone
docstrings still said "empty means host TZ", now incomplete since
""+proxy defaults to auto.
- launcher timezone arg: full precedence (auto default, host escape
hatch, fail-early on unresolvable zone behind a proxy)
- config.get_default_stealth_prefs: note it does NOT resolve "auto"
(pure pref builder, no proxy/network context)
- CHANGELOG: Unreleased entry for timezone="auto" + new deps
A proxy in a different country paired with the host timezone is the
classic timezone_mismatch signal, so a session with a proxy and no
explicit timezone now resolves the zone automatically.
- discover the egress IP through the proxy (SOCKS via requests[socks]),
map it to an IANA zone with an offline mmdb (daijro/geoip-all-in-one,
downloaded + cached like the Firefox binary; GPL so not vendored)
- precedence: explicit zone wins; ""+proxy and "auto"+proxy resolve;
""/"auto" without a proxy stay host; "host"/"local" force host TZ
- fail-early when a proxy is set but the zone cannot be resolved, never
a silent host-TZ fallback
- deps: requests[socks], maxminddb, tzdata (zoneinfo ships no DB on Windows)
- resolve_session_timezone / ensure_geoip_mmdb exported for integrations
Live smoke test caught a footgun: passing headless=True directly to
playwright.firefox.launch() with our prefs puts Firefox in true
headless mode (no rendering pipeline) which breaks canvas/audio/WebGL
fingerprint coherence. InvisiblePlaywright translates user-facing
headless=True to Playwright headless=False + virtual display
automatically; the new public helpers do not, so the docstring +
README now flag this explicitly.
Verified: same prefs + headless=False via firefox.launch() reaches
bot.sannysoft.com with 23 passed / 0 failed, matching what
InvisiblePlaywright produces.
Adds invisible_playwright.config module with:
- get_default_stealth_prefs(seed, *, pin, locale, timezone,
extra_prefs, humanize, virtual_display) -> dict
- get_default_args() -> list
Both also re-exported at the package root alongside the existing
InvisiblePlaywright. ensure_binary is also re-exported there for
parity with the cloakbrowser.download.ensure_binary integration
pattern that downstream projects (Skyvern PR #5340, crawlee-python
PR #1794, agno PR #8129) already expect.
These helpers let third-party fetchers (changedetection.io plugins,
Crawlee BrowserPool subclasses, agno toolkits) drive
playwright.firefox.launch(executable_path=..., firefox_user_prefs=...)
themselves without depending on the InvisiblePlaywright context
manager owning the lifecycle. Same seed semantics, same humanize
toggle, same extra_prefs overlay as the existing wrapper.
Tests: tests/unit/test_config_public.py adds 14 unit tests covering
deterministic seed, locale/timezone/pin/extra_prefs/humanize
variations, and round-trip via the public namespace. Full unit suite
(392 tests) stays green.
Backwards compatible: InvisiblePlaywright surface is unchanged.
BINARY_VERSION stays at firefox-7. Python-only release.
Carries the #24 fix (importlib.metadata-driven __version__ + top-level
--version flag) so users see a real version bump after `pip install --upgrade`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs reported in #24:
1. `python -m invisible_playwright version` printed the literal "0.1.0"
regardless of the installed version. Root cause: __version__ in
__init__.py was hardcoded and never bumped when the package version
moved past 0.1.0. Fix: read from importlib.metadata so __version__
stays in lockstep with pyproject.toml's `version` field by construction.
2. `python -m invisible_playwright --version` errored with "the
following arguments are required: cmd". Root cause: the parser had
`required=True` on its subparsers and no top-level --version flag.
Fix: add a top-level `--version`/`-V` flag using argparse's standard
version action, drop `required=True`, and reroute the "no subcommand"
case through parser.error() so the existing test_no_subcommand_errors
contract is preserved.
7 new unit tests pin both behaviours so they can't regress silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced em-dashes (—) with commas, colons, or periods depending on
context. Kept all emoji (✅/❌/⚠️) in the comparison table since
those are scannable scoring cues, not stylistic.
Net cleanup: 6 em-dashes removed from tagline, hero alt-text,
"Why it's powerful" paragraph, and the comparison intro.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous build still had ~25px of dark padding above the screenshot
plus a thin shadow border around it. Both gone now. Screenshot is
flush against the top edge of the frame and runs full-width; the only
remaining chrome is the 90px caption bar at the bottom with the green
checkmark and detector verdict.
Net effect: roughly 25-30% more usable screenshot area per frame, no
visible "bar" above. Size 478 KB (was 381 KB, larger because the
embedded pixels are now bigger).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "invisible_playwright" / github URL strip at the top of the
hero was redundant - the README headline right above the embed
already says both. Removing it frees vertical pixels for the actual
screenshot, which is what the visitor is here to see.
Layout now: screenshot fills everything above the 90px caption bar
at the bottom. Each screenshot renders ~10% larger.
Size 381 KB (was 356 KB - slightly larger because the screenshots
themselves are bigger after the scale change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two trailing paragraphs (testing disclaimer and the
"if you need Firefox + active maintenance" wrap-up) restated what
the table already showed. The table is the comparison; let it stand
on its own and jump straight to Install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous wording lumped CloakBrowser in with Camoufox as
"open-source peers." That was wrong: CloakBrowser publishes a Chromium
binary plus a wrapper, but the C++ source patches that produce the
binary are not in the repo. From a user's standpoint that's the same
trust profile as a closed-source commercial fork.
Comparison table updated to reflect this: Open source column now
distinguishes Camoufox (MPL, full source) and ours (MIT, full source
in invisible_firefox) from CloakBrowser (binary only) and the four
SaaS competitors (closed).
Intro paragraph rewritten to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hero GIF already shows each detector with its verdict in-frame
(reCAPTCHA 0.90, CreepJS 0 lies, FingerprintJS Pro not detected,
WebRTC no leak, sannysoft all green). Repeating the same five points
as a bullet list immediately below the visual was duplication that
pushed the comparison table and install snippet further down.
Above the fold is now: tagline, hero GIF, then straight into Why
it's powerful.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hero GIF above the fold already lists each detector with its
verdict in-frame. The Results section was repeating the same five
items in prose right after the visual, which is the kind of
above-the-fold padding that pushes the install snippet further down
without adding signal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hero is a 5-frame slideshow (12.5s loop, 356KB, 1200x675) cycling
through the five detection-test screenshots with a green-check
caption per frame. Branded with the project name top-left and a
github URL top-right so it works when embedded in tweets or blogs
without context.
Sits immediately after the tagline so the first thing a visitor sees
above the fold is moving visual proof, before any text. Industry
research on top-performing OSS READMEs in 2026 consistently puts
GIF/video first as the single biggest conversion lever for star intent.
Also collapsed the five expanded Results subsections down to a single
section: the GIF already shows each tester live, so the prose now
gives one short bullet per detector plus links to the original
full-resolution screenshots for anyone who wants to inspect them.
Net effect: roughly 200 lines of vertical scroll removed above the
fold, hero visual added, deep-link to per-tester screenshot
preserved.
No new test surface. No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three changes aimed at making the project's value legible in 5 seconds
for someone landing from a Trending list or HN thread:
- New tagline + quantified bullet hook at top. Concrete numbers
(0.90 reCAPTCHA, 0 CreepJS lies, 5/5 detection suites passed) up
front instead of generic "passes the hardest detectors" wording.
- Comparison table rewritten. The commercial-stack columns (Multilogin,
GoLogin, AdsPower, Dolphin Anty, Kameleo) were noise for the OSS
audience we want to reach. Replaced with the two relevant
source-level peers: Camoufox (Firefox, currently in a long
maintenance gap) and CloakBrowser (Chromium, fresh, but capped at
the Chromium reCAPTCHA ceiling).
- "Why it's powerful" opens with explicit positioning vs the two
peers, plus the reCAPTCHA v3 score that's the most defensible
numeric claim.
No code change. Repo homepage URL set separately via API to deep-link
the "Why it's powerful" section so visitors from external links land
on the value pitch rather than the badges row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
firefox.exe --version on Windows prints the version string but may
return non-zero exit code (sub-process fork quirk). The previous check
treated that as a launch failure, producing a false-positive failure
across the whole matrix while the binary actually launched cleanly.
Switch to matching the printed output instead, so we only fail when the
binary really can't start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triggered by issue #22 (firefox-7 SxS mismatch reported on Win11 26200
by jannusdorfer-create). Verifies the shipped binary launches cleanly
on every Windows runner GitHub offers: windows-2022, windows-2025,
windows-latest, across Python 3.11 / 3.12 / 3.13.
Each cell does the reporter's exact flow: fresh checkout, pip install
from source, python -m invisible_playwright fetch, then runs the
InvisiblePlaywright(seed=9128) snippet.
If all cells pass the bug is environment-specific to the reporter
(corporate edition, EDR, GPO). If any cell fails the same way we ship
a sidecar mozglue.manifest in the next release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds opt-in helper that auto-injects coherent cookie history into every
BrowserContext created via new_context(). Content is fully deterministic
from the persona seed so a given seed always presents the same cookies
across sessions.
Composition (per persona, all derived from seed):
- 5 cookies on .google.com (NID, CONSENT, SOCS, _GRECAPTCHA, ENID).
Excludes 1P_JAR which was deprecated by Google in 2022. CONSENT
`lang+region` token derived from the persona's IANA timezone
(Europe/Rome -> it+IT, America/* -> en+FX, etc.). NID prefix
broadened to 100-540 to cover historical versions.
- Per-site cookies on 13-25 "visited" everyday domains, sampled from a
Bayesian network conditioned on gpu_class - workstation/high_end
personas trend toward dev/tech sites, low_end/integrated_old trend
toward shop/news/reference. Each site contributes 1-7 cookies based
on a `cookie_profile` tag. Cookie pool includes _ga, _gid, _clck,
_clsk, __cf_bm, OneTrust/CookieYes consent, _fbp (Facebook Pixel),
_dc_gtm_<id> (Tag Manager helper), __hssrc (HubSpot helper).
API:
Stealthfox(seed=42, prep_recaptcha=True)
No per-call configuration: visited-sites + cookie composition all derived
from the persona seed via the Bayesian sampler.
Gated server-side: forced False if profile_dir is set (persistent profile
owns its own state). All expiries capped to 395 days per Chrome/Firefox
400-day RFC 6265bis-15 limit.
Bayesian integration:
- New `derive_browsing_history(gpu_class, rng)` in _fpforge/_sampler.py
(parallel to `derive_font_prefs`).
- New data files: browsing_pool.json (50 site entries) and
cpt_browsing_given_class.json (per-class probabilities).
- Profile dataclass exposes `browsing_history` field.
- _recaptcha_seed.py consumes Profile.browsing_history; receives
timezone separately to derive CONSENT lang+region.
Also drops a dead Chromium-only e2e test that always skipped on our
Firefox-only wrapper.
Test coverage: 29 unit tests covering composition, profile recipes
(minimal/ga_only/ga_cf/ga_consent/ga_consent_clarity), determinism,
Chrome 400-day cap, Playwright field requirements, CONSENT lang
mapping (IT/DE/US/default), helper-cookie probability distributions,
end-to-end with real fpforge Profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-asset shields.io endpoint rendered the asset filename next to
the count ('1 [launch.txt]'). Switching to the per-tag total endpoint
renders as just the integer.
Pulls the github download_count of the companion repo's usage-counter
asset. Click-through lands on the invisible_firefox release where the
full disclosure of what the counter measures lives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>