GA4 audit
--demo needs no accountEvery other command answers what happened. craft audit answers the question
before it: is this property measuring the site at all, and is what it measured worth reading.
Fifteen checks over four weeks. Each finding is graded and carries what it means and what fixes it. One command, no console, nothing to configure.
01What an audit is actually looking for
A Google Analytics 4 property can collect flawlessly for a year and still answer no question anybody has. Traffic arrives, the charts move, every number in them is real — and nothing in the property was ever marked as the point, so no report can say whether any of that traffic was worth having.
That failure is invisible from inside the reports, which is why it survives so long. A conversion
count of zero looks identical whether nobody converted or nobody ever told GA4 what a conversion is.
The same is true of revenue that reads zero because purchase arrives without a value, and
of a bounce rate that looks wonderful because a second tag is engaging every session.
So the audit reads two APIs, because measurement and configuration fail separately. The
reporting API says how often purchase fired. The admin API says whether anybody ever
declared purchase a key event. Only the pair of answers is worth anything, and a site can
pass one and fail the other for a year without noticing.
02The fifteen checks
Five of them mean a number somewhere is wrong, not merely unflattering:
| Check | Finds |
|---|---|
| collecting | Nothing recorded at all. The tag is not on the site, or this is not the property the site reports to — a measurement id copied from a second property looks exactly like this |
| data_stream | No web data stream. There is no measurement id to put on a site, so nothing can ever report in |
| key_events_configured | Nothing marked as a key event. GA4 is counting traffic and nothing else; Google Ads has nothing to import or optimise against |
| key_events_firing | A key event configured and never fired. Either it is not being sent, or it is being sent under a different name — names match exactly, so Purchase and purchase are two events |
| revenue_tagged | Purchases without their money. purchase is firing and the property has no revenue, which is the event arriving without value and currency |
| measurement_off | Enhanced measurement switched off at the master switch. The stream's automatic events still show as on underneath it, and nothing collects them |
Five mean the numbers are real but something is distorting them:
| Check | Finds |
|---|---|
| double_counting | Page views counted twice. A bounce rate near nothing with views per session doubled — the shape a gtag snippet left beside a GTM tag makes |
| self_referral | The site referring itself. A visit cut in half by a domain the cross-domain configuration does not cover, crediting the site for its own traffic |
| payment_handoff | A payment or sign-in page credited with conversions. The return trip starts a new session referred by the gateway, and whatever actually brought the visitor loses the conversion to it |
| event_silence | An event that stopped firing between this window and the one before — a tag removed, renamed, or moved behind something that no longer runs |
| key_events_unmarked | An outcome arriving unmarked. sign_up firing a thousand times with nothing telling GA4 it is the point, so no conversion report counts it and Ads cannot import it |
| measurement_silent | Measurement on and recording nothing. The stream is configured to collect site search, video or downloads and has not seen one in a month. The only check with no threshold to tune — the expectation is Google's, not ours |
And five are context worth having before reading anything else:
| Check | Finds |
|---|---|
| event_names | One event under two names. sign_up and signUp are collected, reported and marked as key events separately |
| source_not_set | Sessions with no source at all. (not set) is GA4 saying it could not attribute the session to anything — these can never be credited to a channel |
| direct_share | Almost everything arriving as direct. Normal for a site people reach by typing its name; otherwise untagged campaigns, a referrer-dropping redirect, or a consent banner holding the tag back |
| data_stream | More than one site reporting in. Every number is the sum of all streams — right for one site on several domains, wrong for staging sharing a property with production |
| measurement_off | Measurement the stream could collect and does not. Scrolls, outbound clicks, downloads — no code to write and nothing to deploy. A note rather than a warning: site search and form interactions record what a visitor typed, which is a privacy decision, not an oversight |
03Run it
# fifteen checks over the last 28 days $ craft audit # a longer window $ craft audit --days 90 # see the whole shape of a report — no account, no subscription $ craft audit --demo
Twenty-eight days is the default because a key event that did not fire this week may simply not have happened, and one that did not fire in a month is broken. Four weeks also covers every weekday the same number of times, so a site that is quiet at weekends does not read as a site that stopped.
04Three grades
| Grade | Means |
|---|---|
| critical | A number somewhere is wrong, not just unflattering. Reading the reports before fixing this produces confident conclusions about nothing |
| warning | The numbers are real, and something is distorting them. Worth knowing which way before quoting one |
| note | Context to have before reading anything else. Not necessarily a defect |
Each finding carries the symptom, the evidence behind it, and a sentence on what it means and what fixes it — so the report is readable by whoever runs it rather than by whoever wrote it.
05What it will not do
It reports the symptom and names the usual cause, never the other way round. “Page views look counted twice” is a finding; “you have two page_view tags” is the sentence under it. A report that states guesses as findings gets believed once and then ignored.
It cannot see inside a Google Tag Manager container. It reads the reporting and admin APIs, so it finds the tagging bugs that reach the data — a tag firing twice, a tag that stopped, a tag sending the wrong name — and not a trigger misconfigured in a way that produces plausible numbers.
And every check has a floor under it. A property with eighty sessions has no meaningful bounce rate and no meaningful direct share, so those checks report as not run rather than firing on noise. The line under every report says how many of the fifteen actually ran, because “no findings” means nothing without the number of ways it looked.
06Scripting it
| Exit | Means |
|---|---|
| 0 | Checked, nothing found |
| 2 | Something was found |
| 1 | The audit itself failed |
The same convention craft watch uses, so a weekly audit into
Slack is one cron line:
0 9 * * 1 craft audit --format slack \
| curl -sX POST -H 'Content-Type: application/json' -d @- "$SLACK_WEBHOOK"
--format json prints one object carrying every finding with a stable slug, its grade,
its evidence and its explanation — plus how many checks ran out of how many exist. Unlike a watch, a
clean audit still prints: it is something somebody asked for, and “fifteen checks, nothing found” is the
answer they asked for.
07Ask an assistant to run it
The same fifteen checks are an MCP tool. With
the Google Analytics MCP server wired up, audit_site hands the
graded findings to Claude — which is the difference between an assistant that reports a number and one
that can tell you the number is not to be trusted.
"Why are our conversions zero this month?" → audit_site → nothing on this property is marked as a key event
08Look at it first
craft audit --demo runs on synthetic data: no Google account, no subscription, and one
finding of every grade, so the whole shape of a report is visible before anything is connected.
$ craft audit --demo
The audit itself is part of the Anacrafter Pro ($5.99) plan,
alongside Slack alerts — the tier for a site somebody is responsible for.
The dashboard, the one-shot reports and craft watch to the terminal stay on the plans
below it.
09Questions
What does a GA4 audit check?
Whether the property is collecting at all, whether anything in it is marked as a key event, whether those key events ever fire, whether outcomes are arriving unmarked, whether the stream is measuring what it was told to and recording what it measures, whether purchases carry their revenue, whether page views are being counted twice, whether the site or a payment page is credited with its own conversions, whether an event stopped firing, whether one event is arriving under two names, and how much traffic cannot be attributed to any source. craft audit runs fifteen such checks over four weeks and grades each finding.
Why does my GA4 show zero conversions?
Usually because nothing on the property is marked as a key event, or because the key event is configured under a name nothing is sending. GA4 matches event names exactly, so Purchase and purchase are two unrelated events and only one of them counts. Both cases report as zero conversions, and neither is visible from inside the reports.
Why is my GA4 revenue zero when purchases are firing?
The purchase event is arriving without its value and currency parameters. GA4 records the event and has no money to attach to it, so every revenue, ARPU and ROAS figure reads zero — including in any Google Ads account importing conversions from that property.
Why is my GA4 bounce rate so low?
A bounce rate near zero alongside doubled page views per session is the shape a duplicate page_view tag makes — typically a gtag.js snippet left in the page beside a Google Tag Manager tag that also sends one. The second view engages every session that would otherwise have bounced.
Can it find Google Tag Manager problems?
Only the ones that reach the data. craft audit reads the Google Analytics reporting and admin APIs, not the GTM container, so it finds a tag firing twice, a tag that stopped firing and a tag sending the wrong event name — and not a trigger misconfigured in a way that produces plausible data.