Google Ads offline-conversion pipeline fix: deep-field gclid capture + event-relative 24h upload delay
The client's WhatsApp landing-page campaigns generated ~1,200 Google Ads clicks but registered almost no conversions. I found two compounding bugs: the daily extraction read gclid from a shallow field that is usually empty instead of the populated attribution.click.clickData.google.gclid, and the gateway's per-destination min_delay_seconds was a phantom config never applied at enqueue, so gclids uploaded within seconds and couldn't match. I rewrote the Patagon extraction to pull gclid/fbclid/fbc/fbp/IP/UA/referralUrl from the deep clickData object with attributionHistory fallbacks, and specified an event-relative Cloud Tasks scheduleTime so Google uploads fire at event_time+24h (backfilled events fire immediately). I verified the fix on a real event that completed the full cycle, delivered to Google with HTTP 200 exactly at the 24h mark. Delivery, previously succeeding blindly, is now correct and observable.
A live event proved the fix: received 2026-08-05 20:27, delivered to Google 2026-08-06 17:50, HTTP 200, the delayed upload fired exactly at event_time+24h.
Role: Sole diagnostician and architect of the fix; deploy executed via paired Claude Code (CC)
The Problem
The client ran WhatsApp landing-page campaigns on Google Ads across 3 countries and collected ~1,200 clicks over ~10 days but Google Ads registered zero/near-zero WhatsApp conversions, blocking optimization and scaling of a historically strong channel.
Two independent bugs: (1) the daily extraction read gclid from the shallow list field attributionSource.gclid, which is frequently empty, instead of the populated deep field attribution.click.clickData.google.gclid; (2) the per-destination min_delay_seconds config was 'phantom', defined in the Zod schema and Supabase but never applied at enqueue, so gclids were uploaded within seconds and could not match before Google propagated the click.
The two bugs masked each other, Google's offline match rate is naturally partial, and a live diagnostic showed delivery succeeding (HTTP 200) even while nothing matched, so 'it's delivering' was misleading. Distinguishing 'not delivered' from 'delivered but unmatched' required reading the Cloud Tasks enqueue path, not just the ingest response.
Approach & Architecture
Traced the empty-gclid symptom to the wrong extraction field and rewrote the Patagon extraction to pull gclid/fbclid/fbc/fbp/ip/userAgent/referralUrl from the deep clickData object (with attributionHistory fallbacks). Separately proved via code read that scheduleTime was never set on Cloud Tasks, then specified an event-relative delay so Google uploads fire at event_time+24h (and old backfill events fire immediately). Verified the delayed delivery end-to-end on a real event.
Patagon (source of leads via MCP) → daily/backfill Node sender → gateway backend endpoint (/v1/collect/{client}/backend, HMAC-signed) → Cloud Tasks (per-destination task, scheduleTime = event_time+24h for Google) → delivery worker → Google Data Manager API events:ingest. Submission state in Firestore; audit in BigQuery delivery_attempts.
Key Decisions & Trade-offs
Hardest Part
Proving the delay was never applied: a live test event delivered to Google in ~5 seconds (received 20:33:15, google delivered 20:33:20), which contradicted the documented 24h behavior; reading cloudTasksPublisher.ts confirmed createTask was called with no scheduleTime and min_delay_seconds was consumed nowhere.
Technical Detail
Internal normalized event; dedup key = client_id + normalized_event + event_id + destination_platform (Firestore 'dedup' collection with TTL)
Cloud Run gateway; Cloud Tasks queue (scheduleTime supports up to 30 days future); Firestore (submissions/dedup); BigQuery delivery_attempts / dead_letter_events
Live diagnostic scripts hitting the real gateway + polling GET /v1/submissions; confirmed a fresh event scheduled 'pending' then verified it delivered to Google exactly at event_time+24h with HTTP 200 a day later
Code
// GcpCloudTasksPublisher.enqueue, google destinations only
const delaySec = 86400; // 24h, permanent for platform 'google'
const target = Math.floor(Date.parse(task.event.event_time)/1000) + delaySec;
const now = Math.floor(Date.now()/1000);
task.scheduleTime = target > now ? { seconds: target } : undefined; // old events => immediategclid: attribution.click.clickData.google.gclid || attribution.click.gclid || '', fbclid: attribution.click.clickData.meta.fbclid || attribution.click.fbclid || '', fbc: attribution.click.clickData.meta.fbc || '', ip: attribution.click.clickData.client.clientIpAddress || '', pageUrl: attribution.source.referralUrl || ''
const r = await fetch(${BASE}/v1/collect/${CLIENT}/backend, {method:'POST', headers:{'x-api-key':KEY,'x-sc-timestamp':ts,'x-sc-signature':sig}, body:raw});
const sid = JSON.parse(await r.text()).submission_id;
fetch(${BASE}/v1/submissions/${sid}).then(r=>r.text()).then(console.log);Diagnosis done in Claude Cowork mode: read the gateway TypeScript to disprove assumptions (grep for min_delay_seconds/scheduleTime, read cloudTasksPublisher.ts and deliver.ts), wrote and ran standalone Node diagnostic scripts against the live gateway, and delegated heavy Patagon extraction to a general-purpose subagent to keep the main context clean. Repo edits and Cloud Run deploys were handed to a paired Claude Code (CC) via precisely written instruction prompts.
Corrected the recurring daily bridge so future gclids are captured deeply and uploaded with the correct delay
Measured Results
Both root causes were fixed: gclids are now pulled from the deep clickData field and Google uploads fire event-relative at event_time+24h (old events immediately). A real delayed event was verified end-to-end (received 2026-08-05 20:27, Google delivered_at 2026-08-06 17:50, HTTP 200). Delivery is confirmed; Google's own match rate over the following days is expected to lift the near-zero conversion count.
| Metric | Value | Before | Source |
|---|---|---|---|
| Google deliveries succeeded (is_success=true) over 10 days for client_ 0 failures, 0 skipped | 1335 | n/a | BigQuery delivery_attempts (reported by CC) |
| Conversions Google actually matched/registered The gap between 1335 delivered and 13 matched is what triggered the investigation | 13 | n/a | Google Ads UI (client report) |
| gclid-bearing leads captured over 15 days after the deep-field fix | 60 (48 MX + 5 AR + 7 CL) | n/a | Re-extraction counts |
| Landing-page clicks with no Google WhatsApp conversion registered | ~1,200 over ~10 days | n/a | Client ticket (André Vasconcelos) |
Every figure above was recorded during the work itself. Where no number was measured, none is claimed.