Segment Cross-Domain Tracking | OpsBlu Docs

Segment Cross-Domain Tracking

How to track users across multiple domains and subdomains with Segment. Covers cross-domain configuration, cookie handling, session stitching, and.

Overview

Cross-domain tracking in Segment ensures that user identity remains consistent as visitors navigate between multiple domains you control. Unlike platform-specific solutions (GA4's _gl parameter or Adobe's Visitor API), Segment's cross-domain tracking revolves around persisting the anonymousId across domain boundaries and coordinating identity resolution across both client-side and server-side sources.

When properly configured, cross-domain tracking prevents session fragmentation, maintains attribution accuracy, and ensures that downstream destinations receive unified user profiles regardless of domain transitions.

When Cross-Domain Tracking Is Required

Common Multi-Domain Scenarios

  • Separate checkout domains: Main site on www.example.com, checkout on secure-checkout.example.com
  • Third-party payment processors: Redirects to Stripe, PayPal, or other gateways that return users to confirmation pages
  • Multi-brand properties: brand-a.example.com, brand-b.example.com sharing analytics
  • Hosted authentication: OAuth flows or SSO providers on separate domains (e.g., auth.example.com)
  • Marketing microsites: Campaign landing pages on promo.example.com directing to main site
  • Partner integrations: Co-branded experiences spanning your domain and partner domains
  • Regional domains: example.com, example.co.uk, example.de requiring unified tracking
  • App-to-web handoffs: Mobile app deep links opening web content on different domains

How Segment Handles Cross-Domain Identity

The anonymousId Lifecycle

Segment assigns each user an anonymousId (typically a UUID v4) stored in:

  1. First-party cookies: ajs_anonymous_id in the browser
  2. Local storage: ajs_anonymous_id as a fallback
  3. Session storage: Temporary persistence for session-scoped tracking

When a user navigates to a new domain:

  • If the cookie domain doesn't match, the anonymousId is not accessible
  • A new anonymousId is generated, fragmenting the user's identity
  • Downstream destinations receive events from two "different" users

Cross-Domain Identity Strategies

Segment offers three primary approaches:

Strategy Use Case Implementation Complexity
Query Parameter Propagation Simple multi-domain flows Low
Shared Cookie Domain Subdomains only Low
Server-Side Identity Resolution Complex flows, high security High

Implementation Methods

Method 1: Query Parameter Propagation

Pass the anonymousId between domains via URL parameters.

Client-Side Implementation

// Domain A: Capture anonymousId before redirect
function redirectToDomainB(targetUrl) {
  const anonymousId = analytics.user().anonymousId();
  const separator = targetUrl.includes('?') ? '&' : '?';
  const urlWithId = `${targetUrl}${separator}ajs_aid=${anonymousId}`;

  window.location.href = urlWithId;
}

// Example usage
redirectToDomainB('https://checkout.example.com/cart');
// Results in: https://checkout.example.com/cart?ajs_aid=abc-123-def-456

Domain B: Restore anonymousId

// Domain B: Read and restore anonymousId from URL
(function() {
  const urlParams = new URLSearchParams(window.location.search);
  const crossDomainId = urlParams.get('ajs_aid');

  if (crossDomainId) {
    // Set anonymousId before analytics.js initializes
    analytics.load('YOUR_WRITE_KEY', {
      integrations: {
        'Segment.io': {
          apiHost: 'api.segment.io/v1',
          protocol: 'https'
        }
      },
      anonymousId: crossDomainId
    });
  } else {
    // Normal initialization
    analytics.load('YOUR_WRITE_KEY');
  }
})();

For static HTML links between domains:

// Automatically decorate all cross-domain links
analytics.ready(function() {
  const anonymousId = analytics.user().anonymousId();
  const crossDomains = ['checkout.example.com', 'auth.example.com'];

  document.querySelectorAll('a').forEach(function(link) {
    const hostname = new URL(link.href, window.location.origin).hostname;

    if (crossDomains.some(domain => hostname.includes(domain))) {
      const separator = link.href.includes('?') ? '&' : '?';
      link.href = `${link.href}${separator}ajs_aid=${anonymousId}`;
    }
  });
});

For subdomains under the same parent domain, configure cookie sharing.

Analytics.js Configuration

