setup-guides

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 eventWhen it firesParams on the event
listen_startedFirst audio play of the session (deduped once per session)season_id, season_title, episode_id, episode_number, source
listen_milestone_30s30s cumulative listenedseason_id, season_title, cumulative_seconds, source
listen_milestone_90s90s cumulative listened — the "qualified listener" signalseason_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:

  1. Open GA4 → Admin → DebugView (or Reports → Realtime).
  2. In a browser, visit a Graphene season page and press play; let it run 90s.
  3. You should see listen_started appear within seconds, then listen_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

  1. GA4 → Admin → Events (or Key events).
  2. Toggle listen_started to a Key event.
  3. 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.


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.

  1. GA4 → Admin → Product links → Google Ads links → Link.
  2. 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").
  3. Enable Personalized advertising and auto-tagging when prompted.

Step 4 — import the conversion into Google Ads

  1. Google Ads (account 299-167-6745) → Goals → Conversions → + New conversion action → Import → Google Analytics 4 properties → Web.
  2. Import listen_started and listen_milestone_90s.
  3. Configure:
    • listen_startedPrimary conversion, set as the campaign optimization goal.
    • listen_milestone_90sSecondary (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_90s alone is too sparse to optimize on — the campaign would never learn.
  • Move optimization to listen_milestone_90s later, 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) → triggers isPaidAdVisitor() in analytics.ts, which renders the one-tap ColdAdListenHero instead of the buried-player default. The ad's job becomes a single tap, which is what makes listen_started achievable.
  • utm_source / utm_campaign → these are exactly what /dashboard/admin/ad-efficiency groups 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 gclid on 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:

  1. /dashboard/admin/ad-efficiency at 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.
  2. Google Ads → campaign → Content → "Where ads showed" — exclude any junk placements that slip through.
  3. Don't touch bids/targeting for ~2 weeks (learning phase). After that, consider a Target CPA and/or shifting optimization to listen_milestone_90s.

  • 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.
GOOGLE ADS CONVERSION IMPORT — Docs | HiveJournal