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:
- Global site tag (gtag.js): Loads on every page
- 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>
);
}
Environment variables:
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GADS_ID=AW-XXXXXXXXX
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>
);
}
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
});
}
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
});
}
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 });
}
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'
}]
});
}
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');
}
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
}))
}
});
Then marketing configures triggers and tags in GTM without code changes.
Testing Conversions
- Google Tag Assistant: Browser extension to verify tags fire
- Real-time reports: Check GA4 real-time to see events
- Google Ads conversion diagnostic: Shows if conversions are recording
- 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'
});
}
}
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)