<!-- Cogny documentation. Canonical page: https://cogny.com/docs/conversion-tracking-debugger -->

# 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.

**Author:** Cogny Team  
**Published:** 2025-02-11  
**Updated:** 2026-10-09  
**Canonical:** https://cogny.com/docs/conversion-tracking-debugger

## 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.

| Factor | GA4 | Google Ads |
|--------|-----|------------|
| **Attribution model** | Data-driven (cross-channel) | Data-driven (Google channels only) |
| **Counting method** | Configurable: once per session or once per event | Configurable: one conversion or every conversion |
| **Conversion window** | Default 30 days | Default 30 days (configurable up to 90 days) |
| **Cross-device** | Based on Google Signals + User-ID | Based on Google account sign-in |
| **View-through** | Not counted by default | Counts view-through for Display/Video (1-day default) |
| **Data freshness** | 24-72 hour processing lag | Conversions can appear within hours |
| **Modeled conversions** | Behavioral modeling for consent gaps | Conversion modeling for unobserved conversions |

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

### Meta Pixel vs GA4 Differences

| Factor | Meta | GA4 |
|--------|------|-----|
| **Click-through window** | 7 days (default) | 30 days (default) |
| **View-through** | 1-day view-through included by default | Not counted by default |
| **Attribution** | Last-touch within Meta ecosystem | Data-driven cross-channel |
| **Deduplication** | Uses `eventID` for browser+CAPI dedup | Uses `transaction_id` parameter |
| **Cross-device** | Facebook login graph | Google Signals + User-ID |
| **Modeled data** | Aggregated 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:**

```javascript
// 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: [/* ... */]
  }
});
```

```javascript
// 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:

```javascript
// 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:

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

Or install the [GA Debugger Chrome extension](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna). 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:

```sql
-- 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:

```javascript
// 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**

```javascript
// 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](https://chrome.google.com/webstore/detail/meta-pixel-helper/fdgfkebogiimcoedlicjlajpkdmockpc):

- [ ] 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

```javascript
// 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' });
```

```bash
# 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

```sql
-- 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

```sql
-- 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
```

```sql
-- 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

```sql
-- 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
```

```sql
-- 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

```sql
-- 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

```sql
-- 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:**

| Signal | Controls | Required |
|--------|----------|----------|
| `analytics_storage` | GA4 cookies and full measurement | Yes (for GA4) |
| `ad_storage` | Google Ads cookies, remarketing, conversion measurement | Yes (for Google Ads) |

**Additional signals (Consent Mode v2):**

| Signal | Controls | Required |
|--------|----------|----------|
| `ad_user_data` | Sending user data to Google for advertising | Yes (EEA, required for Enhanced Conversions) |
| `ad_personalization` | Remarketing and personalized advertising | Yes (EEA) |

### Consent Mode Behavior

| Consent State | What Happens |
|---------------|-------------|
| `granted` | Full data collection, cookies set, user-level data sent |
| `denied` | Cookieless pings sent (no user identifiers), Google models conversions |
| Not set | Treated 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

```javascript
// 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
});
```

```javascript
// 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'
  });
}
```

```javascript
// 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

```sql
-- 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:**

| Field | Format | Required |
|-------|--------|----------|
| Email | SHA-256 hashed, lowercase, trimmed | At least one of: email, phone, or name+address |
| Phone | SHA-256 hashed, E.164 format (+1234567890) | Optional but recommended |
| First name | SHA-256 hashed, lowercase, trimmed | Optional |
| Last name | SHA-256 hashed, lowercase, trimmed | Optional |
| Street address | SHA-256 hashed, lowercase, trimmed | Optional |
| City | SHA-256 hashed, lowercase, trimmed | Optional |
| Region/State | SHA-256 hashed, lowercase, trimmed | Optional |
| Postal code | SHA-256 hashed, trimmed | Optional |
| Country | ISO 3166-1 alpha-2, unhashed | Optional |

### How Hashing Works

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

```javascript
// 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**

```javascript
// 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)**

```javascript
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

```javascript
// 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

```javascript
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

```javascript
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)

```javascript
// 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:

```bash
# 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 ->](https://github.com/cognyai/claude-code-marketing-skills/tree/main/skills/conversion-debug)

## Resources

- **GA4 Conversion Setup:** [support.google.com/analytics/answer/9267568](https://support.google.com/analytics/answer/9267568)
- **Google Ads Conversion Tracking:** [support.google.com/google-ads/answer/6095821](https://support.google.com/google-ads/answer/6095821)
- **Enhanced Conversions:** [support.google.com/google-ads/answer/9888656](https://support.google.com/google-ads/answer/9888656)
- **Consent Mode:** [developers.google.com/tag-platform/security/guides/consent](https://developers.google.com/tag-platform/security/guides/consent)
- **Meta Conversions API:** [developers.facebook.com/docs/marketing-api/conversions-api](https://developers.facebook.com/docs/marketing-api/conversions-api)
- **Meta Pixel Helper:** [developers.facebook.com/docs/meta-pixel/support/pixel-helper](https://developers.facebook.com/docs/meta-pixel/support/pixel-helper)
- **GTM Server-Side Tagging:** [developers.google.com/tag-platform/tag-manager/server-side](https://developers.google.com/tag-platform/tag-manager/server-side)
- **Claude Code Marketing Skills:** [github.com/cognyai/claude-code-marketing-skills](https://github.com/cognyai/claude-code-marketing-skills)

---

In this section:

- **Conversion Tracking Debugger Reference** (this page): https://cogny.com/docs/conversion-tracking-debugger
- Core Web Vitals Reference: https://cogny.com/docs/core-web-vitals
- GA4 BigQuery Export Schema Reference: https://cogny.com/docs/ga4-bigquery-export-schema
- GA4 Event Implementation Reference: https://cogny.com/docs/ga4-event-implementation
- Google Ads Query Language (GAQL) Reference: https://cogny.com/docs/gaql-reference
- Google Ads Scripts Reference: https://cogny.com/docs/google-ads-scripts
- GTM Event Tracking & Setup Reference: https://cogny.com/docs/gtm-event-tracking
- Meta Conversions API (CAPI) Setup Reference: https://cogny.com/docs/meta-conversions-api
- Schema.org Structured Data Reference: https://cogny.com/docs/structured-data-reference
- UTM Parameter Strategy & Builder Reference: https://cogny.com/docs/utm-strategy

Source page: https://cogny.com/docs/conversion-tracking-debugger  
All documentation: https://cogny.com/docs  
Cogny for agents: https://cogny.com/llms.txt
