Guide

GA4 audit

craft audit · part of the Anacrafter Pro plan · --demo needs no account
What it does

Every 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:

CheckFinds
collectingNothing 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_streamNo web data stream. There is no measurement id to put on a site, so nothing can ever report in
key_events_configuredNothing marked as a key event. GA4 is counting traffic and nothing else; Google Ads has nothing to import or optimise against
key_events_firingA 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_taggedPurchases without their money. purchase is firing and the property has no revenue, which is the event arriving without value and currency
measurement_offEnhanced 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:

CheckFinds
double_countingPage 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_referralThe 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_handoffA 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_silenceAn 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_unmarkedAn 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_silentMeasurement 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:

CheckFinds
event_namesOne event under two names. sign_up and signUp are collected, reported and marked as key events separately
source_not_setSessions 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_shareAlmost 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_streamMore 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_offMeasurement 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

GradeMeans
criticalA number somewhere is wrong, not just unflattering. Reading the reports before fixing this produces confident conclusions about nothing
warningThe numbers are real, and something is distorting them. Worth knowing which way before quoting one
noteContext 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

ExitMeans
0Checked, nothing found
2Something was found
1The 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.