Read a website's public Google tagging and report what is likely broken, without ever touching the Ads or Analytics account.
It exists because the same handful of failures keep costing advertisers real money, and all of them are visible from outside: a conversion tag wired to the page-load event, a missing conversion linker, one conversion action fired by two tags with nothing to deduplicate on, two GA4 properties on the same page.
$ tracking-check example.com
Scanned: https://example.com
Containers: GTM-ABCD123
[CRITICAL] An Ads conversion fires on page load
Evidence: GTM-ABCD123 tag #7 conversion 11248148062 triggers: event=gtm.js
The tag fires before any success happens, so it counts page views as conversions.
Smart Bidding optimises on that signal.
Verify: In Tag Manager, open this tag and read its trigger. If it is All Pages, the
conversion should instead fire on the confirmation event, or on a URL only reachable
after the form is submitted.
No dependencies beyond the standard library. Python 3.10+.
git clone https://github.com/<you>/tracking-checker
cd tracking-checker
python3 -m tracking_checker.cli example.comOr install it:
pip install -e .
tracking-check example.comtracking-check example.com # one site
tracking-check a.com b.com c.com # several
tracking-check example.com --lang fr # French output
tracking-check example.com --format json # machine readable
tracking-check example.com --format markdown > report.mdExit code is 1 when at least one critical finding was raised, so it can sit in a cron
job or a CI step and actually fail.
from tracking_checker import check
result = check("example.com")
print(result.critical, [f.id for f in result.findings])| id | severity | what it means |
|---|---|---|
conversion-on-pageview |
critical | An Ads conversion tag is gated only by the page-load event, so it counts visits rather than successes |
ga4-conversion-on-pageview |
critical | A GA4 event named like a success fires on page load. If it is a key event imported into Ads, bidding optimises on page views |
no-conversion-linker |
critical | Ads conversions with the linker off and no standalone linker tag, so the click id is not stored first-party |
conversions-not-visible |
info | No Ads conversion tag at all, so the conversions are decided in the accounts and cannot be read from outside |
duplicate-conversion |
warning | One conversion action fired by several tags with no transaction or lead id to deduplicate on |
multiple-ga4 |
warning | Several GA4 properties measuring the same page |
enhanced-conversions-off |
warning | Enhanced conversions unchecked on every conversion tag |
consent-no-default |
warning | A consent banner is present but no consent default is visible in the served HTML |
consent-signals-incomplete |
warning | A consent default that does not cover all four signals |
multiple-containers |
info | Several Tag Manager containers on one page, the usual leftover of an agency change |
paused-tags |
info | The container holds paused tags, a frequent cause of "the conversion just stopped" |
server-side |
info | Tagging served from a server-side container, which cannot be read from outside |
no-tagging |
warning | No Google tagging in the served HTML |
It never puts a number on the damage. It reads a public container, not an account. It cannot see spend, conversion volume or revenue, so it will never tell you that you are losing a percentage of anything. Every finding says what was observed and how to confirm it yourself, usually in about two minutes. A checker that guesses is worth nothing the moment someone opens the account and looks.
Three concrete blind spots, printed at the end of every report:
- JavaScript is not executed, so tagging injected after load is invisible.
- The Ads and Analytics accounts are not read.
- A server-side container's forwarding rules cannot be read from outside.
A clean report is not proof that a setup is correct. It means the public container shows no known failure pattern.
Two public sources. The HTML the server returns for the URL, and
googletagmanager.com/gtm.js?id=GTM-XXXXXXX, which is the compiled published container:
its tags, its triggers, and the conditions on those triggers. Resolving triggers onto
tags is what makes conversion-on-pageview detectable at all.
parse.py describes what is there. checks.py decides what is wrong. They are kept
apart on purpose, so the description stays true even when the judgement changes.
python3 -m pytest tests/ -qFixtures are synthetic containers built by hand, not captures of real sites.
MIT.