┌── POST 08.31 · Cookie Audits & Scanners · 5 min read

How to Detect Pre-Consent Tracking With a Playwright Script

A working Playwright script that captures every network request and cookie written before a visitor touches the consent banner, plus the false positives and blind spots that trip up static tracker-domain lists.

TL;DR

Manual checklists for pre-consent tracking do not scale past one page. Playwright — Microsoft’s open-source browser automation library, Apache-2.0, 95k+ stars, actively maintained — can drive a real browser, capture every network request and every cookie written before you touch the consent banner, and flag anything that should not be there. About forty lines of code. This walks through the script, what to look for in the output, and where automated detection still needs a human.

Every cookie scanner on the market, ours included, does roughly the same thing under the hood: load a page in a real browser, do not interact with the consent banner, and record what fires anyway. There is nothing proprietary about the mechanism. It is worth knowing how to do it yourself, both to sanity-check what a tool reports and to run a quick check on a page before you commit to buying anything.

This is that mechanism, in a script you can run today.

What “before consent” actually means to a browser

A page has no interaction until a script fires. Between the first byte of HTML arriving and a visitor clicking anything on the banner, the browser is already parsing script tags, executing inline JavaScript, and — if nothing is stopping it — sending requests and writing cookies. That window is short, often under a second, but it is exactly the window GDPR and the ePrivacy Directive require consent for, and exactly the window most CMPs get wrong because loading their own banner script takes longer than loading the tracker it is supposed to gate.

Detecting the problem means capturing two things in that window, before any click: outbound network requests, and cookies written to the browser.

The script

This uses Playwright for Python. Node works the same way with near-identical syntax.

pip install playwright
playwright install chromium
from playwright.sync_api import sync_playwright

TRACKER_HINTS = (
    "google-analytics.com", "googletagmanager.com", "doubleclick.net",
    "facebook.com/tr", "connect.facebook.net", "hotjar.com",
    "clarity.ms", "tiktok.com", "linkedin.com/px",
)

def audit(url):
    requests_seen = []

    with sync_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context()
        page = context.new_page()

        page.on("request", lambda r: requests_seen.append(r.url))

        page.goto(url, wait_until="networkidle")
        # Deliberately do not click anything on the banner.

        cookies = context.cookies()
        storage = page.evaluate(
            "() => ({ local: Object.keys(localStorage), "
            "session: Object.keys(sessionStorage) })"
        )

        browser.close()

    flagged = [u for u in requests_seen if any(h in u for h in TRACKER_HINTS)]
    third_party_cookies = [c for c in cookies if url.split("/")[2] not in c["domain"]]

    print(f"{len(requests_seen)} requests total, {len(flagged)} to known trackers")
    for u in flagged:
        print(f"  FIRED PRE-CONSENT: {u}")

    print(f"{len(cookies)} cookies set, {len(third_party_cookies)} third-party")
    for c in third_party_cookies:
        print(f"  COOKIE: {c['name']} ({c['domain']})")

    print(f"localStorage keys: {storage['local']}")
    print(f"sessionStorage keys: {storage['session']}")

if __name__ == "__main__":
    audit("https://example.com")

Run it against your own site before you accept anything. If it prints entries under FIRED PRE-CONSENT, that is a script executing and sending data before a visitor has made any choice — the exact failure mode the manual checklist is trying to catch by hand, done in a few seconds and repeatably.

Reading the output correctly

A few things the raw output does not tell you, and that cause false positives and false negatives if you skip them:

  • Domain hints go stale. The TRACKER_HINTS list above is a starting point, not a source of truth. Vendors move endpoints, add subdomains, and proxy through first-party paths specifically to dodge exactly this kind of static list — server-side GTM setups often do this deliberately. A request to yourdomain.com/gtm.js that then forwards data to Google will not match any string in that list.
  • Cookies are not the whole picture. Anything that persists a fingerprint or identifier — localStorage, sessionStorage, IndexedDB — never shows up in a cookie table, but does exactly the same job. The script above checks the two storage APIs the majority of trackers actually use, but a thorough audit checks IndexedDB and the Cache API too.
  • `networkidle` is a compromise. Waiting for network idle catches most synchronous and near-immediate requests, but a script that deliberately delays its own fire — again, something server-side setups can do — will slip past a fixed wait. A production scanner runs a timed wait after idle, not just idle itself.
  • One page is one data point. Tag managers frequently load different rule sets per URL. A homepage that is clean proves nothing about a checkout flow or a blog post with an embedded video.

What this replaces, and what it does not

This script is enough to answer “is my homepage doing anything obviously wrong right now” in under a minute, and that is a genuinely useful thing to be able to check yourself instead of trusting a vendor’s dashboard. It is not enough to replace a proper audit, for the same reasons listed above: static hint lists rot, single-page runs miss route-dependent tag rules, and nothing here classifies what a request actually contains — only that it fired. The tooling landscape for automated scanning exists precisely to cover the gap between “ran a script once” and “actually know what every page does.”

If you are choosing between building this out further yourself or buying a tool that already does it across a full crawl, that decision has the same shape as choosing an open-source CMP over a paid one: control and cost on one side, coverage and maintenance on the other. Neither answer is wrong. What matters is not guessing which one your site currently is.

Want the crawl-scale version of this — every route, storage API, and script classified, not just one page checked by hand? Run a full audit with CookieInspector.

C
About the author
Consent Mode HQ
Editorial team at Consent Mode HQ
Read more by author ↗