How to Build a World-Class GA4 Setup, Part 2: Correct Collection — Audit, Server-Side & Conversions
A GA4 property can pass every check it can run on itself and still miss the leads it exists to count. This is how to audit for the silent failures — a server conversion that POSTed to a 404 host for the property’s entire life, a button click miscast as a conversion — and fix collection before you touch architecture.
Francisco Contreras · Founder, Machina
12 min read

Key takeaways
- The most consequential defect was total data loss hiding behind clean-looking code. The server-side generate_lead conversion POSTed to www.google.com/mp/collect instead of www.google-analytics.com/mp/collect, returning HTTP 404, so the property’s ad-blocker-resistant lead signal reached GA4 zero times from creation (2026-04-24) to audit. A missing conversion cannot advertise its own absence.
- Correctness before architecture. The audit ran in two layers: a deterministic, read-only inventory through the GA4 Admin and Data APIs, then an adversarial multi-agent pass (18 agents across 9 measurement pillars, roughly 943,000 tokens) whose fact-checkers existed only to break the researchers’ claims — which is how the form-tracking double-count surfaced.
- A conversion is a claim about business value, not a count of activity. A cta_click event was marked a key event across roughly 1,200 pages; the moment a Google Ads link exists, that trains Smart Bidding to buy button-clickers, not leads. One click was demoted; the canonical lead was promoted only once it actually arrived.
- Skip, don’t fake. Of 29 leads in the database, 20 carried a first-party _ga cookie (69% capture) and were attributed; the missing 31% were ad-block and privacy-browser loss and were skipped, not fabricated. A synthetic client_id does not recover a lost user — it mints a phantom one that pollutes every aggregate.
- Event-data retention sat at the two-month floor, silently making the year-over-year seasonal analysis this business most needs structurally impossible. A single dataRetentionSettings PATCH raised it to fourteen months — a zero-risk change, but not retroactive, so the horizon could only be rebuilt going forward.
Why does a healthy-looking GA4 property still lose the leads it exists to count?
The property presented well. Over the thirty days before the audit it recorded 2,328 events, 401 active users, and 527 sessions, with a daily event count moving between 24 and 113. Data flowed. Realtime showed users. Six events carried the key-event star and eight custom dimensions sat registered and populated. A stack engineer glancing at the admin screens would have found nothing to alarm them — and that impression is exactly the problem. This is Part 2 of a three-part series; Part 1 built the measurement plan, and this part is the dramatic core: finding and fixing the silent failures before touching architecture.
The case study behind this series is an anonymized Central-California pest-control operator — call it Cypress — running one GA4 property across four counties. Its revenue arrives entirely offline: a homeowner finds a wasp nest in April or a rat in a November crawlspace, calls the phone number or submits a form, and a technician books the job days later. No cart, no checkout, no online transaction. The property has exactly one job, to count the leads that become booked jobs, and it was failing at that job while passing every surface check GA4 offers.
The through-line across everything that follows is that GA4 gives an operator almost no negative feedback. A wrong host returns nothing the application logic reads. A malformed event returns the same acknowledgement as a valid one. An over-broad conversion looks identical to a well-chosen one until the bidding money follows it. A two-month retention cap surfaces only when someone tries to build the report it forbids. The property passed every check it could run on itself. The audit’s contribution was to run the checks it could not.
How do you audit a GA4 property so the findings survive scrutiny?
The default GA4 audit is a person clicking through admin screens and writing down impressions. That method samples rather than enumerates, records readings without evidence, and collapses factual errors and reasoning errors into one pile. A durable audit defeats all three with two layers, and it keeps them strictly separate.
Layer one: a deterministic, read-only inventory
The first layer establishes what is true. It reads a fixed, ordered list of endpoints through the GA4 Admin API and the Data API, under a named service-account identity, using short-lived impersonated tokens rather than a downloaded key — so a leaked credential is worth one hour of read access, not a permanent one. Scopes are least-privilege: analytics.readonly for reporting, analytics.edit for configuration reads, and a distinct analytics.manage.users.readonly for the access-bindings read (forget it and Google returns ACCESS_TOKEN_SCOPE_INSUFFICIENT — a scope error, not a permissions one). Every raw JSON response is persisted to a timestamped directory before anyone interprets a field. A finding without a response file on disk is not a finding; it is a memory, and memories do not survive review.
That inventory is what read the two-month retention value verbatim, enumerated all eight custom dimensions, named the six key events, and pulled the Data API traffic mix that exposed google/cpc at 46 sessions flowing into a property with no Google Ads link. Facts first, judgment second.
Layer two: an adversarial multi-agent pass
The inventory says what is configured. It cannot say what that configuration means, or whether the auditor’s interpretation is correct — and interpretation is where audits fail, because a plausible-sounding measurement claim can be confidently false. The second layer supplies an adversary: a workflow of eighteen agents organized as nine measurement pillars, each staffed by a researcher who investigates and a fact-checker whose only mandate is to break the researcher’s claims. Self-critique inherits its own blind spots; a fresh agent pointed at a claim with instructions to falsify it starts somewhere else.
~943K
Tokens consumed by the adversarial audit — 18 agents across 9 measurement pillars (researcher plus fact-checker each), over roughly 18 minutes. The spend buys insurance against confidently-wrong recommendations, the expensive kind that survive casual review precisely because they are confident.
Multi-agent audit workflow, GA4 property field work, 2026
The verify pass earned its cost by reversing four over-claims a competent single auditor would have shipped: that Consent Mode v2 was a legal mandate (it is a Google Ads requirement for serving ads into the EEA and UK, not a statute, and this is a US-only operator); that the server payload leaked PII (it carried none — client_id, currency, a value, and identifiers, no names or emails); that Google Signals would corrupt data (it was already off, and its real harm at low volume is threshold-suppressed report rows, not wrong numbers); and that the legacy Google Ads offline-conversion path was still buildable (it was blocked for new accounts on 2026-06-15). More valuable still, the cross-pillar read surfaced an omission every researcher had half-seen: Enhanced Measurement’s form-interactions setting auto-fires GA4’s own form_start/form_submit on top of the site’s hand-built form events, double-counting form activity nobody had reconciled.
What was the P0 defect that had run silently since day one?
The single most consequential defect sat in two server routes, both posting their server-side Measurement Protocol event to https://www.google.com/mp/collect. That host is wrong. The documented and only functional collection endpoint is https://www.google-analytics.com/mp/collect. The distinction looks cosmetic. It is total. The audit did not infer it from documentation — it reproduced the failure live, sending the identical payload to each host with curl:
POST https://www.google.com/mp/collect→ HTTP 404 — the path does not exist on that host; the request is rejected before any GA4 system sees a payload.POST https://www.google-analytics.com/mp/collect→ HTTP 204 — the standard Measurement Protocol acknowledgement, a bare "accepted, no content."
The failure is a delivery problem outside GA4, not a data-quality problem inside it. The generate_lead event the contact API constructed on every form submission — client ID, currency USD, a value, lead metadata — was serialized, posted, and thrown away by a Google web server that has nothing to do with analytics ingestion. Because Cypress has no online transaction, this server event was the entire point: the ad-blocker-resistant record of a lead that would count even when a visitor’s browser blocked the client tag. It reached GA4 zero times across the property’s life, from creation on 2026-04-24 through the audit.
0 events
Server-side generate_lead conversions that reached GA4 between the property’s creation on 2026-04-24 and the audit. The one signal engineered to survive ad blockers produced not a degraded number but zero, and the calling code never inspected the 404 that swallowed it.
Reproduced live: identical payload returned 404 on www.google.com, 204 on www.google-analytics.com
The failure was silent in a specific and damning way. Because no generate_lead ever arrived, no one in the property’s history ever had reason to mark it a key event. One defect (the wrong host) produced a second symptom (no key event) that looked independent and was not. The absence of the event masked the absence of the data.
A missing conversion cannot advertise its own absence.
Two secondary defects rode inside the same payload and would have degraded the event even after the host was corrected. The client_id fell back to a synthetic server.<timestamp> string whenever the _ga cookie was absent — a fresh phantom user per cookieless lead. And the payload omitted both session_id and engagement_time_msec, so a correctly hosted event would still have attributed to (direct)/(none) regardless of how the visitor arrived. The audit ranked every finding by the money and the decisions it touched:
| Severity | Finding | Mechanism | Business consequence |
|---|---|---|---|
| P0 | MP posted to www.google.com/mp/collect (both routes) | Wrong host returns 404; payload never ingested | generate_lead reached GA4 zero times for the property’s life; only the ad-blockable client event survived |
| P0 | Synthetic client_id = server.<timestamp> | Fabricated ID per cookieless lead | Phantom one-session users distort every per-user metric |
| P0 | Payload missing session_id, engagement_time_msec | GA4 cannot stitch the event to a session | Server conversions attribute to (direct)/(none) |
| P1 | cta_click marked a key event | A micro-interaction defined as a conversion | Trains Smart Bidding to buy button-clickers, not leads |
| P1 | Google Ads spending while GA4 unlinked | 46 google/cpc sessions, no product link | No conversion signal to bidding; no cost-per-lead |
| P2 | Retention at TWO_MONTHS (the floor) | Floor caps dimensioned event data | Seasonal by-service, by-county analysis impossible |
| P2 | Enhanced Measurement page-changes + form-interactions on | Auto-events shadow custom events | Double-counted page_view, form_start, form_submit |
Severity-ranked findings for the audited property, read through the Admin API and Data API, July 2026. Wrong-host behavior confirmed live against the Measurement Protocol reference endpoint.
How do you rebuild a broken server-side Measurement Protocol event?
The reconstruction rewrote the Measurement Protocol block field by field, and every change addresses a named failure. The endpoint contract is unforgiving by design: a syntactically acceptable request returns HTTP 204, which means "I received your bytes," not "I recorded your event." An implementer who never reads the response is flying blind — which is exactly how a 404 lived for months.
| Field | As the audit found it | Corrected behavior |
|---|---|---|
| Host | www.google.com/mp/collect (returns 404) | www.google-analytics.com/mp/collect (returns 204) |
| client_id | Synthetic server.<timestamp> when the _ga cookie was absent | Parsed from the real _ga cookie; the send is skipped when absent, never faked |
| session_id | Omitted | Parsed from the _ga_<container> cookie, handling both the GS1 and GS2 formats |
| engagement_time_msec | Omitted | Set to 1 — the minimum honest value that keeps the event in session counts |
| ip_override | Omitted | Set to the visitor’s IP so a Salinas lead geolocates to Salinas, not the datacenter |
| value | Flat 150 for every lead | Per-service map from 100 to 500 (provisional, internal, never published) |
| source / medium / campaign | Bare fields re-declared on the event | Removed — they overwrite the session’s real acquisition source |
| HTTP status | Response never inspected | Non-200 responses logged so a silent 404 can never hide again |
Field-by-field reconstruction of the contact-route Measurement Protocol send; endpoint, client_id, session_id, and engagement_time_msec semantics per Google’s Measurement Protocol reference and sending-events guide.
The session_id is not stored in the _ga cookie; it lives in a per-stream _ga_<container> cookie whose packed value encodes the session. Two formats exist in the wild — a legacy dot-delimited GS1 string and a newer dollar-delimited GS2 string — and a parser that assumes one silently returns null for the other, dropping the event back to unsessioned direct traffic. Handling the union is correctness, not over-engineering. (Google documents that the cookie exists but not its internal layout, so this one parse is grounded empirically against the live cookie, and flagged as dependent on an undocumented format.)
The reconstruction treated a change as unverified until three signals agreed. First, the payload validated clean against the debug endpoint, returning validationMessages: [] — the only place Google will tell you a payload is well-formed before it silently swallows a malformed one in production. Second, a live send returned HTTP 204, the acknowledgement the wrong host could never produce. Third, the event surfaced in GA4 Realtime within about twenty seconds, registered as the generate_lead key event. Debug-clean, a live 204, and a Realtime appearance close the loop the original 404 left permanently open — and only then was generate_lead promoted to a key event, because you do not mark a conversion for data you have never seen.
Should you ever fake a missing conversion? The 69 percent rule.
The hardest decision in the repair is not a parsing detail. It is what to do when the join key is missing. The client_id lives in the _ga cookie that gtag.js sets, and a visitor whose browser blocks or never loads that tag still submits the form — the form is server-validated and does not depend on Google’s tag — but arrives with no cookie and no client ID. The reconstruction measured the size of that gap directly from the database rather than estimating it.
69%
Of 29 leads in the database, 20 carried a first-party _ga analytics cookie and could be honestly attributed to a real user and session. The missing 31% were ad-block and privacy-browser loss — browsers that never let gtag.js write the cookie — and were skipped, not faked.
Direct count from the contact_submissions table (source of truth), not an estimate
Faced with that gap, the original code chose to fake it with server.<timestamp>. The reconstruction chose to skip it. The reasoning is that a synthetic identifier does not repair attribution — it fabricates a user. Each value is unique, so every untrackable lead becomes a distinct new user arriving from (direct)/(none), smearing the channel mix with ghosts that correspond to no real acquisition. A property that exists to answer "which service and which county produce leads" is actively degraded by injecting users whose source is a lie. Skipping the send costs one conversion in GA4. Faking it costs the integrity of every aggregate the phantom touches — and the lead itself still lives in PostgreSQL either way, so the business loses only the GA4 credit for a lead it cannot honestly attribute.
A live test proved the gate behaves as designed. A real submission through the header quote panel, made on a clean Chrome profile that had never visited the site, landed in the database with an empty client ID — and the code correctly skipped the Measurement Protocol send rather than minting a ghost. That single hand-made lead is the empirical proof that the skip logic works, and it is a specimen of the discipline the whole reconstruction runs on: an honest gap is cheaper than a dishonest fill.
Which events are truly conversions, and why was one click not?
A conversion is a claim about business value, encoded as a measurement event. That definition sounds pedantic until you watch what happens when it is ignored. The audit found six key events, and at least three made claims the business could not honor. The discipline of conversion architecture is a bright line between the macro tier — the event you would pay to cause, here a submitted lead — and the micro tier of leading indicators (form_start, cta_click, view_item) that are valuable for diagnosis and actively harmful as optimization targets. GA4 blurs that line by letting any event become a key event with one toggle. The blur is the trap.
| Event | What it marks | A valid conversion? | Decision |
|---|---|---|---|
| generate_lead (server) | A submitted lead, carrying value | Yes — the canonical macro conversion | Promote to key event once the host fix makes it real |
| contact_form_submit (client) | The same lead, from the browser | Redundant with generate_lead | Keep as key event only until the Ads link, then demote |
| quote_form_submit (client) | A quote-panel lead | Redundant with generate_lead | Same deliberate deferral |
| cta_click | A button press across ~1,200 pages | No — a repeating micro-interaction | Delete the key-event flag; keep the event for analysis |
| purchase | An ecommerce sale | No — this business has none, and it never fired | Dead default; GA4 refuses to delete it (INVALID_ARGUMENT) |
| career_application_submit | A job application | Real for HR, but zero purchase intent | Keep in GA4; never import into Google Ads |
Conversion decisions for the audited property; key-event, counting-method, and deletion-boundary behavior per Google’s Admin API documentation.
cta_click as a key event is the most instructive anti-pattern in the property, because one wrong toggle scales into systemic corruption. Nearly every one of roughly 1,200 static pages carries call-to-action buttons, and each click fired cta_click. Marked a conversion, a curious visitor clicking three CTAs on three pages generates three "conversions" while buying nothing. Once a Google Ads link exists — and google/cpc was already spending 46 sessions a month against an unlinked property, so the link is coming — the bidder learns conversions are cheap and abundant, then chases high-volume, low-intent browsing instead of the narrow stream of visitors who actually submit a lead. Because click events vastly outnumber lead events, the noise becomes the dominant training signal. Phase 0 deleted the key-event flag as a zero-risk change; the event still collects for analysis, it simply no longer claims to be a conversion. The same logic keeps career_application_submit permanently out of the Ads-eligible set: a job seeker is a real action but never a booked pest-control job, and optimizing toward it would spend the marketing budget attracting applicants.
The canonical lead carries a differentiated per-service value, because a flat number tells a value-based bidder that a fumigation lead and a single-ant call are worth the same, which is false and suppresses the exact optimization value-based bidding exists to perform. The reconstruction replaced the flat 150 with a map, applied as an explicit value on every generate_lead:
- Fumigation $500, termite $400, commercial $400 — the high-ticket, seasonal revenue.
- Bed-bug $250, rodent $180 — the mid tier.
- General/pest $150, other $120, ant $100, wasp-bee $100 — the low-ticket calls.
- Every figure is labeled provisional and internal: a bidding and analysis signal that lives in versioned application code, never a customer-facing price, to be replaced by observed closed-job values as real data accrues.
One judgment here is a deliberate deferral, not an omission. Demoting the client contact_form_submit and quote_form_submit from key events is the obvious final step of canonicalization, and the reconstruction did not take it — on purpose. With no Ads link yet, the double-count lives entirely inside GA4 reporting, where an analyst can see both the client and server events side by side; there it is not merely harmless but useful, because the events blockers suppress and the server event they do not bracket the blocker-loss gap from both sides. GA4 deduplicates only purchase, keyed on transaction_id, and offers no equivalent for any other event, so the moment an Ads link exists the redundancy stops being diagnostic and becomes a bidding hazard. The plan therefore binds the demotion to the Ads-link event: do the harmless thing now, schedule the consequential thing to the moment it becomes consequential.
What does "correct before clever" mean for your own GA4 setup?
The sequence matters as much as the fixes. Correctness comes before architecture, and architecture comes before activation. The host fix made the canonical conversion real; only then did promoting it to a key event make sense. The conversion demarcation removed a false claim before it could poison a bidder that does not yet exist. And the cheapest, highest-leverage change of all was a single setting.
2 → 14 mo
Event-data retention was raised off the two-month floor to fourteen months with one dataRetentionSettings PATCH. The floor silently made year-over-year seasonal analysis by service and county impossible; the fix is zero-risk but not retroactive, so the lost horizon rebuilds only going forward.
Retention window and its scope (Explorations and dimensioned reports) per Google’s data-retention documentation
That setting is a lesson in scope. GA4’s standard aggregate reports retain effectively indefinitely, so top-line trends were never at risk; the two-month cap bites only Explorations and funnels that pivot on event- or user-scoped custom dimensions — which is precisely the by-service, by-county seasonal analysis this business lives on. Do it before you need the report, because you cannot recover the window you never kept.
Honest expectations close the picture. At roughly 46 paid clicks and 500 sessions a month, this property sits below the volumes where Google’s data-driven attribution (historically near 400 conversions per 28 days) and value-based Smart Bidding (roughly 15 to 30 conversions a month) stabilize. Building the differentiated value model now is still correct: it gives the office cost-per-booked-job visibility immediately and accumulates honest history for the day volume rises. The point of this part of the series is not a clever configuration — it is a correct one. A property that reports confidently on missing, misattributed, or misdirected data is worse than one that reports nothing, because someone will act on the confident number.
With collection correct and conversions honest, the remaining work is activation and governance: closing the offline-revenue loop back into Google Ads, consent by design, warehousing, and the controls that stop a clean property from drifting. That is Part 3. If you would rather have the audit run on your own property, that is the shape of our measurement and SEO work — the same programmatic read, ranked by the money each defect touches.
FAQ
Frequently asked questions
Can a GA4 property look completely healthy and still be broken?
Yes — that is the central risk. The audited property recorded 2,328 events, 401 active users, and 527 sessions over thirty days, showed six key events, and passed every surface check, while its server-side generate_lead conversion had reached GA4 zero times for the property’s entire life because it POSTed to a 404 host. GA4 gives almost no negative feedback: a wrong host, a malformed event, and an over-broad conversion all look normal until someone runs the check the property cannot run on itself.
How do you know if your server-side Measurement Protocol events are actually landing?
Verify three signals. Send the payload to the debug endpoint (/debug/mp/collect) and confirm validationMessages is empty. Confirm a live send to www.google-analytics.com/mp/collect returns HTTP 204, not 404. Then confirm the event appears in GA4 Realtime within about twenty seconds. Critically, inspect and log the HTTP status in your code — the original defect survived for months because the calling function never read the 404 it received.
Why is www.google.com/mp/collect the wrong endpoint for the Measurement Protocol?
The only documented and functional GA4 collection host is www.google-analytics.com. The path /mp/collect does not exist on www.google.com, so a POST there returns HTTP 404 and is discarded before any analytics system sees it. The correct host returns HTTP 204. A single wrong subdomain caused total, silent data loss because the endpoint returns nothing an application swallowing the response would read as an error.
Should you invent a client_id when the _ga cookie is missing?
No. A synthetic client_id does not recover a lost user; it fabricates a phantom one. Because each value is unique, every cookieless lead becomes a distinct new user arriving from (direct)/(none), inflating the user count and smearing the channel mix. In the case study, 20 of 29 leads carried a real cookie (69% capture) and were attributed; the missing 31% were skipped, not faked. The lead still lives in your database, so you lose only the GA4 conversion credit — not the lead.
Why should a call-to-action click not be marked as a conversion?
Because a click is a repeating micro-interaction, not a discrete intent to buy. Marked as a key event across roughly 1,200 pages, a cta_click lets a curious visitor generate several conversions while buying nothing. Once a Google Ads link imports it, Smart Bidding optimizes toward button-clickers instead of leads, and because clicks vastly outnumber leads the noise becomes the dominant training signal. Keep the event for diagnosis; reserve conversion status for the submitted lead.
What is the highest-leverage zero-risk GA4 fix?
Raising event-data retention off the two-month floor to fourteen months with a single dataRetentionSettings change. Standard aggregate reports retain indefinitely, but Explorations and any report using event- or user-scoped custom dimensions are capped by this window — so a two-month floor makes year-over-year seasonal analysis impossible. The change carries no risk, but it is not retroactive, so make it before you need the report you cannot yet build.
Sources
- Google Analytics — Measurement Protocol (GA4) reference: the google-analytics.com collection host and required client_id + events array (the wrong-host 404 bug)
- Google Analytics — Sending Measurement Protocol events: client_id, session_id, and engagement_time_msec, the fields that decide session stitching and Realtime appearance
- Google Analytics — Validating Measurement Protocol events: the /debug/mp/collect endpoint and its validationMessages array
- Google Analytics Admin API v1 (guide): the configuration surface for custom dimensions, key events, data-retention settings, and Enhanced Measurement
- Google Analytics Data API v1 (guide): the reporting surface used to read the traffic mix and volume envelope during the audit
- Google Analytics Help — Data retention: the two-to-fourteen-month event-data window and its scope over Explorations and dimensioned reports
- Google Analytics Help — About attribution and attribution modeling: data-driven attribution’s volume requirements and silent fallback
- Google Ads Developer Blog (May 2026) — Changes to offline click conversion uploads: the 2026-06-15 block that foreclosed the legacy path for new accounts
- Google Ads Help — About consent mode: Consent Mode v2 as a Google Ads requirement for advertising into the EEA and UK, not a statute
- Google Analytics — User-provided data for the Measurement Protocol: the hashed identity fields (context for the corrected no-PII payload finding)
- Google Ads Help — Import conversions from clicks: the offline conversion import behind the revenue loop developed in Part 3
Keep reading
Related guides
Explore related work
Next steps
Let's talk about your project
Tell us what you're working on. We'll tell you if we can help — and exactly what we'd do.