Most GA4 problems never announce themselves. The property keeps collecting. The reports keep rendering. The numbers keep being wrong in the same direction, week after week, until somebody sets a budget against them.
That is the failure mode a GA4 audit exists to catch — not an outage, a quiet bias. Outages get fixed the same afternoon, because somebody screams. A tag that has been silently dropping one purchase in six since March gets fixed after a quarter of misallocated ad spend, if it gets fixed at all.
A GA4 audit is not a walk through the reports hunting anomalies. It's a trace of every path data takes into the property, in dependency order, asking one question at each hop: what would this look like if it were broken? If the answer is "exactly like it looks now," that hop needs a test, not an opinion.
1. Event provenance before event volume
The first question is never "how many events." It's "where does this event name come from."
In more containers than you would expect, custom event names are manufactured inside Google Tag Manager from a lookup table keyed on the clicked element's text. A button reading "Reserve your seat" maps to one event name, "Apply to speak" to another. The tag looks stable. It isn't — it's keyed on marketing copy.
Change that button to "Save your seat" and the event stops firing. No error. No console warning. No drop in sessions. Just a metric that flatlines while every number around it looks normal — and a remarketing audience built on that event quietly starving.
So I map every custom event back to its trigger before reading a single count, and grade each one: stable (URL, form ID, dataLayer push) or fragile (copy, class names, DOM position). Fragile isn't automatically wrong. It just has to be written down where the person editing the button will look.
2. Purchase integrity
Revenue is the number people act on, so it gets the most checks: is transaction_id present and unique, does value carry a currency, does the same purchase arrive twice from two tags, and — the one people miss — what is conditioned on what.
A purchase trigger that only fires when ecommerce.value > 0 is a reasonable defense against test transactions and $0 comps polluting bidding. It is also, on the day someone claims a comped seat or a group allocation, a silent hole: real registrations that send nothing to GA4, nothing to Google Ads, nothing to the CRM mirror.
Bug or feature depends entirely on whether anyone wrote it down. Undocumented, it's a bug — someone will reconcile the processor against GA4, find the gap, and lose a day to it. Documented, it's a guard the reconciliation query accounts for. Same code either way.
3. PII in the URL
Once an email address lands in page_location as a query parameter, it is in the property, in the BigQuery export, and in every report built on either. Google's terms prohibit it, but the practical problem lands first: you now have a data-handling obligation you never scoped.
I scan page_path and page_location for @, digit runs that look like phone numbers, and CRM record IDs. The fix is always upstream — strip the parameter in the tag or the form's redirect. Filtering it out of a report fixes the report and nothing else; the data is still in the property.
4. The server-side path
If there's a server-side GTM container in the stack, the audit widens. The checks that actually catch things:
- Which client claims the request, and in what priority order. Two clients that both claim a request produce duplicate events or silently dropped ones depending on which wins.
- Transport URL on a first-party subdomain, resolving correctly, with a valid certificate — not the default endpoint the container ships with.
- Consent Mode ordering. The consent default block must sit above the GTM snippet in the head. Below it, defaults land after the container has already made decisions, and the failure reads as a modest, plausible dip rather than an error.
- The hosting provider's loader. On Stape-hosted containers, a custom loader path is a real dependency: if it's changed or removed on the hosting side and the site's head HTML still points at the old path, collection stops at the edge, where nothing in GA4 will tell you about it.
Verify every head-level finding against the rendered page, never the editor — some CMS platforms apply domain-level overrides that shadow global settings, so an edit that looks saved may never render.
5. Conversions API mirrors (Meta CAPI, Enhanced Conversions)
When the same conversion is sent from the browser and from the server, the audit is about deduplication and match quality, in that order.
Dedup: browser and server events must carry the same event ID, or the platform counts both. Match quality: for Meta CAPI and for Google's Enhanced Conversions, the payload's identifiers determine whether the conversion attaches to a user at all. Email-only is the common shortcut. Email plus phone measurably improves match rates, and both must be normalized and hashed exactly as specified — trailing whitespace and uppercase are enough to break the join.
The trap is a dashboard reporting "events received." Received is not matched; matched is not attributed. Check the platform's match-quality diagnostic, not the delivery count.
6. When the checkout isn't yours
Plenty of businesses take money on a checkout they don't control — a payment link, a third-party registration platform, a marketplace. There's no page to tag, so the browser path can't see the purchase. The fallback is server-side: the payment processor's webhook fires, a small handler receives it, and the purchase is sent to GA4 through the Measurement Protocol.
Two rules make that handler auditable. Log before send — write the event to a sheet or table first, then transmit, so a failed send leaves evidence instead of silence. And check the right status code:
const res = UrlFetchApp.fetch(MP_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(body),
muteHttpExceptions: true
});
// GA4 Measurement Protocol returns 204 No Content on success.
// Checking for 200 marks every successful hit as a failure.
const ok = res.getResponseCode() === 204;
sheet.getRange(row, STATUS_COL).setValue(ok ? 'sent' : res.getResponseCode());
Worth knowing: the live endpoint returns 204 and validates nothing. The debug endpoint returns 200 with a validationMessages array. A payload that the live endpoint accepts without complaint can still be malformed enough that GA4 discards it downstream. Validate against debug first, then ship.
What the audit cannot do
This is the part worth stating plainly, because it changes when you should run one.
GA4 has no delete. There is no mechanism to remove a bad purchase from history. The only supported correction for inflated revenue is sending offsetting refund events, which nets the total out without ever removing the original rows. Anything that made it in, stays in.
Renaming an event breaks continuity. The old name keeps its history, the new name starts empty, and nothing joins them. Sometimes the right call is a bad event name kept forever.
Nothing is reprocessed. A tag fixed today fixes data from today. The wrong months stay wrong, and every year-over-year comparison that crosses the fix date carries a step change that has nothing to do with the business.
Which is the real argument for auditing early. An audit is a diagnostic, not a repair of the past — the value is entirely in the data you haven't collected yet.
What it costs
Scoped honestly, a single-property GA4 audit is about ten engineering hours: a trace of every collection path, a written finding per hop with severity, the reconciliation query that proves or disproves each revenue claim, and a fix list ordered by what's silently wrong rather than what's loudest. Most of that time is verification, not discovery. Finding a suspicious number takes minutes; proving it is wrong takes the afternoon.
If you're about to make a spend decision on GA4 numbers nobody has traced end to end, trace them first.
Not sure your GA4 numbers survive a trace?
The Automation Audit is a fixed-scope diagnostic of one workflow — findings, severity, and a fix list in 7 business days.
See the Automation Audit