Google Ads conversion import — optimize YouTube ads for listens, not clicks
Goal: make a Google Ads Video Action campaign optimize toward people who actually start listening, instead of cheap clicks. This is the single change that separates a real campaign from the junk-traffic failure documented in the 2026-06-10 funnel readout (≈1,050 paid visitors / 48h → 1 listen, 94–100% sub-2s bounce). Without an imported conversion, Google can only chase cheapest-click — which is the junk.
You don't need to add any tracking code: the site already fires the right events to GA4. This guide wires one of them into Google Ads as the optimization goal.
What already fires (no code needed)
apps/frontend/src/lib/listen-tracking.ts sends these to GA4 via gtag('event', …) (GA4 is loaded by GoogleAnalytics.tsx when NEXT_PUBLIC_GA_MEASUREMENT_ID is set):
| GA4 event | When it fires | Params on the event |
|---|---|---|
listen_started | First audio play of the session (deduped once per session) | season_id, season_title, episode_id, episode_number, source |
listen_milestone_30s | 30s cumulative listened | season_id, season_title, cumulative_seconds, source |
listen_milestone_90s | 90s cumulative listened — the "qualified listener" signal | season_id, season_title, cumulative_seconds, source |
These fire from every player surface (paid /listen, /seasons/[id], novel-mode playlists) and once per session, so they're clean conversion inputs.
Step 1 — verify the events actually reach GA4 (don't skip)
The code fires them, but only if NEXT_PUBLIC_GA_MEASUREMENT_ID is set in the production Vercel env. Confirm before building anything on top:
- Open GA4 → Admin → DebugView (or Reports → Realtime).
- In a browser, visit a Graphene season page and press play; let it run 90s.
- You should see
listen_startedappear within seconds, thenlisten_milestone_30s/_90s.
If nothing arrives: NEXT_PUBLIC_GA_MEASUREMENT_ID isn't set in prod. Set it in Vercel (Project → Settings → Environment Variables) to the G-XXXXXXXX id, redeploy, and re-test. See GOOGLE_ANALYTICS_SETUP.md.
Step 2 — mark the event as a Key Event in GA4
- GA4 → Admin → Events (or Key events).
- Toggle
listen_startedto a Key event. - Do the same for
listen_milestone_90s(you'll use it as a secondary/quality signal).
Wait until you've seen real occurrences in Step 1 — GA4 only lists events it has actually received.
Step 3 — link GA4 ↔ Google Ads
Link the Graphene GA4 property (
graphene.fm), not a shared one. GA4 is per-brand by hostname (NEXT_PUBLIC_GA_MEASUREMENT_ID_GRAPHENE— see GOOGLE_ANALYTICS_SETUP.md), so the Graphene property only contains Graphene traffic. Importing conversions from a property mixed with HiveJournal/WriteCafe data would dilute the conversion rate and pollute remarketing audiences.
- GA4 → Admin → Product links → Google Ads links → Link.
- Pick the Google Ads account that runs the campaigns — account
299-167-6745(the one linked to the YouTube channel; visible under YouTube Studio → Settings → Channel → "Google Ads account linking"). - Enable Personalized advertising and auto-tagging when prompted.
Step 4 — import the conversion into Google Ads
- Google Ads (account
299-167-6745) → Goals → Conversions → + New conversion action → Import → Google Analytics 4 properties → Web. - Import
listen_startedandlisten_milestone_90s. - Configure:
listen_started→ Primary conversion, set as the campaign optimization goal.listen_milestone_90s→ Secondary (observe-only). Keeps it visible for quality without forcing the algorithm to optimize on a sparse signal.
Which one to optimize on, and when to switch
- Start on
listen_started. A Video Action campaign needs roughly 10+ conversions/week to exit the learning phase. At current listen volume,listen_milestone_90salone is too sparse to optimize on — the campaign would never learn. - Move optimization to
listen_milestone_90slater, once it's reliably clearing ~10+/week. That trades volume for higher-intent listeners (it's the same signal Meta uses as the weighted qualified-lead event,value: 1.0).
Step 5 — tag the ad's landing URL so everything lines up
Set the campaign/ad Final URL to the season page with these exact UTMs:
https://graphene.fm/seasons/<SEASON_ID>?utm_source=youtube&utm_medium=paid&utm_campaign=yt-<slug>-<yyyy-mm>
Why each part matters:
utm_medium=paid(exact) → triggersisPaidAdVisitor()inanalytics.ts, which renders the one-tap ColdAdListenHero instead of the buried-player default. The ad's job becomes a single tap, which is what makeslisten_startedachievable.utm_source/utm_campaign→ these are exactly what/dashboard/admin/ad-efficiencygroups on. Use a legible slug (e.g.yt-whispers-2026-06), not the raw numeric campaign id, so the dashboard reads cleanly.- Auto-tagging adds
gclidon top — that's fine, it's a second independent paid-visitor signal.
Step 6 — the same-day feedback loop
After launch, you don't have to wait on Google's reporting lag:
/dashboard/admin/ad-efficiencyat the 24h window — watch fraud% (sub-2s bounce share) and median seconds. With Video Partners off + conversion optimization on, median should climb from ~0.15s toward several seconds and fraud% should fall hard.- Google Ads → campaign → Content → "Where ads showed" — exclude any junk placements that slip through.
- Don't touch bids/targeting for ~2 weeks (learning phase). After that, consider a Target CPA and/or shifting optimization to
listen_milestone_90s.
Related
- Campaign-level setting that kills most junk before this even matters: uncheck "Video partners on the Google Display Network" (Google Ads → campaign → Settings → Networks). The Studio "Promote" tool can't do this — build the campaign in Google Ads proper.
- docs/ai/INDEX.md → Paid acquisition — the ad-efficiency dashboard + impression-gating + listen-tracking fan-out.
- GOOGLE_ANALYTICS_SETUP.md — GA4 base setup.