Back to Blog
January 28, 2026
Technical TutorialGA4Conversion Tracking

Complete GA4 Conversion Tracking: Browser + Server-Side Implementation

How we built bulletproof conversion tracking for syntermedia.ai

The Problem with Browser-Only Tracking

Ad blockers break browser-side tracking. iOS privacy changes limit cookie lifetimes. Third-party cookies are dying. If you're still relying solely on client-side pixels, you're missing 30-50% of your conversions.

This tutorial walks through exactly how we set up conversion tracking for syntermedia.ai — combining browser-side GA4 events with server-side conversion APIs for Google Ads, Meta, Reddit, and LinkedIn.

What You'll Learn

  • Setting up GA4 with proper conversion events
  • Linking GA4 to Google Ads for browser-side attribution
  • Implementing server-side GCLID uploads for offline conversions
  • Multi-platform server-side tracking (Meta CAPI, Reddit CAPI, LinkedIn CAPI)
  • Building a unified conversion tracking service

The Architecture: Defense in Depth

Our conversion tracking uses two parallel paths to ensure nothing gets missed:

┌─────────────────────────────────────────────────────────────────┐
│                        User Signup                              │
└─────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
┌─────────────────────────┐     ┌─────────────────────────────────┐
│   Browser-Side (GA4)    │     │      Server-Side (APIs)         │
├─────────────────────────┤     ├─────────────────────────────────┤
│ • gtag('event', ...)    │     │ • Google Ads Offline Uploads    │
│ • Automatic event       │     │ • Meta Conversions API          │
│ • Works if no blocker   │     │ • Reddit Conversions API        │
│                         │     │ • LinkedIn Conversions API      │
│ → Flows to Google Ads   │     │                                 │
│   via GA4 property link │     │ → Works even with ad blockers   │
└─────────────────────────┘     └─────────────────────────────────┘

If the browser-side event fires, great. If the user has an ad blocker, the server-side upload ensures attribution still works.

Part 1: GA4 Browser-Side Setup

Step 1: Install the GA4 Tag

Add the gtag.js snippet to your site. For Next.js, put this in layout.tsx:

<Script
  id="gtag"
  strategy="afterInteractive"
  dangerouslySetInnerHTML={{
    __html: `
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'G-JN16WWF6EM');  // GA4 Measurement ID
      gtag('config', 'AW-17838828774'); // Google Ads Conversion ID
    `,
  }}
/>

Step 2: Fire Conversion Events

When a user signs up, fire the synter_signup event:

export function trackGA4Event(eventName: string, params?: Record<string, any>) {
  if (!window.gtag) return;
  window.gtag("event", eventName, params);
}

// Called after successful signup
trackGA4Event("synter_signup", {
  value: 1,
  currency: "USD",
});

Step 3: Mark Events as Conversions

In GA4 Admin → Events → Mark as conversion. Or use the Admin API:

ACCESS_TOKEN=$(gcloud auth application-default print-access-token)

curl -X POST \
  "https://analyticsadmin.googleapis.com/v1beta/properties/517384210/conversionEvents" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"eventName": "synter_signup"}'

Step 4: Link GA4 to Google Ads

In GA4 Admin → Google Ads Links → Link to your Google Ads account. Then in Google Ads → Tools → Conversions → Import → GA4 → Select your events.

⚠️ Common Mistake: Wrong GA4 Property

Make sure you link the correct GA4 property to Google Ads. We initially had conversions linked to an old property instead of our actual property. Zero conversions flowed through until we fixed it.

Part 2: Server-Side Google Ads Offline Conversions

Browser-side tracking breaks with ad blockers. Server-side uploads work by sending the GCLID directly to Google Ads when a conversion happens.

Step 1: Capture the GCLID

When users land from a Google ad, capture the gclid parameter:

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const url = request.nextUrl;
  
  // Capture Google Click ID
  const gclid = url.searchParams.get("gclid");
  if (gclid) {
    response.cookies.set("_gclid", gclid, {
      maxAge: 60 * 60 * 24 * 90, // 90 days
      httpOnly: true,
      secure: true,
      sameSite: "lax",
    });
  }
  
  return response;
}

Step 2: Upload on Conversion

When the user converts, read the GCLID and send it to Google Ads:

