DEV Community

Joshua Matthews
Joshua Matthews

Posted on

Google Ads Conversion Tracking: A Developer's Implementation Guide

Marketing teams want conversion tracking. Developers get handed a bunch of scripts and told to "make it work."

Here's how to implement Google Ads conversion tracking properly, without breaking your site.

The Basics: What We're Tracking

Conversions are valuable actions: purchases, form submissions, sign-ups, phone calls. Google Ads uses this data to optimise ad delivery and report ROI.

Two components:

  1. Global site tag (gtag.js): Loads on every page
  2. Conversion tracking: Fires when a conversion happens

Installing the Global Site Tag

The marketing team will give you a conversion ID (looks like AW-XXXXXXXXX).

For Next.js, add to your layout:

// app/layout.jsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <Script
          src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
          strategy="afterInteractive"
        />
        <Script id="gtag-init" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${process.env.NEXT_PUBLIC_GA_ID}');
            gtag('config', '${process.env.NEXT_PUBLIC_GADS_ID}');
          `}
        </Script>
      </head>
      <body>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Environment variables:

NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GADS_ID=AW-XXXXXXXXX
Enter fullscreen mode Exit fullscreen mode

Tracking Purchase Conversions

The marketing team will give you a conversion label (looks like AbCdEfGhIjKlMnOp).

Fire this when payment is confirmed:

// lib/analytics.js
export function trackPurchase(transactionData) {
  if (typeof window === 'undefined' || !window.gtag) return;

  window.gtag('event', 'conversion', {
    send_to: `${process.env.NEXT_PUBLIC_GADS_ID}/AbCdEfGhIjKlMnOp`,
    value: transactionData.total,
    currency: 'GBP',
    transaction_id: transactionData.orderId
  });
}

// Usage on thank you page
// app/order/[orderId]/page.jsx
'use client';
import { useEffect } from 'react';
import { trackPurchase } from '@/lib/analytics';export default function OrderConfirmation({ order }) {
  useEffect(() => {
    trackPurchase({
      total: order.total,
      orderId: order.id
    });
  }, [order]);

  return (
    <div>
      <h1>Thank you for your order!</h1>
      {/* Order details */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Important: Only fire conversion once. Use the transaction ID to prevent duplicates.

Tracking Form Submissions

// components/ContactForm.jsx
'use client';
import { trackFormSubmission } from '@/lib/analytics';export function ContactForm() {
  const handleSubmit = async (e) => {
    e.preventDefault();

    const response = await submitForm(formData);

    if (response.success) {
      // Track the conversion
      trackFormSubmission('contact_form');
      // Redirect or show success
    }
  };

  return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}

// lib/analytics.js
export function trackFormSubmission(formName) {
  if (typeof window === 'undefined' || !window.gtag) return;

  window.gtag('event', 'conversion', {
    send_to: `${process.env.NEXT_PUBLIC_GADS_ID}/FormConversionLabel`,
    event_category: 'form',
    event_label: formName
  });
}
Enter fullscreen mode Exit fullscreen mode

Enhanced Conversions

Enhanced conversions improve tracking accuracy by sending hashed user data:

// Track with enhanced conversion data
export function trackPurchaseEnhanced(transactionData, userData) {
  if (typeof window === 'undefined' || !window.gtag) return;

  // Set user data for enhanced conversions
  window.gtag('set', 'user_data', {
    email: userData.email,
    phone_number: userData.phone,
    address: {
      first_name: userData.firstName,
      last_name: userData.lastName,
      city: userData.city,
      postal_code: userData.postcode,
      country: 'GB'
    }
  });

  window.gtag('event', 'conversion', {
    send_to: `${process.env.NEXT_PUBLIC_GADS_ID}/PurchaseLabel`,
    value: transactionData.total,
    currency: 'GBP',
    transaction_id: transactionData.orderId
  });
}
Enter fullscreen mode Exit fullscreen mode

Google hashes this data automatically before sending.

Server-Side Conversion Tracking

For more reliable tracking (avoids ad blockers):

// app/api/track-conversion/route.js
export async function POST(request) {
  const { conversionAction, orderId, value } = await request.json();

  const response = await fetch(
    'https://www.google-analytics.com/mp/collect' +
    `?measurement_id=${process.env.GA_MEASUREMENT_ID}` +
    `&api_secret=${process.env.GA_API_SECRET}`,
    {
      method: 'POST',
      body: JSON.stringify({
        client_id: getClientId(request), // From cookie
        events: [{
          name: conversionAction,
          params: {
            transaction_id: orderId,
            value: value,
            currency: 'GBP'
          }
        }]
      })
    }
  );

  return Response.json({ success: response.ok });
}
Enter fullscreen mode Exit fullscreen mode

For Google Ads specifically, use the Google Ads API for offline conversions:

// Import conversions via API
const { GoogleAdsApi } = require('google-ads-api');

async function importOfflineConversion(conversionData) {
  const client = new GoogleAdsApi({
    client_id: process.env.GOOGLE_ADS_CLIENT_ID,
    client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET,
    developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN
  });

  const customer = client.Customer({
    customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID,
    refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN
  });

  await customer.conversionUploads.uploadClickConversions({
    conversions: [{
      gclid: conversionData.gclid, // Captured at click time
      conversion_action: `customers/${customerId}/conversionActions/${conversionActionId}`,
      conversion_date_time: new Date().toISOString(),
      conversion_value: conversionData.value,
      currency_code: 'GBP'
    }]
  });
}
Enter fullscreen mode Exit fullscreen mode

Capturing GCLID for Offline Conversions

Store the Google Click ID when users arrive:

// lib/gclid.js
export function captureGclid() {
  if (typeof window === 'undefined') return;

  const urlParams = new URLSearchParams(window.location.search);
  const gclid = urlParams.get('gclid');

  if (gclid) {
    // Store in cookie (90 day expiry)
    document.cookie = `gclid=${gclid}; max-age=${90 * 24 * 60 * 60}; path=/`;

    // Also store in localStorage as backup
    localStorage.setItem('gclid', gclid);
    localStorage.setItem('gclid_timestamp', Date.now().toString());
  }
}

export function getGclid() {
  // Try cookie first
  const cookies = document.cookie.split(';');
  const gclidCookie = cookies.find(c => c.trim().startsWith('gclid='));
  if (gclidCookie) {
    return gclidCookie.split('=')[1];
  }

  // Fall back to localStorage
  return localStorage.getItem('gclid');
}
Enter fullscreen mode Exit fullscreen mode

Call captureGclid() on page load, store getGclid() with form submissions and orders.

Google Tag Manager Alternative

If your marketing team prefers GTM:

// app/layout.jsx
<Script id="gtm" strategy="afterInteractive">
  {`
    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXXX');
  `}
</Script>

// Push events to dataLayer
window.dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: order.id,
    value: order.total,
    currency: 'GBP',
    items: order.items.map(item => ({
      item_id: item.sku,
      item_name: item.name,
      price: item.price,
      quantity: item.quantity
    }))
  }
});
Enter fullscreen mode Exit fullscreen mode

Then marketing configures triggers and tags in GTM without code changes.

Testing Conversions

  1. Google Tag Assistant: Browser extension to verify tags fire
  2. Real-time reports: Check GA4 real-time to see events
  3. Google Ads conversion diagnostic: Shows if conversions are recording
  4. Test transactions: Use a test order and verify it appears in Google Ads

Common Issues

Conversions not recording:

  • Check conversion window (default 30 days)
  • Verify gtag is loading (no ad blocker)
  • Ensure conversion fires after successful action, not on page load

Duplicate conversions:

  • Always use unique transaction IDs
  • Fire conversion only once per order

Value mismatch:

  • Ensure currency matches Google Ads account settings
  • Round values appropriately

Privacy and Consent

Don't forget cookie consent:

// Only initialise tracking after consent
function initAnalytics(hasConsent) {
  if (!hasConsent) {
    // Limited tracking without consent
    window.gtag('consent', 'default', {
      analytics_storage: 'denied',
      ad_storage: 'denied'
    });
  } else {
    window.gtag('consent', 'update', {
      analytics_storage: 'granted',
      ad_storage: 'granted'
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

At LogicLeap, we set up Google Ads campaigns that actually convert, with proper tracking from day one. No wasted ad spend on unmeasured results.

Top comments (0)