analytics.load('YOUR_WRITE_KEY', {
  integrations: {
    'Segment.io': {
      apiHost: 'api.segment.io/v1',
      protocol: 'https'
    }
  },
  cookie: {
    domain: '.example.com',  // Note the leading dot
    path: '/',
    maxage: 31536000,  // 365 days
    sameSite: 'Lax'
  }
});
Configuration Accessible From
domain: '.example.com' www.example.com, checkout.example.com, auth.example.com
domain: 'example.com' Only example.com (not subdomains)
domain: undefined Current domain only

Method 3: Server-Side Identity Resolution

For high-security flows or when client-side storage is unreliable.

Flow Architecture

1. User starts on Domain A (client-side tracking active)
2. Server captures anonymousId before redirect
3. Redirect to Domain B includes server-generated token
4. Domain B server exchanges token for anonymousId
5. Server-side track call with correct anonymousId
6. Client-side analytics initializes with restored ID

Implementation Example

// Domain A: Server endpoint to capture anonymousId
app.post('/api/prepare-checkout', (req, res) => {
  const anonymousId = req.body.anonymousId;
  const sessionToken = crypto.randomBytes(32).toString('hex');

  // Store mapping in Redis with TTL
  redis.setex(`cross_domain:${sessionToken}`, 300, anonymousId);

  res.json({
    checkoutUrl: `https://checkout.example.com?token=${sessionToken}`
  });
});

// Domain B: Server endpoint to restore identity
app.get('/checkout', async (req, res) => {
  const token = req.query.token;
  const anonymousId = await redis.get(`cross_domain:${token}`);

  if (anonymousId) {
    // Track server-side with correct identity
    analytics.track({
      anonymousId: anonymousId,
      event: 'Checkout Started',
      properties: {
        source: 'cross_domain'
      }
    });

    // Pass anonymousId to client
    res.render('checkout', { anonymousId });
  } else {
    // Fallback: Generate new anonymousId
    res.render('checkout', { anonymousId: null });
  }
});
<!-- Domain B: Checkout page template -->
<script>
  <% if (anonymousId) { %>
    analytics.load('YOUR_WRITE_KEY', {
      anonymousId: '<%= anonymousId %>'
    });
  <% } else { %>
    analytics.load('YOUR_WRITE_KEY');
  <% } %>
</script>

Third-Party Integration Flows

Payment Processor Return URLs

When redirecting to external payment providers:

// Before redirect to Stripe/PayPal
function initiatePayment(provider) {
  const anonymousId = analytics.user().anonymousId();
  const userId = analytics.user().id();

  // Construct return URL with identity parameters
  const returnUrl = new URL('https://example.com/confirmation');
  returnUrl.searchParams.set('ajs_aid', anonymousId);
  if (userId) {
    returnUrl.searchParams.set('ajs_uid', userId);
  }

  // Pass to payment provider
  if (provider === 'stripe') {
    stripe.redirectToCheckout({
      sessionId: 'sess_xxx',
      successUrl: returnUrl.toString()
    });
  }
}

// On return from payment processor
function handlePaymentReturn() {
  const params = new URLSearchParams(window.location.search);
  const anonymousId = params.get('ajs_aid');
  const userId = params.get('ajs_uid');

  if (anonymousId) {
    // Track purchase with preserved identity
    analytics.track('Order Completed', {
      order_id: params.get('order_id'),
      total: parseFloat(params.get('total'))
    }, {
      anonymousId: anonymousId,
      userId: userId || undefined
    });
  }
}

OAuth and SSO Flows

For authentication flows on separate domains:

// Pre-authentication: Store anonymousId
function initiateLogin() {
  const anonymousId = analytics.user().anonymousId();

  // Store in session storage (survives redirect)
  sessionStorage.setItem('segment_anon_id', anonymousId);

  // Redirect to auth provider
  window.location.href = 'https://auth.example.com/login?return_to=' +
    encodeURIComponent(window.location.href);
}

// Post-authentication: Restore and identify
function handleAuthCallback() {
  const anonymousId = sessionStorage.getItem('segment_anon_id');
  const userProfile = getUserProfileFromToken();

  // Initialize with preserved anonymousId
  analytics.load('YOUR_WRITE_KEY', {
    anonymousId: anonymousId
  });

  // Identify user (links anonymousId to userId)
  analytics.identify(userProfile.id, {
    email: userProfile.email,
    name: userProfile.name
  });

  // Clean up
  sessionStorage.removeItem('segment_anon_id');
}

Device-Mode vs Cloud-Mode Considerations

Device-Mode Limitations