export async function uploadSignupConversion({ gclid, userId }: ConversionParams) {
  const customer = client.Customer({
    customer_id: GOOGLE_ADS_CUSTOMER_ID,
    refresh_token: GOOGLE_ADS_REFRESH_TOKEN,
  });

  const clickConversion = {
    gclid,
    conversion_action: `customers/${GOOGLE_ADS_CUSTOMER_ID}/conversionActions/${CONVERSION_ACTION_ID}`,
    conversion_date_time: formatGoogleAdsDateTime(new Date()),
    conversion_value: 1.0,
    currency_code: "USD",
  };

  const response = await customer.conversionUploads.uploadClickConversions({
    conversions: [clickConversion],
    partial_failure: true,
  });

  return { success: true, response };
}

Part 3: Multi-Platform Server-Side Tracking

The same pattern works for Meta, Reddit, and LinkedIn. Create a unified tracking service:

export async function trackSignupConversion(params: ConversionParams) {
  const eventId = generateEventId();
  
  // Record to database for audit trail
  const conversionId = await recordConversionToDb({ ...params, eventId });
  
  // Fire all uploads in parallel
  const [metaResult, googleResult, redditResult, linkedinResult] = 
    await Promise.all([
      sendToMeta(params),
      sendToGoogle(params),
      sendToReddit(params),
      sendToLinkedIn(params),
    ]);
  
  return { eventId, metaResult, googleResult, redditResult, linkedinResult };
}

Platform-Specific Click IDs

Each platform has its own Conversions API and click ID cookie:

PlatformAPIClick ID Cookie
Google AdsOffline Conversions APIgclid (URL param)
MetaConversions API (CAPI)_fbc, _fbp
RedditConversions APIrdt_cid, _rdt_uuid
LinkedInConversions APIli_fat_id

Part 4: The Cron Job for Retries

Sometimes uploads fail — rate limits, network issues, token expiry. Run a cron job to retry failed conversions:

export async function GET(request: NextRequest) {
  // Find conversions with gclid that haven't been uploaded
  const pendingConversions = await db.$queryRaw`
    SELECT id, gclid, synter_user_id, value
    FROM conversions
    WHERE gclid IS NOT NULL
      AND google_uploaded_at IS NULL
      AND timestamp > NOW() - INTERVAL '90 days'
    ORDER BY timestamp DESC
    LIMIT 20
  `;
  
  for (const conv of pendingConversions) {
    const result = await uploadSignupConversion({
      gclid: conv.gclid,
      userId: conv.synter_user_id,
    });
    
    if (result.success) {
      await markGoogleUploaded(conv.id);
    }
  }
  
  return NextResponse.json({ processed: pendingConversions.length });
}

Part 5: Using the Google Analytics MCP

Synter supports the Google Analytics MCP, allowing you to query GA4 data and configure conversions programmatically.

Available Tools

  • ga4_list_properties — List all GA4 accounts and properties
  • ga4_run_report — Run reports with custom dimensions and metrics
  • ga4_list_conversions — List all conversion events
  • ga4_create_conversion — Mark an event as a conversion
  • ga4_list_google_ads_links — Check Google Ads account links

Example: Pull Campaign Performance

{
  "script_name": "ga4_run_report",
  "platform": "GA4",
  "args": [
    "--property-id", "517384210",
    "--dimensions", "sessionCampaignName",
    "--metrics", "sessions,totalUsers,conversions,engagementRate",
    "--days", "30"
  ]
}

Results: Complete Attribution Coverage

After implementing this architecture, we saw:

  • +35% more conversions attributed — Server-side uploads caught conversions that ad blockers were hiding
  • Better ROAS visibility — Google Ads now has accurate conversion data for Smart Bidding
  • Multi-platform attribution — We can see which platform actually drove the conversion

Key Takeaways

  1. Never rely on browser-side tracking alone — Ad blockers will hide 30-50% of your conversions
  2. Capture click IDs in middleware — Store them in cookies before the user even hits your app
  3. Use the right conversion action types — GA4-linked for browser, Offline Upload for server-side
  4. Build retry logic — API calls fail; have a cron job to catch up
  5. Check your GA4 property links — A misconfigured link means zero conversions flow through

Try It Yourself

If you're using Synter, you can set up this same conversion tracking for your clients. The Campaign IDE agent can help you:

  • Configure GA4 conversion events
  • Verify Google Ads property links
  • Set up server-side tracking for multiple platforms
  • Monitor conversion attribution across channels

Open Campaign IDE →

Ready to let AI agents run your campaigns?

Start for free with 1,000 credits and launch campaigns across Google, Meta, LinkedIn, Reddit, and more.

Complete GA4 Conversion Tracking Guide: Browser + Server-Side Implementation | Synter