Cross-Domain Tracking | Blue Frog Docs

Cross-Domain Tracking

Complete guide to implementing cross-domain tracking in GA4 and GTM, with code examples, debugging techniques, and common pitfalls to avoid.

Cross-Domain Tracking

Cross-domain tracking enables you to track a single user session across multiple domains. Without it, a user visiting shop.example.com after www.example.com would appear as two separate users with two separate sessions.

Why Cross-Domain Tracking Matters

When a user moves between domains, their cookies don't travel with them. This causes:

  • Session fragmentation: One user journey becomes multiple sessions
  • Inflated user counts: Same user counted multiple times
  • Broken attribution: Original traffic source lost on domain change
  • Inaccurate conversion data: Conversions attributed to "referral" from your own domain

Common Multi-Domain Scenarios

Scenario Domains Involved
Main site + checkout example.comcheckout.example.com
Marketing + product marketing.example.comapp.example.com
Country-specific sites example.comexample.co.uk
Third-party checkout store.example.comshopify.example.com
Landing pages + main site promo.example.comwww.example.com

How Cross-Domain Tracking Works

Cross-domain tracking passes user identification data via URL parameters when navigating between domains. The receiving domain reads these parameters and "adopts" the existing user session instead of creating a new one.

User clicks link on domain-a.com
                ↓
Link URL decorated with _gl parameter
https://domain-b.com?_gl=1*abc123*_ga*MTIzNDU2Nzg5*...
                ↓
domain-b.com reads _gl parameter
                ↓
Same client_id used, session continues

The _gl parameter contains encoded client ID and session data that allows the receiving domain to identify the user.

GA4 Implementation

Basic Configuration

In your GA4 configuration, add all domains that should share sessions:

// gtag.js implementation
gtag('config', 'G-XXXXXXXXXX', {
  'linker': {
    'domains': ['example.com', 'checkout.example.com', 'app.example.com']
  }
});

This automatically decorates all links pointing to the listed domains with the _gl parameter.

Full Implementation Example

<!-- In <head> section of ALL domains -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-XXXXXXXXXX', {
    // List ALL domains that should share sessions
    'linker': {
      'domains': [
        'example.com',
        'www.example.com',
        'checkout.example.com',
        'app.example.com'
      ],
      // Accept incoming linker parameters
      'accept_incoming': true,
      // Decorate forms (for POST submissions)
      'decorate_forms': true
    }
  });
</script>

Important Settings

Setting Purpose Default
domains List of domains to link Required
accept_incoming Read incoming _gl parameters true
decorate_forms Add linker to form actions false
url_position Parameter position (query or fragment) query

Including Subdomains

For subdomains, you have two options:

// Option 1: List each subdomain explicitly
'linker': {
  'domains': ['example.com', 'shop.example.com', 'blog.example.com']
}

// Option 2: Use automatic subdomain matching (GA4 does this by default)
// Same root domain subdomains share cookies automatically
// Cross-domain is only needed for DIFFERENT root domains

Note: Subdomains of the same root domain (e.g., www.example.com and shop.example.com) typically share cookies automatically in GA4. Cross-domain tracking is primarily needed for completely different domains.

GTM Implementation

Step 1: Configure GA4 Configuration Tag

  1. Open your GA4 Configuration tag in GTM
  2. Scroll to "Fields to Set"
  3. Add these fields:
Field Name Value
linker (see configuration object below)

Or use the dedicated "Cross Domain Measurement" section in newer GTM versions.

Step 2: Cross-Domain Configuration

In the GA4 Configuration tag:

  1. Click "Configure tag settings"
  2. Find "Configure your domains"
  3. Add each domain (one per line):
    • example.com
    • checkout.example.com
    • app.example.com

Step 3: Custom JavaScript Variable (Advanced)

For dynamic domain configuration:

// GTM Custom JavaScript Variable: "cjs - Linker Domains"
function() {
  return {
    'domains': [
      'example.com',
      'checkout.example.com',
      'app.example.com'
    ],
    'accept_incoming': true,
    'decorate_forms': true
  };
}

