Skip to main content

Cross-Domain Tracking

Resolving session breaks and attribution issues when users navigate between domains.

When You Need Cross-Domain

Common Scenarios

| Scenario | Example | |----------|---------| | Separate checkout | shop.com → checkout.thirdparty.com | | Payment processor | site.com → stripe.com/checkout | | Multi-brand | brand1.com → brand2.com | | Subdomains (sometimes) | www.site.com → app.site.com | | Regional domains | site.com → site.co.uk |

What Happens Without It

User Journey Without Cross-Domain:
─────────────────────────────────────────────────────────
site.com                    checkout.external.com
Session 1                   Session 2 (NEW)
─────────────────────────────────────────────────────────
Source: google/cpc          Source: site.com/referral
User: abc123                User: xyz789 (NEW)
─────────────────────────────────────────────────────────

Result:
• Attribution broken (conversion credited to site.com referral)
• User appears as new visitor
• Session splits
• Self-referrals in reports

GA4 Configuration

Method 1: GA4 Admin Settings

  1. AdminData Streams → Select your stream
  2. Configure tag settingsConfigure your domains
  3. Add all domains that should share sessions:
example.com
checkout.example.com
shop.example.com
example.co.uk

Method 2: GTM Configuration

In your Google Tag (GA4 Configuration):

  1. Configuration SettingsDomains to link
  2. Add comma-separated list:
example.com,checkout.example.com,shop.example.com

What This Does

When enabled, GA4 appends the _gl parameter to cross-domain links:

https://checkout.example.com/cart?_gl=1*1abc2de*_ga*MTIzNDU...

This parameter contains:

  • Client ID
  • Session ID
  • Timestamp
  • Linker version

Subdomain Tracking

Same Domain, Different Subdomains

By default, cookies are set on the full hostname:

www.example.com sets cookie on: www.example.com
app.example.com CANNOT read cookie from www.example.com

Solution: Root Domain Cookies

In GA4 Configuration tag, set:

  • Fields to Setcookie_domain = .example.com
// Or via gtag:
gtag('config', 'G-XXXXXXXXXX', {
  'cookie_domain': '.example.com'
});

The leading dot makes the cookie accessible to all subdomains.

Troubleshooting

Issue: Sessions Still Splitting

Diagnosis:

  1. Navigate from domain A to domain B
  2. Check URL for _gl parameter
  3. Check cookie domain on both sites
// Check cookies
document.cookie.split(';').filter(c => c.includes('_ga'))

// Should see same _ga cookie value on both domains

Common Causes:

| Cause | Solution | |-------|----------| | _gl parameter missing | Verify link decoration enabled | | _gl stripped by redirect | Preserve query parameters | | Cookie domain mismatch | Set explicit cookie domain | | JavaScript redirect | Use proper link decoration |

Issue: Self-Referrals

Symptom: Your own domain appears as a traffic source

Fix in GA4:

  1. AdminData Streams → Your stream
  2. More tagging settingsList unwanted referrals
  3. Add your domains:
example.com
checkout.example.com

Issue: _gl Parameter Not Appearing

Check Link Decoration:

// In console, check if gtag is decorating links
gtag('get', 'G-XXXXXXXXXX', 'linker', function(linker) {
  console.log('Linker settings:', linker);
});

Manual Decoration (if needed):

// Force decorate specific links
document.querySelectorAll('a[href*="checkout.example.com"]').forEach(link => {
  link.addEventListener('click', function(e) {
    e.preventDefault();
    gtag('get', 'G-XXXXXXXXXX', 'client_id', function(clientId) {
      const url = new URL(link.href);
      url.searchParams.set('_gl', generateLinkerParam(clientId));
      window.location.href = url.toString();
    });
  });
});

Issue: Payment Processor Redirect

Many payment processors strip query parameters:

Solution 1: Embed in Path

// Some processors allow path-based data
// https://checkout.processor.com/session/YOUR_GA_DATA_HERE

Solution 2: Server-Side Tracking

Send purchase event from your server after payment callback:

// Server endpoint receiving payment callback
app.post('/payment-callback', async (req, res) => {
  const { orderId, amount, ga_client_id } = req.body;

  // Send to GA4 server-side
  await sendGA4Event(ga_client_id, 'purchase', {
    transaction_id: orderId,
    value: amount
  });
});

Solution 3: Post-Purchase Page

Redirect back to your domain for tracking:

Processor: payment.complete → your-site.com/thank-you?order=123
Your site: Tracks purchase on thank-you page

Testing Cross-Domain

Manual Testing

  1. Clear cookies on both domains
  2. Navigate from source domain to destination
  3. Check URL for _gl parameter
  4. Compare GA4 client_id on both domains
// Get client_id on each domain
gtag('get', 'G-XXXXXXXXXX', 'client_id', function(clientId) {
  console.log('Client ID:', clientId);
});

// Should be identical on both domains

GTM Preview Mode

  1. Start Preview on both containers (if separate)
  2. Complete cross-domain journey
  3. Check that same client_id appears on both

GA4 DebugView

  1. Enable debug mode
  2. Navigate across domains
  3. Verify same user_pseudo_id in both sessions

Complex Scenarios

Multiple Third-Party Domains

site.com → payment.processor1.com → shipping.processor2.com → site.com

Solution:

  • Use server-side tracking for third-party steps
  • Pass order ID through the flow
  • Reconstruct journey server-side

iFrames

Cross-domain iframes can't share cookies:

// Parent page: send client_id to iframe via postMessage
const clientId = getGA4ClientId();
iframe.contentWindow.postMessage({ clientId: clientId }, 'https://iframe-domain.com');

// iframe: receive and use
window.addEventListener('message', function(event) {
  if (event.data.clientId) {
    gtag('config', 'G-XXXXXXXXXX', {
      'client_id': event.data.clientId
    });
  }
});

Single Page Apps with Route Changes

// For SPAs, track virtual pageviews
window.addEventListener('routeChange', function(event) {
  gtag('event', 'page_view', {
    page_location: event.detail.url,
    page_title: event.detail.title
  });
});

Verification Checklist

## Cross-Domain Verification

### Configuration
- [ ] All domains listed in GA4 Admin
- [ ] Same Measurement ID on all domains
- [ ] Cookie domain set to root (if subdomains)
- [ ] Unwanted referrals configured

### Testing
- [ ] _gl parameter appears on cross-domain links
- [ ] _gl parameter survives redirects
- [ ] Client ID matches across domains
- [ ] Session ID matches across domains
- [ ] No self-referrals in reports

### Data Validation
- [ ] User counts accurate (no inflation)
- [ ] Session counts accurate (no splitting)
- [ ] Attribution correct (source preserved)
- [ ] Conversions track properly

BigQuery Validation

-- Check for session continuity across domains
WITH sessions AS (
  SELECT
    user_pseudo_id,
    (SELECT value.int_value FROM UNNEST(event_params)
     WHERE key = 'ga_session_id') as session_id,
    REGEXP_EXTRACT(
      (SELECT value.string_value FROM UNNEST(event_params)
       WHERE key = 'page_location'),
      r'^https?://([^/]+)'
    ) as domain
  FROM `project.analytics_XXXXXX.events_*`
  WHERE _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
)
SELECT
  user_pseudo_id,
  session_id,
  ARRAY_AGG(DISTINCT domain) as domains_in_session
FROM sessions
GROUP BY user_pseudo_id, session_id
HAVING COUNT(DISTINCT domain) > 1
LIMIT 100

Previous: GA4 Data Discrepancies Related: Debug & Validation