← Back to Documentation
    DocumentationintegrationsFeb 11, 2025

    Conversion Tracking Debugger Reference

    Complete debugging guide for conversion tracking across GTM, GA4, Google Ads, and Meta Pixel. Covers discrepancy diagnosis, BigQuery validation queries, Consent Mode v2, Enhanced Conversions, server-side tagging, and step-by-step troubleshooting flowcharts.

    Overview

    Conversion tracking breaks silently. Tags fire but data never arrives. Platforms report different numbers for the same event. This guide provides a systematic approach to diagnosing and fixing conversion tracking issues across the full stack: Google Tag Manager, GA4, Google Ads, and Meta Pixel.

    The Conversion Tracking Stack

    Understanding data flow is the first step to debugging. Here is how the major platforms connect:

    User Action (click, form submit, purchase)
            |
            v
    +-------------------+
    |   Website / App   |
    |   (dataLayer)     |
    +-------------------+
            |
            v
    +-------------------+       +-------------------+
    |  Google Tag       | ----> |  GA4              |
    |  Manager (GTM)    |       |  (Measurement     |
    |                   |       |   Protocol)       |
    |  - GA4 Config Tag |       +-------------------+
    |  - GA4 Event Tag  |               |
    |  - Google Ads Tag |               v
    |  - Meta Pixel Tag |       +-------------------+
    +-------------------+       |  BigQuery Export  |
            |                   |  (daily/intraday) |
            |                   +-------------------+
            |
            +----------> +-------------------+
            |            |  Google Ads       |
            |            |  (Conversion      |
            |            |   Tracking)       |
            |            +-------------------+
            |
            +----------> +-------------------+
                         |  Meta Pixel       |
                         |  + Conversions    |
                         |    API (CAPI)     |
                         +-------------------+
    
    Server-Side Tagging (optional):
    
    +-------------------+       +-------------------+
    |  GTM Web          | ----> |  GTM Server       |
    |  Container        |       |  Container        |
    +-------------------+       +-------------------+
                                        |
                            +-----------+-----------+
                            |           |           |
                            v           v           v
                          GA4      Google Ads    Meta CAPI
    

    Key data flow points:

    1. dataLayer push -- The website pushes event data to window.dataLayer
    2. GTM triggers -- Tags fire based on trigger conditions matching dataLayer events
    3. Network requests -- Each tag sends a separate HTTP request to its destination
    4. Platform processing -- Each platform processes, deduplicates, and attributes conversions independently
    5. BigQuery export -- GA4 exports raw events to BigQuery (daily at ~5 AM property timezone, intraday every few hours)

    Common Discrepancies and Why

    GA4 vs Google Ads Conversion Count Differences

    GA4 and Google Ads will almost never show the same conversion count. This is expected behavior, not a bug.

    FactorGA4Google Ads
    Attribution modelData-driven (cross-channel)Data-driven (Google channels only)
    Counting methodConfigurable: once per session or once per eventConfigurable: one conversion or every conversion
    Conversion windowDefault 30 daysDefault 30 days (configurable up to 90 days)
    Cross-deviceBased on Google Signals + User-IDBased on Google account sign-in
    View-throughNot counted by defaultCounts view-through for Display/Video (1-day default)
    Data freshness24-72 hour processing lagConversions can appear within hours
    Modeled conversionsBehavioral modeling for consent gapsConversion modeling for unobserved conversions

    Typical variance: 5-20% difference is normal. Over 20% warrants investigation.

    Meta Pixel vs GA4 Differences

    FactorMetaGA4
    Click-through window7 days (default)30 days (default)
    View-through1-day view-through included by defaultNot counted by default
    AttributionLast-touch within Meta ecosystemData-driven cross-channel
    DeduplicationUses eventID for browser+CAPI dedupUses transaction_id parameter
    Cross-deviceFacebook login graphGoogle Signals + User-ID
    Modeled dataAggregated Event Measurement for iOS 14+Consent mode modeling

    Why Meta often reports higher: View-through conversions. A user sees a Meta ad, does not click, later converts via Google search. Meta counts it; GA4 attributes it to Google organic/CPC.

    GTM Firing but GA4 Not Receiving

    Common causes when GTM shows tags firing but GA4 has no data:

    1. Consent Mode blocking -- Tag fires but sends no data because consent was denied. Check analytics_storage consent state.
    2. Ad blockers -- ~25-30% of users run ad blockers that block google-analytics.com and googletagmanager.com requests.
    3. Race conditions -- Page unloads before the GA4 request completes. Common on form submits and outbound link clicks.
    4. Measurement ID mismatch -- Tag configured with wrong G-XXXXXXX ID.
    5. Filter/modification rules -- GA4 property has data filters or event modifications that hide or alter events.
    6. Debug vs production container -- Testing in GTM Preview mode uses the draft container, but live site uses the published version.

    Duplicate Conversions

    The most expensive tracking bug. Common causes:

    1. Missing transaction_id -- Without a unique transaction ID, GA4 cannot deduplicate purchase events. Every page reload fires another conversion.
    2. Double-firing tags -- Multiple tags for the same conversion event (e.g., both a GA4 event tag and a Google Ads conversion tag firing purchase with send_to including GA4).
    3. Thank-you page reloads -- User bookmarks or refreshes the order confirmation page.
    4. Back button -- User navigates back to the confirmation page from a subsequent page.
    5. Missing CAPI deduplication -- Meta Pixel and Conversions API both sending the same event without matching eventID.

    Prevention:

    // dataLayer push with transaction_id for deduplication
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'purchase',
      ecommerce: {
        transaction_id: 'T-12345',  // REQUIRED for dedup
        value: 99.99,
        currency: 'USD',
        items: [/* ... */]
      }
    });
    
    // Prevent duplicate dataLayer pushes on page reload
    if (!window.sessionStorage.getItem('purchase_tracked_T-12345')) {
      window.dataLayer.push({
        event: 'purchase',
        ecommerce: {
          transaction_id: 'T-12345',
          value: 99.99,
          currency: 'USD'
        }
      });
      window.sessionStorage.setItem('purchase_tracked_T-12345', 'true');
    }
    

    Debugging Checklist by Platform

    Google Tag Manager (GTM)

    1. Preview Mode Inspection

    Open GTM Preview mode (https://tagmanager.google.com > Preview) and verify:

    • The correct workspace/container version is active
    • Navigate to the page where the conversion should fire
    • Confirm the trigger event appears in the event timeline (left panel)
    • Confirm the tag shows as "Fired" (not "Not Fired")
    • Check the "Tags" tab for any tags that fired unexpectedly (duplicates)

    2. Trigger Conditions

    For each conversion tag, verify the trigger:

    • Trigger type matches the event (Custom Event, Page View, DOM Ready, etc.)
    • Event name in trigger matches dataLayer.push({ event: '...' }) exactly (case-sensitive)
    • All trigger conditions/filters are correct (URL contains, variable equals, etc.)
    • No blocking triggers are preventing the tag from firing

    3. Variable Values

    In Preview mode, click on the event in the timeline and check:

    • Data Layer variables resolve to expected values
    • transaction_id is populated and unique per transaction
    • value and currency are present and correct types (number, not string)
    • Custom JavaScript variables return expected values (check for errors)

    4. dataLayer Inspection

    Open browser console and inspect the dataLayer directly:

    // View full dataLayer contents
    console.table(window.dataLayer);
    
    // Filter for specific events
    window.dataLayer.filter(e => e.event === 'purchase');
    
    // Check ecommerce data
    window.dataLayer
      .filter(e => e.event === 'purchase')
      .map(e => e.ecommerce);
    

    5. Network Request Verification

    In Chrome DevTools Network tab, filter for:

    • google-analytics.com/g/collect -- GA4 requests
    • googleads.g.doubleclick.net/pagead/conversion -- Google Ads conversion requests
    • facebook.com/tr -- Meta Pixel requests

    Verify the request payload contains the expected parameters.

    GA4

    1. DebugView

    Enable DebugView for real-time event inspection:

    // Enable debug mode via gtag.js
    gtag('config', 'G-XXXXXXX', { debug_mode: true });
    

    Or install the GA Debugger Chrome extension. Then check GA4 > Admin > DebugView:

    • Events appear in the event stream
    • Event parameters are present and correct
    • User properties are populated
    • No error events visible

    2. Realtime Reports

    GA4 > Reports > Realtime:

    • Event count is incrementing when you trigger the conversion
    • Conversions card shows the event (if marked as conversion)
    • User location and device match your test setup

    3. BigQuery Raw Event Validation

    The definitive source of truth. Query the raw export:

    -- Check if conversion events are landing in BigQuery
    SELECT
      event_name,
      COUNT(*) as event_count,
      COUNT(DISTINCT user_pseudo_id) as unique_users,
      MIN(TIMESTAMP_MICROS(event_timestamp)) as earliest,
      MAX(TIMESTAMP_MICROS(event_timestamp)) as latest
    FROM `project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
      AND event_name IN ('purchase', 'generate_lead', 'sign_up', 'add_to_cart', 'begin_checkout')
    GROUP BY event_name
    ORDER BY event_count DESC
    

    4. Conversion Marking

    GA4 > Admin > Events > Mark as conversion:

    • The event name is marked as a conversion
    • The counting method is correct (once per event vs. once per session)
    • The event is actually being received (check Events report)

    Google Ads

    1. Conversion Action Status

    Google Ads > Goals > Conversions > Summary:

    • Status shows "Recording conversions" (green badge)
    • "No recent conversions" means the tag is installed but not firing or data is delayed
    • "Tag inactive" means the tag has not received any hits in 7+ days
    • "Unverified" means the tag was never detected on the website

    2. Tag Diagnostics

    Google Ads > Goals > Conversions > select action > Diagnostics:

    • Tag is detected on the correct pages
    • No "tag not found" warnings
    • Enhanced conversions status (if enabled)
    • Consent mode status

    3. Google Tag Setup

    Verify the Google tag configuration:

    // Check if Google Ads conversion tracking is loading
    // In browser console:
    typeof gtag === 'function'  // Should return true
    
    // Verify conversion linker is present
    // Look for _gcl_aw cookie:
    document.cookie.split(';').filter(c => c.includes('_gcl'))
    

    4. Enhanced Conversions Verification

    // Check enhanced conversion data is being sent
    // In GTM Preview mode, look for the Google Ads Conversion tag
    // and verify the "User-provided data" section shows:
    // - Email (hashed)
    // - Phone (hashed)
    // - Name (first + last)
    // - Address (street, city, region, postal, country)
    

    Meta Pixel

    1. Events Manager Test Events

    Meta Events Manager > Data Sources > Your Pixel > Test Events:

    • Enter your website URL and click "Open Website"
    • Perform the conversion action
    • Check that the event appears in the test events log
    • Verify event parameters (value, currency, content_ids, etc.)
    • Check the "Event Match Quality" score (aim for 6+)

    2. Pixel Helper Browser Extension

    Install Meta Pixel Helper:

    • Green icon = pixel detected and firing
    • Check each event has correct parameters
    • Look for warnings (missing parameters, invalid values)
    • Verify eventID is present for CAPI deduplication

    3. Conversions API (CAPI) Event Deduplication

    When using both browser Pixel and server-side CAPI, both events must share:

    • Same eventID value (or event_id in CAPI)
    • Same event_name
    • Events sent within 48 hours of each other
    // Browser-side: include eventID in fbq call
    fbq('track', 'Purchase', {
      value: 99.99,
      currency: 'USD',
      content_ids: ['SKU-123'],
      content_type: 'product'
    }, { eventID: 'purchase_T-12345' });
    
    # Server-side CAPI call (curl example)
    curl -X POST \
      "https://graph.facebook.com/v18.0/PIXEL_ID/events?access_token=TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "data": [{
          "event_name": "Purchase",
          "event_time": 1700000000,
          "event_id": "purchase_T-12345",
          "action_source": "website",
          "user_data": {
            "em": ["HASHED_EMAIL"],
            "ph": ["HASHED_PHONE"],
            "client_ip_address": "1.2.3.4",
            "client_user_agent": "Mozilla/5.0..."
          },
          "custom_data": {
            "value": 99.99,
            "currency": "USD",
            "content_ids": ["SKU-123"],
            "content_type": "product"
          }
        }]
      }'
    

    4. Event Match Quality

    Meta Events Manager > Data Sources > Your Pixel > Overview > Event Match Quality:

    • Score of 6.0 or higher recommended
    • Check which parameters are missing (email, phone, fbp, fbc, external_id)
    • Higher match quality = better attribution = lower CPAs

    BigQuery Validation Queries

    1. Count Conversions by Day and Compare to GA4 UI

    -- Compare this output against GA4 > Reports > Engagement > Conversions
    SELECT
      PARSE_DATE('%Y%m%d', event_date) as date,
      event_name,
      COUNT(*) as total_events,
      COUNT(DISTINCT user_pseudo_id) as unique_users,
      COUNT(DISTINCT
        CONCAT(user_pseudo_id, '-',
          (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id'))
      ) as unique_sessions
    FROM `project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
      AND event_name IN ('purchase', 'generate_lead', 'sign_up')
    GROUP BY 1, 2
    ORDER BY 1 DESC, 2
    

    Note: Small differences (1-5%) between BigQuery and GA4 UI are normal due to:

    • Thresholding in the GA4 UI (data suppressed for small user counts)
    • Sampling in the GA4 UI for large datasets
    • Consent mode modeled conversions (present in UI, absent in BigQuery)

    2. Find Duplicate Transaction IDs

    -- Find transactions that appear more than once (duplicate conversions)
    WITH purchase_events AS (
      SELECT
        PARSE_DATE('%Y%m%d', event_date) as date,
        user_pseudo_id,
        (SELECT value.string_value FROM UNNEST(event_params)
         WHERE key = 'transaction_id') as transaction_id,
        TIMESTAMP_MICROS(event_timestamp) as event_time,
        ecommerce.purchase_revenue_in_usd as revenue
      FROM `project.analytics_PROPERTY_ID.events_*`
      WHERE _TABLE_SUFFIX BETWEEN
        FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
        AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
        AND event_name = 'purchase'
    )
    
    SELECT
      transaction_id,
      COUNT(*) as occurrence_count,
      COUNT(DISTINCT user_pseudo_id) as distinct_users,
      MIN(event_time) as first_occurrence,
      MAX(event_time) as last_occurrence,
      TIMESTAMP_DIFF(MAX(event_time), MIN(event_time), SECOND) as seconds_between,
      SUM(revenue) as total_revenue_recorded,
      MAX(revenue) as actual_revenue
    FROM purchase_events
    WHERE transaction_id IS NOT NULL
    GROUP BY transaction_id
    HAVING COUNT(*) > 1
    ORDER BY occurrence_count DESC
    
    -- Find purchase events with NULL transaction_id (cannot be deduplicated)
    SELECT
      PARSE_DATE('%Y%m%d', event_date) as date,
      COUNT(*) as purchases_without_txn_id,
      SUM(ecommerce.purchase_revenue_in_usd) as revenue_at_risk
    FROM `project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
      AND event_name = 'purchase'
      AND (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') IS NULL
    GROUP BY 1
    ORDER BY 1 DESC
    

    3. Check Conversion Event Parameters Completeness

    -- Audit which parameters are present on conversion events
    SELECT
      event_name,
      param.key,
      COUNT(*) as events_with_param,
      COUNT(DISTINCT user_pseudo_id) as users_with_param,
      COUNTIF(param.value.string_value IS NOT NULL) as has_string_value,
      COUNTIF(param.value.int_value IS NOT NULL) as has_int_value,
      COUNTIF(param.value.float_value IS NOT NULL) as has_float_value,
      COUNTIF(param.value.double_value IS NOT NULL) as has_double_value
    FROM `project.analytics_PROPERTY_ID.events_*`,
      UNNEST(event_params) as param
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
      AND event_name IN ('purchase', 'generate_lead', 'sign_up', 'begin_checkout')
    GROUP BY 1, 2
    ORDER BY 1, events_with_param DESC
    
    -- Check for required ecommerce fields on purchase events
    SELECT
      PARSE_DATE('%Y%m%d', event_date) as date,
      COUNT(*) as total_purchases,
      COUNTIF(ecommerce.transaction_id IS NOT NULL) as has_transaction_id,
      COUNTIF(ecommerce.purchase_revenue_in_usd > 0) as has_revenue,
      COUNTIF(ecommerce.total_item_quantity > 0) as has_quantity,
      COUNTIF(ARRAY_LENGTH(items) > 0) as has_items,
      COUNTIF((SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'currency') IS NOT NULL) as has_currency,
      -- Completeness percentage
      ROUND(SAFE_DIVIDE(
        COUNTIF(
          ecommerce.transaction_id IS NOT NULL
          AND ecommerce.purchase_revenue_in_usd > 0
          AND ARRAY_LENGTH(items) > 0
        ),
        COUNT(*)
      ) * 100, 1) as pct_fully_complete
    FROM `project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
      AND event_name = 'purchase'
    GROUP BY 1
    ORDER BY 1 DESC
    

    4. Validate Enhanced Conversions Data

    -- Check for enhanced conversion signals in user properties
    -- Enhanced conversions data appears as hashed user data
    SELECT
      PARSE_DATE('%Y%m%d', event_date) as date,
      event_name,
      COUNT(*) as total_events,
      -- Check for user-provided data signals
      COUNTIF(user_id IS NOT NULL) as has_user_id,
      COUNTIF(
        (SELECT value.string_value FROM UNNEST(user_properties) WHERE key = 'email') IS NOT NULL
        OR (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'user_data_email') IS NOT NULL
      ) as has_email_signal,
      COUNTIF(
        (SELECT value.string_value FROM UNNEST(user_properties) WHERE key = 'phone') IS NOT NULL
        OR (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'user_data_phone') IS NOT NULL
      ) as has_phone_signal
    FROM `project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
      AND event_name IN ('purchase', 'generate_lead', 'sign_up')
    GROUP BY 1, 2
    ORDER BY 1 DESC, 2
    

    5. Cross-Reference GA4 Events with Expected GTM Triggers

    -- Compare expected conversion events against actual received events
    -- Use this to find events that GTM should send but GA4 never receives
    WITH expected_events AS (
      SELECT event_name FROM UNNEST([
        'purchase', 'add_to_cart', 'begin_checkout', 'view_item',
        'generate_lead', 'sign_up', 'add_payment_info', 'add_shipping_info'
      ]) as event_name
    ),
    
    actual_events AS (
      SELECT
        event_name,
        COUNT(*) as event_count,
        MAX(TIMESTAMP_MICROS(event_timestamp)) as last_seen
      FROM `project.analytics_PROPERTY_ID.events_*`
      WHERE _TABLE_SUFFIX BETWEEN
        FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
        AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
      GROUP BY event_name
    )
    
    SELECT
      e.event_name,
      COALESCE(a.event_count, 0) as event_count,
      a.last_seen,
      CASE
        WHEN a.event_count IS NULL THEN 'MISSING - never received'
        WHEN a.last_seen < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR) THEN 'STALE - no events in 48h'
        ELSE 'OK'
      END as status
    FROM expected_events e
    LEFT JOIN actual_events a USING(event_name)
    ORDER BY
      CASE
        WHEN a.event_count IS NULL THEN 0
        WHEN a.last_seen < TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 48 HOUR) THEN 1
        ELSE 2
      END,
      e.event_name
    

    Consent Mode v2

    How Consent Mode Affects Conversion Tracking

    Google Consent Mode v2 (required in the EEA since March 2024) controls whether tags send full data, limited data, or no data based on user consent.

    Two consent signals:

    SignalControlsRequired
    analytics_storageGA4 cookies and full measurementYes (for GA4)
    ad_storageGoogle Ads cookies, remarketing, conversion measurementYes (for Google Ads)

    Additional signals (Consent Mode v2):

    SignalControlsRequired
    ad_user_dataSending user data to Google for advertisingYes (EEA, required for Enhanced Conversions)
    ad_personalizationRemarketing and personalized advertisingYes (EEA)

    Consent Mode Behavior

    Consent StateWhat Happens
    grantedFull data collection, cookies set, user-level data sent
    deniedCookieless pings sent (no user identifiers), Google models conversions
    Not setTreated as granted (but risks compliance issues)

    Modeled conversions: When consent is denied, Google estimates conversions using machine learning based on observed patterns from consented users. Modeled conversions appear in the GA4 UI and Google Ads but do NOT appear in BigQuery exports.

    Implementation Checklist

    // 1. Set default consent state BEFORE loading GTM
    // Place this in <head> before the GTM snippet
    window.dataLayer = window.dataLayer || [];
    function gtag() { dataLayer.push(arguments); }
    
    // Default: deny all (adjust based on your legal requirements)
    gtag('consent', 'default', {
      analytics_storage: 'denied',
      ad_storage: 'denied',
      ad_user_data: 'denied',
      ad_personalization: 'denied',
      wait_for_update: 500  // Wait 500ms for CMP to load
    });
    
    // 2. Update consent when user makes a choice (called by your CMP)
    function updateConsent(consentChoices) {
      gtag('consent', 'update', {
        analytics_storage: consentChoices.analytics ? 'granted' : 'denied',
        ad_storage: consentChoices.marketing ? 'granted' : 'denied',
        ad_user_data: consentChoices.marketing ? 'granted' : 'denied',
        ad_personalization: consentChoices.marketing ? 'granted' : 'denied'
      });
    }
    
    // 3. Verify consent state in browser console
    // Check current consent state
    dataLayer.filter(e => e[0] === 'consent');
    

    GTM Consent Mode setup:

    • Consent Mode default is set BEFORE GTM snippet loads
    • All four v2 signals are configured (analytics_storage, ad_storage, ad_user_data, ad_personalization)
    • CMP (Cookie Consent Platform) calls gtag('consent', 'update', ...) on user choice
    • GTM tags have correct consent settings (Built-in consent checks enabled)
    • wait_for_update is set (500ms recommended) to give CMP time to initialize
    • Consent state persists across pages (CMP handles cookie storage)
    • Test with consent denied: verify cookieless pings are sent (Network tab: gcs=G100 parameter)
    • Test with consent granted: verify full measurement resumes

    BigQuery: Identify Consent Mode Impact

    -- Check consent-related signals in your data
    -- Events from users who denied consent will have limited parameters
    SELECT
      PARSE_DATE('%Y%m%d', event_date) as date,
      COUNTIF(user_pseudo_id IS NOT NULL) as events_with_user_id,
      COUNTIF(user_pseudo_id IS NULL) as events_without_user_id,
      COUNTIF(
        (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') IS NOT NULL
      ) as events_with_session,
      COUNTIF(
        (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') IS NULL
      ) as events_without_session,
      -- Approximate consent denial rate
      ROUND(SAFE_DIVIDE(
        COUNTIF((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') IS NULL),
        COUNT(*)
      ) * 100, 1) as pct_likely_consent_denied
    FROM `project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN
      FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
      AND FORMAT_DATE('%Y%m%d', CURRENT_DATE() - 1)
    GROUP BY 1
    ORDER BY 1 DESC
    

    Enhanced Conversions

    What Data Is Sent

    Enhanced Conversions improve conversion measurement by sending hashed first-party customer data alongside your conversion tags. Google matches this data against signed-in Google accounts.

    Supported data fields:

    FieldFormatRequired
    EmailSHA-256 hashed, lowercase, trimmedAt least one of: email, phone, or name+address
    PhoneSHA-256 hashed, E.164 format (+1234567890)Optional but recommended
    First nameSHA-256 hashed, lowercase, trimmedOptional
    Last nameSHA-256 hashed, lowercase, trimmedOptional
    Street addressSHA-256 hashed, lowercase, trimmedOptional
    CitySHA-256 hashed, lowercase, trimmedOptional
    Region/StateSHA-256 hashed, lowercase, trimmedOptional
    Postal codeSHA-256 hashed, trimmedOptional
    CountryISO 3166-1 alpha-2, unhashedOptional

    How Hashing Works

    Google auto-hashes in most implementations, but if you hash manually:

    // SHA-256 hashing example (browser)
    async function hashValue(value) {
      const encoder = new TextEncoder();
      // Normalize: lowercase, trim whitespace
      const normalized = value.toLowerCase().trim();
      const data = encoder.encode(normalized);
      const hashBuffer = await crypto.subtle.digest('SHA-256', data);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    }
    
    // Usage
    const hashedEmail = await hashValue('User@Example.com');
    // Result: consistent SHA-256 hash of "user@example.com"
    

    Setup via GTM

    Option A: Automatic collection (recommended)

    1. In the Google Ads Conversion tag, enable "Include user-provided data from your website"
    2. Select "Automatic" -- GTM will scan the page for email/phone/address fields
    3. GTM uses CSS selectors and autocomplete attributes to identify fields

    Option B: Manual configuration via dataLayer

    // Push user data to dataLayer (GTM will auto-hash)
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'purchase',
      user_data: {
        email: 'user@example.com',       // Will be hashed by GTM
        phone_number: '+11234567890',      // Will be hashed by GTM
        address: {
          first_name: 'John',
          last_name: 'Doe',
          street: '123 Main St',
          city: 'New York',
          region: 'NY',
          postal_code: '10001',
          country: 'US'
        }
      },
      ecommerce: {
        transaction_id: 'T-12345',
        value: 99.99,
        currency: 'USD'
      }
    });
    

    Option C: Code-based (gtag.js direct)

    gtag('set', 'user_data', {
      email: 'user@example.com',
      phone_number: '+11234567890',
      address: {
        first_name: 'John',
        last_name: 'Doe',
        street: '123 Main St',
        city: 'New York',
        region: 'NY',
        postal_code: '10001',
        country: 'US'
      }
    });
    

    Validation

    • Google Ads > Goals > Conversions > select action > Diagnostics > Enhanced Conversions
    • Status should show "Recording" or "Active"
    • Match rate visible after sufficient data (48-72 hours)
    • GTM Preview mode: check the conversion tag's "User-provided data" tab
    • Network request: look for em= (hashed email) in the conversion pixel request

    Server-Side Tagging

    Benefits for Conversion Accuracy

    Server-side tagging moves tag execution from the user's browser to a server you control. Key benefits:

    1. Ad blocker resistance -- Requests go to your first-party domain, not blocked google-analytics.com
    2. Longer cookie lifetime -- First-party cookies set server-side last 2+ years vs. 7 days (ITP)
    3. Reduced page weight -- Fewer scripts in the browser = faster page load
    4. Data control -- Inspect and modify data before sending to vendors
    5. Better consent enforcement -- Single point to enforce consent rules server-side

    GTM Server Container Setup Overview

    Browser                    Your Infrastructure              Vendor Endpoints
    +----------------+        +--------------------+           +----------------+
    | GTM Web        | -----> | GTM Server         | -------> | GA4            |
    | Container      |  HTTPS | Container          |           +----------------+
    |                |        | (Cloud Run / GCE)  | -------> | Google Ads     |
    | Sends GA4      |        |                    |           +----------------+
    | measurement    |        | Tags:              | -------> | Meta CAPI      |
    | protocol hit   |        | - GA4 tag          |           +----------------+
    +----------------+        | - Google Ads tag   |
                              | - Meta CAPI tag    |
                              +--------------------+
                                       |
                                       v
                              First-party domain:
                              gtm.yourdomain.com
    

    Key setup steps:

    1. Create server container in GTM (Server-side container type)
    2. Deploy to Cloud Run (recommended) or App Engine
    3. Configure first-party domain -- Map gtm.yourdomain.com to your server container
    4. Update web container -- Change the GA4 Config tag's transport_url to your server domain
    5. Add server-side tags -- GA4, Google Ads Conversion, Meta CAPI
    6. Configure server-side consent -- Mirror browser consent decisions

    First-Party Domain Setup

    // In your GTM web container's GA4 Config tag:
    // Set the server container URL as the transport URL
    gtag('config', 'G-XXXXXXX', {
      server_container_url: 'https://gtm.yourdomain.com'
    });
    

    DNS setup:

    # Add CNAME or A record pointing to your Cloud Run service
    gtm.yourdomain.com  CNAME  your-cloud-run-service-xxxxxxxxx.run.app
    

    Troubleshooting Flowcharts

    Flowchart 1: Conversions Not Showing Up

    START: Conversion not appearing in [Platform]
      |
      v
    Is the tag installed on the page?
      |
      +-- NO --> Install the tag. Check GTM container is published.
      |           Verify GTM snippet is in <head> on all pages.
      |
      +-- YES
           |
           v
      Does the tag fire in GTM Preview mode?
           |
           +-- NO --> Check trigger configuration:
           |          - Event name matches exactly (case-sensitive)?
           |          - Trigger conditions met (URL, variable values)?
           |          - Blocking triggers preventing fire?
           |          - dataLayer.push() happening before tag evaluation?
           |
           +-- YES
                |
                v
           Do you see the network request in DevTools?
                |
                +-- NO --> Consent Mode blocking the request?
                |          - Check: analytics_storage / ad_storage state
                |          - Check: ad blocker active?
                |          - Check: CSP (Content Security Policy) blocking?
                |
                +-- YES
                     |
                     v
                Is the request returning 200/204 (success)?
                     |
                     +-- NO --> Check endpoint URL, measurement ID, API credentials.
                     |          For Meta CAPI: check access token and pixel ID.
                     |
                     +-- YES
                          |
                          v
                     Check platform-specific processing:
                     - GA4: Wait 24-48h. Check DebugView for real-time.
                       Check data filters, event modification rules.
                     - Google Ads: Wait 24h. Check conversion action status.
                       Verify conversion is included in "Conversions" column.
                     - Meta: Check Events Manager > Test Events.
                       Verify event match quality. Check CAPI dedup.
    

    Flowchart 2: Conversion Count Mismatch

    START: Platform A shows X conversions, Platform B shows Y
      |
      v
    Is the difference > 20%?
      |
      +-- NO --> Likely normal. Different attribution models,
      |          counting methods, and conversion windows
      |          will always cause some variance. Document
      |          the baseline variance for future comparison.
      |
      +-- YES
           |
           v
      Which platform shows MORE conversions?
           |
           +-- Google Ads > GA4:
           |     - Google Ads counting "every" conversion vs GA4 "once per session"?
           |     - Google Ads view-through conversions included (Display/Video)?
           |     - Google Ads longer conversion window (90d vs 30d)?
           |     - Google Ads modeled conversions from Consent Mode?
           |
           +-- GA4 > Google Ads:
           |     - GA4 counting all channels, Google Ads only Google-attributed?
           |     - Check: is the conversion action set to "Primary" in Google Ads?
           |     - Google Ads conversion tag not firing on some pages?
           |
           +-- Meta > GA4:
           |     - Meta counting view-through conversions (1-day window)?
           |     - Meta 7-day click window overlapping with GA4 30-day window?
           |     - Meta Aggregated Event Measurement modeling for iOS users?
           |
           +-- GA4 > Meta:
                 - Meta Pixel blocked by ad blockers more than GA4?
                 - CAPI not configured (missing server-side events)?
                 - Meta event match quality too low (< 5)?
                 - iOS 14+ App Tracking Transparency limiting Meta data?
           |
           v
      Run BigQuery validation queries (above) to get ground truth.
      Compare raw event counts against both platforms.
    

    Flowchart 3: Conversion Value Wrong

    START: Conversion value incorrect in [Platform]
      |
      v
    Is the value always 0 or NULL?
      |
      +-- YES --> Check dataLayer push:
      |           - Is 'value' property present?
      |           - Is 'currency' property present? (Required for Google Ads)
      |           - Is value a number (not a string)?
      |           - GTM variable mapping: value variable resolving correctly?
      |
      +-- NO
           |
           v
      Is the value consistently wrong (e.g., always 1.00)?
           |
           +-- YES --> Check GTM tag configuration:
           |          - Is the value field mapped to the correct variable?
           |          - Is a default/static value overriding the dynamic value?
           |          - Currency conversion: sending local currency but
           |            platform expecting USD?
           |
           +-- NO (value varies but is incorrect)
                |
                v
           Is the value doubled or multiplied?
                |
                +-- YES --> Duplicate conversion events (see duplicate section above).
                |          Multiple tags sending the same conversion.
                |          dataLayer.push firing multiple times.
                |
                +-- NO
                     |
                     v
                Check data type issues:
                - Value including tax when it shouldn't (or vice versa)?
                - Value including shipping?
                - Currency mismatch (sending cents vs dollars)?
                - String formatting ("$99.99" vs 99.99)?
                - Locale issues ("99,99" vs "99.99")?
                |
                v
                Validate in BigQuery:
                SELECT
                  ecommerce.transaction_id,
                  ecommerce.purchase_revenue_in_usd,
                  ecommerce.purchase_revenue,
                  ecommerce.tax_value_in_usd,
                  ecommerce.shipping_value_in_usd
                FROM events_*
                WHERE event_name = 'purchase'
                ORDER BY event_timestamp DESC
                LIMIT 20
    

    Quick Reference: dataLayer Push Examples

    Standard E-Commerce Purchase

    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({ ecommerce: null });  // Clear previous ecommerce data
    window.dataLayer.push({
      event: 'purchase',
      ecommerce: {
        transaction_id: 'T-12345',
        value: 99.99,
        tax: 8.50,
        shipping: 5.99,
        currency: 'USD',
        coupon: 'SUMMER2025',
        items: [{
          item_id: 'SKU-001',
          item_name: 'Product Name',
          item_brand: 'Brand',
          item_category: 'Category',
          item_variant: 'Blue',
          price: 99.99,
          quantity: 1,
          coupon: 'ITEM10OFF'
        }]
      }
    });
    

    Lead Generation

    window.dataLayer.push({
      event: 'generate_lead',
      value: 50.00,
      currency: 'USD',
      lead_source: 'contact_form',
      form_id: 'demo_request'
    });
    

    gtag.js Conversion Tracking (No GTM)

    // Google Ads conversion
    gtag('event', 'conversion', {
      send_to: 'AW-XXXXXXXXX/CONVERSION_LABEL',
      value: 99.99,
      currency: 'USD',
      transaction_id: 'T-12345'
    });
    
    // GA4 purchase event
    gtag('event', 'purchase', {
      transaction_id: 'T-12345',
      value: 99.99,
      currency: 'USD',
      items: [{ item_id: 'SKU-001', item_name: 'Product', price: 99.99, quantity: 1 }]
    });
    

    Claude Code Skill

    This debugger reference is also available as a free Claude Code skill -- use it directly in your terminal with live GTM, GA4, Google Ads, and Meta API access:

    # Install
    curl -sSL https://raw.githubusercontent.com/cognyai/claude-code-marketing-skills/main/install.sh | bash
    
    # Use
    /conversion-debug                          # Full debugging overview
    /conversion-debug duplicate conversions    # Diagnose duplicate purchases
    /conversion-debug consent mode             # Consent Mode v2 audit
    /conversion-debug meta vs ga4              # Cross-platform discrepancy analysis
    

    View on GitHub ->

    Resources

    Ready for similar results?
    Start with Solo at $9/mo or talk to us about Cloud.
    ❯ get startedcompare plans →