Then reference {{cjs - Linker Domains}} in your GA4 Configuration tag's linker field.

Handling Form Submissions

Forms that POST to another domain need special handling since the URL isn't visible:

gtag('config', 'G-XXXXXXXXXX', {
  'linker': {
    'domains': ['example.com', 'checkout.example.com'],
    'decorate_forms': true  // Critical for forms
  }
});

Manual Form Decoration

For dynamically loaded forms or custom implementations:

// Manually decorate a form's action URL
document.querySelector('form.checkout-form').addEventListener('submit', function(e) {
  // gtag automatically decorates if decorate_forms is true
  // For manual control:
  var form = e.target;
  var action = form.getAttribute('action');

  // Use GA's linker to decorate the URL
  gtag('get', 'G-XXXXXXXXXX', 'client_id', function(clientId) {
    // Append client_id to form action
    var separator = action.includes('?') ? '&' : '?';
    form.setAttribute('action', action + separator + '_gl_cid=' + clientId);
  });
});

JavaScript Navigation

For JavaScript-triggered navigation (SPAs, custom links):

// Custom navigation function with linker support
function navigateToDomain(url) {
  // Let gtag decorate the URL first
  gtag('get', 'G-XXXXXXXXXX', 'linker_param', function(linkerParam) {
    if (linkerParam) {
      var separator = url.includes('?') ? '&' : '?';
      window.location.href = url + separator + linkerParam;
    } else {
      window.location.href = url;
    }
  });
}

// Usage
document.querySelector('.external-checkout').addEventListener('click', function(e) {
  e.preventDefault();
  navigateToDomain('https://checkout.example.com/cart');
});

Debugging Cross-Domain Tracking

  1. Open browser DevTools
  2. Right-click a cross-domain link
  3. Copy link address
  4. Check for _gl parameter:
https://checkout.example.com/cart?_gl=1*1abc2de*_ga*MTIzNDU2Nzg...

If the _gl parameter is missing, check your domain configuration.

Console Debugging

// Check if linker is configured
gtag('get', 'G-XXXXXXXXXX', 'linker_param', function(param) {
  console.log('Linker parameter:', param);
});

// Check client ID consistency
gtag('get', 'G-XXXXXXXXXX', 'client_id', function(clientId) {
  console.log('Client ID:', clientId);
});

// Monitor all link clicks
document.addEventListener('click', function(e) {
  if (e.target.tagName === 'A') {
    console.log('Link href:', e.target.href);
    console.log('Contains _gl:', e.target.href.includes('_gl='));
  }
}, true);

Verify Session Continuity

On both domains, check that client_id matches:

// Run this on domain-a.com before clicking link
gtag('get', 'G-XXXXXXXXXX', 'client_id', function(id) {
  console.log('Domain A client_id:', id);
});

// Run this on domain-b.com after arriving
gtag('get', 'G-XXXXXXXXXX', 'client_id', function(id) {
  console.log('Domain B client_id:', id);
});

// IDs should match if cross-domain is working

GA4 DebugView Verification

  1. Enable debug mode on both domains
  2. Navigate from domain A to domain B
  3. In GA4 DebugView, events should show:
    • Same client_id (user identifier)
    • Continuous session_id
    • No new session_start event on domain B

Common Problems and Solutions

Symptoms: _gl parameter missing from cross-domain links

Causes and fixes:

// 1. Domain not in list (most common)
// WRONG:
'domains': ['example.com']  // Missing checkout domain

// CORRECT:
'domains': ['example.com', 'checkout.example.com']

// 2. Domain format mismatch
// WRONG:
'domains': ['www.example.com']  // Won't match example.com

// CORRECT:
'domains': ['example.com', 'www.example.com']  // Include both

// 3. JavaScript links not triggering
// WRONG: Direct assignment
window.location = 'https://other-domain.com';

// CORRECT: Use gtag to get decorated URL
gtag('get', 'G-XXXXXXXXXX', 'linker_param', function(param) {
  window.location = 'https://other-domain.com?' + param;
});