Device-mode destinations load third-party scripts directly in the browser:

  • Subject to ad blockers and privacy extensions
  • May not receive events if blocked before cross-domain transition
  • Cookie access restricted by browser privacy features (ITP, ETP)

Cross-Domain Strategy Matrix

Scenario Recommended Approach
Device-mode only Query parameter + early restoration
Cloud-mode only Server-side identity resolution
Hybrid (both modes) Dual tracking with fallback logic
High ad-block traffic Prioritize cloud-mode destinations

Fallback Configuration

// Enable cloud-mode fallback for critical destinations
analytics.ready(function() {
  const integrations = analytics.Integrations;

  // Check if Google Analytics device-mode loaded
  if (!integrations['Google Analytics']) {
    console.warn('GA device-mode blocked, cloud-mode active');
  }
});

Edge Cases and Solutions

Safari ITP and Privacy Browsers

Problem: Safari Intelligent Tracking Prevention strips first-party cookies after 7 days for cross-site navigation.

Solution:

// Detect Safari ITP and use localStorage as primary
function getStoragePreference() {
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  const isITP = isSafari && !document.cookie.includes('ajs_anonymous_id');

  return isITP ? 'localStorage' : 'cookie';
}

analytics.load('YOUR_WRITE_KEY', {
  storage: {
    preference: getStoragePreference()
  }
});

Session Storage Limitations

Problem: Session storage doesn't persist across tabs or windows.

Solution: Use a hybrid approach with localStorage backup:

function persistAnonymousId() {
  const anonymousId = analytics.user().anonymousId();

  // Primary: Cookie
  // Fallback: LocalStorage
  // Session: SessionStorage for current tab
  localStorage.setItem('ajs_anonymous_id_backup', anonymousId);
  sessionStorage.setItem('ajs_anonymous_id_session', anonymousId);
}

analytics.ready(persistAnonymousId);

Mobile App to Web Transitions

Problem: Deep links from mobile apps to web don't share cookie space.

Solution: Pass anonymousId as deep link parameter:

// Mobile app: Construct deep link
const deepLink = `https://example.com/product?id=123&ajs_aid=${userAnonymousId}`;
openBrowser(deepLink);

// Web: Restore from deep link
const params = new URLSearchParams(window.location.search);
const appAnonymousId = params.get('ajs_aid');

if (appAnonymousId) {
  // Use app's anonymousId for web session
  analytics.load('YOUR_WRITE_KEY', {
    anonymousId: appAnonymousId
  });

  // Track transition
  analytics.track('App to Web Transition', {
    product_id: params.get('id'),
    source: 'mobile_app'
  });
}

Validation and Testing

Pre-Implementation Checklist

Check Validation Method
Identify all domains in user journey Flow mapping exercise
Determine if subdomains allow shared cookies Test cookie with .domain.com
List third-party redirects (payment, auth) Review checkout and login flows
Assess device-mode blocking rates Check analytics.js load rate
Document identity requirements Define acceptable identity loss rate

Testing Cross-Domain Flows

1. Segment Debugger Validation

// Enable verbose debugging
analytics.debug(true);

// Monitor anonymousId transitions
analytics.ready(function() {
  const originalAnonId = analytics.user().anonymousId();
  console.log('Starting anonymousId:', originalAnonId);

  // Store for comparison after navigation
  sessionStorage.setItem('test_anon_id', originalAnonId);
});

// On destination domain
analytics.ready(function() {
  const currentAnonId = analytics.user().anonymousId();
  const originalAnonId = sessionStorage.getItem('test_anon_id');

  if (currentAnonId === originalAnonId) {
    console.log('✓ Cross-domain identity preserved');
  } else {
    console.error('✗ Identity fragmentation detected');
    console.log('Original:', originalAnonId);
    console.log('Current:', currentAnonId);
  }
});

2. Live Debugger Inspection

  1. Open Segment source debugger: app.segment.com/[workspace]/sources/[source]/debugger
  2. Start user flow on Domain A
  3. Note the anonymousId in the event payload
  4. Navigate to Domain B
  5. Verify subsequent events use the same anonymousId

3. Network Tab Verification

1. Open DevTools > Network tab
2. Filter: segment.com/v1
3. Click a cross-domain link
4. Inspect request payload on destination domain:
   {
     "anonymousId": "abc-123-def-456",  // Should match source domain
     "event": "Page Viewed",
     ...
   }

4. Destination Validation

For each connected destination, verify unified profiles:

Google Analytics 4:

1. GA4 > Reports > Real-time
2. Navigate cross-domain flow
3. Verify single user appears in real-time report
4. Check client_id consistency in DebugView

Mixpanel:

1. Mixpanel > Users > Live View
2. Filter to test user's anonymousId or email
3. Verify events from both domains appear in single profile
4. Check "User Activity" shows continuous journey

Amplitude:

-- BigQuery/SQL export validation
SELECT
  user_id,
  amplitude_id,
  event_type,
  server_upload_time
FROM events
WHERE amplitude_id = 'test-anonymous-id'
ORDER BY server_upload_time;

Automated Testing

// Cypress test for cross-domain identity
describe('Cross-Domain Tracking', () => {
  it('preserves anonymousId across domains', () => {
    let originalAnonId;

    // Domain A
    cy.visit('https://www.example.com');
    cy.window().then(win => {
      originalAnonId = win.analytics.user().anonymousId();
    });

    // Navigate to Domain B
    cy.get('a[href*="checkout.example.com"]').click();

    // Domain B
    cy.origin('https://checkout.example.com', { args: { originalAnonId } }, ({ originalAnonId }) => {
      cy.window().should(win => {
        const currentAnonId = win.analytics.user().anonymousId();
        expect(currentAnonId).to.equal(originalAnonId);
      });
    });
  });
});

Troubleshooting

Symptom Likely Cause Solution
Different anonymousId on destination domain Query parameter not passed or not restored Verify URL decoration and restoration logic
anonymousId present but events still fragmented Destinations receiving events before ID restoration Initialize analytics.js with anonymousId before .load()
Cross-domain links not decorated Link decoration runs before analytics.ready Move decoration logic inside analytics.ready() callback
Shared cookie domain not working Cookie domain doesn't match subdomain structure Use .example.com with leading dot for subdomains
Safari users losing identity after 7 days ITP cookie expiration Implement server-side identity resolution or use localStorage
Payment processor return loses session Return URL missing identity parameters Include ajs_aid in success_url and cancel_url
OAuth callback creates new anonymousId Pre-auth anonymousId not persisted Store in sessionStorage or URL state parameter before redirect
Server-side events not attributed to web session Server using different anonymousId Ensure server receives and uses client's anonymousId
Destination shows duplicate users Cloud-mode and device-mode sending different IDs Disable device-mode or ensure consistent ID across both modes
Mobile app to web transition fragments Deep link doesn't include anonymousId Pass anonymousId in deep link URL parameters

Monitoring and Alerting

Key Metrics to Track

// Track cross-domain transition rate
analytics.track('Cross-Domain Navigation', {
  from_domain: document.referrer ? new URL(document.referrer).hostname : 'direct',
  to_domain: window.location.hostname,
  anonymousId_source: getAnonymousIdSource() // 'url', 'cookie', 'localStorage', 'generated'
});

function getAnonymousIdSource() {
  const urlParams = new URLSearchParams(window.location.search);
  if (urlParams.get('ajs_aid')) return 'url';
  if (document.cookie.includes('ajs_anonymous_id')) return 'cookie';
  if (localStorage.getItem('ajs_anonymous_id')) return 'localStorage';
  return 'generated';
}

Alert Thresholds

Metric Threshold Alert Action
anonymousId_source: 'generated' rate >10% of cross-domain traffic Investigate identity propagation failure
Destination profile duplication >5% week-over-week increase Check device-mode vs cloud-mode sync
Cross-domain conversion drop >15% week-over-week decrease Verify payment/checkout flows

Best Practices

  1. Early Restoration: Initialize analytics with anonymousId before calling .load() to prevent event loss
  2. URL Parameter Cleanup: Remove ajs_aid from URL after restoration to avoid sharing or bookmarking
  3. Consent Alignment: Ensure consent state propagates cross-domain to maintain compliance
  4. Fallback Strategy: Always have cloud-mode destinations as fallback for blocked device-mode tracking
  5. Testing Coverage: Include cross-domain flows in regression test suites
  6. Documentation: Maintain a domain map with identity strategy for each transition
  7. Privacy Compliance: Hash or tokenize identity parameters if passing through untrusted third parties
  8. Expiration Handling: Set appropriate TTLs for server-side identity tokens (5-10 minutes max)
  9. Monitoring: Track identity source distribution and alert on anomalies
  10. Destination Alignment: Verify each destination supports cross-domain identity resolution