Google attribution parity (server-side conversions + gclid rescue)
Status: built 2026-07-08 (branch worktree-fix-google-attribution-gaps); live once the Data Manager API is enabled on the GCP project and env vars are set (see "Activation" below).
Owner: Aaron
Related: investigation.md (API landscape + quantified gap) · marketing-scorecard-attribution · docs/engineering/infrastructure/analytics.md (canonical as-built reference)
Problem
Meta had three routes into conversion_attributions (first-touch fbclid rescue, Instant-Form lead match, CAPI server events); Google had one (browser gtag + on-site UTM/gclid capture). The June 2026 cross-check (scripts/marketing/cross-check-google-attribution.ts) showed Google Ads claiming 27 purchases while only 5 were server-verifiable.
What was built
- gclid first-touch rescue —
User.gclidFirstTouch(+At), written first-touch-only byPOST /api/user/attribution(signup/login) and the tour stub-user path;user-gclid-rescuebranch inAttributionServicecarry-forward. Precedence: prior → meta-lead → fbclid → gclid → cm-reported. - Server-side conversion upload —
lib/google-ads/{data-manager-client,hash-identity,track-purchase,track-lead}.ts, a file-for-file mirror oflib/meta/. Fires Purchase beside every Meta CAPI Purchase (7 sites) and Lead beside every Meta CAPI Lead (6 sites), consent-gated identically. Dedup vs the browser tag via sharedtransactionId(browser gtagtransaction_idfixed from''to the Meta event id). - No Google lead ingestion — audited 2026-07-08: no Google Lead Form assets exist in any account under the MCC. No GoogleLead table built (deliberate).
- AdLeadDaily google rows —
sync-ad-datacron now pulls per-campaign/daySUBMIT_LEAD_FORMconversions forplatform='google'.
Account state (created/verified via scripts/marketing/google-ads-conversion-setup.ts)
- Conversion-tracking owner: customer 6392867894 (tracking id 17907719909); customer data terms accepted.
- Purchase action 7510162937 (existing gtag WEBPAGE action — uploads target it so
transactionIddedup works). - Lead action 7677760314 "Lead (server upload)" (UPLOAD_CLICKS, SUBMIT_LEAD_FORM, secondary — promote to primary only as a deliberate bidding decision).
enhanced_conversions_for_leads_enabledis still false (UI toggle); user-data-only lead matching may not attribute until enabled. gclid-keyed events unaffected.
Activation checklist (blocked on Aaron)
- Enable the Data Manager API on the service-account GCP project:
gcloud services enable datamanager.googleapis.com --project=955034515062 - Run the validateOnly round-trip:
cd apps/api && npx tsx scripts/marketing/google-ads-conversion-setup.ts validate-dm --operating-customer=6392867894 --action-id=7510162937 - Set Vercel prod env:
GOOGLE_ADS_CONVERSION_CUSTOMER_ID=6392867894,GOOGLE_ADS_CONVERSION_ACTION_ID_PURCHASE=7510162937,GOOGLE_ADS_CONVERSION_ACTION_ID_LEAD=7677760314(code silently skips until set). - Optional: enable enhanced conversions for leads in Google Ads UI (Settings → Enhanced conversions) for user-data-only lead matching.
Known limits / deferred
- gbraid/wbraid captured end-to-end (added 2026-07-08 after the initial build):
UTM_PARAMS→attributionSchema→ Stripe metadata →conversion_attributionscolumns (migration20260708130034) → DMadIdentifiers, and both count as real ad attribution. Deliberately NOT persisted as User first-touch columns (no braid analog ofgclidFirstTouch) — braids only survive the 24h sessionStorage window unless the conversion lands in-session; revisit onceconversion_attributionsshows real braid volume. - Meta's InitiateCheckout / CompleteRegistration have no Google counterpart — only Purchase + Lead are mirrored (deliberate: those are the biddable Google actions; mid-funnel Google actions would be unused).
- Vanity conversions are still primary-for-goal in the account (YouTube follow-on/subscriptions, Local actions, Store visits) — bidding-noise cleanup is a marketing decision, flagged not changed.
fulfillOrganizationDedicatedRoom(B2B path) fires no conversion events, same as Meta.