Problem: Self-Referral Traffic

Symptoms: Your own domain appearing as referral source

Causes and fixes:

  1. Missing domain in referral exclusion list:

    • GA4 Admin → Data Streams → Configure tag settings
    • List internal domains under "Referral exclusion"
  2. Subdomain cookie scope issues:

    // Ensure cookie domain is set to root domain
    gtag('config', 'G-XXXXXXXXXX', {
      'cookie_domain': '.example.com'  // Note the leading dot
    });
    

Problem: Session Breaks on Domain Switch

Symptoms: New session starts on second domain, different client_id

Causes and fixes:

// 1. Check accept_incoming is enabled (default true)
gtag('config', 'G-XXXXXXXXXX', {
  'linker': {
    'domains': ['domain-a.com', 'domain-b.com'],
    'accept_incoming': true  // Must be true on receiving domain
  }
});

// 2. Verify _gl parameter isn't stripped
// Check server/CDN config isn't removing query parameters
// Check URL rewrite rules in .htaccess or nginx config

// 3. Check for consent mode blocking
// If consent denied, linker might not work
gtag('consent', 'update', {
  'analytics_storage': 'granted'  // Required for cross-domain
});

Problem: _gl Parameter Causes 404 or Errors

Symptoms: Pages break when _gl parameter is present

Fixes:

// Option 1: Use fragment instead of query parameter
gtag('config', 'G-XXXXXXXXXX', {
  'linker': {
    'domains': ['example.com'],
    'url_position': 'fragment'  // Uses # instead of ?
  }
});

// Option 2: Server-side - ignore the parameter
// In nginx:
// location / {
//   if ($args ~* "_gl=") {
//     rewrite ^(.*)$ $1? redirect;  // Strip query
//   }
// }

Problem: Iframe Cross-Domain

Symptoms: Parent and iframe sessions not linked

Iframes on different domains cannot be linked via standard cross-domain tracking due to security restrictions. Options:

// Option 1: Pass client_id via postMessage
// Parent page
gtag('get', 'G-XXXXXXXXXX', 'client_id', function(clientId) {
  document.querySelector('iframe').contentWindow.postMessage({
    type: 'ga_client_id',
    clientId: clientId
  }, 'https://iframe-domain.com');
});

// Iframe page
window.addEventListener('message', function(event) {
  if (event.origin === 'https://parent-domain.com' &&
      event.data.type === 'ga_client_id') {
    gtag('config', 'G-XXXXXXXXXX', {
      'client_id': event.data.clientId
    });
  }
});

// Option 2: Include client_id in iframe src
var iframe = document.querySelector('iframe');
gtag('get', 'G-XXXXXXXXXX', 'client_id', function(clientId) {
  iframe.src = 'https://iframe-domain.com/page?cid=' + clientId;
});

Testing Checklist

Before launching cross-domain tracking:

  • All domains listed in linker configuration
  • Configuration deployed to ALL domains
  • Links are decorated with _gl parameter
  • Forms are decorated (decorate_forms: true)
  • JavaScript navigation handles linker
  • Client_id matches across domains (test in console)
  • No new session_start on domain switch (DebugView)
  • No self-referrals in traffic reports
  • Referral exclusion list includes all domains
  • Consent mode doesn't block linker
  • Server doesn't strip _gl parameter
  • Old links/bookmarks with _gl work

Privacy Considerations

Cross-domain tracking raises privacy implications:

  1. User Disclosure: Inform users that tracking spans multiple sites
  2. Consent Requirements: May require explicit consent under GDPR/CCPA
  3. Privacy Policy: Update to reflect cross-domain data sharing
  4. Data Minimization: Only implement if genuinely needed for business purposes
// Respect user consent preferences
if (userHasConsented) {
  gtag('config', 'G-XXXXXXXXXX', {
    'linker': {
      'domains': ['example.com', 'checkout.example.com']
    }
  });
} else {
  gtag('config', 'G-XXXXXXXXXX', {
    // No linker - each domain tracked separately
  });
}
// SYS.FOOTER