Heap Cross-Domain Tracking | OpsBlu Docs

Heap Cross-Domain Tracking

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

Overview

Cross-domain tracking in Heap ensures user identities and sessions persist when visitors navigate between multiple domains or subdomains you own. Heap uses first-party cookies and identity mapping to maintain continuity across domain boundaries, enabling accurate attribution and user journey analysis.

Unlike traditional analytics platforms, Heap's autocapture technology automatically records interactions across domains once properly configured. However, identity preservation requires careful cookie domain configuration and, in some cases, manual identity passing.

When Cross-Domain Tracking Is Required

Multiple Subdomains

  • Main application: app.example.com
  • Marketing site: www.example.com
  • Documentation: docs.example.com
  • Blog: blog.example.com

Separate Checkout or Payment Domains

  • Main site on www.example.com, checkout on checkout.example.com
  • Third-party payment processors (Stripe, PayPal, Square) that redirect users
  • Hosted shopping cart solutions on separate domains

Regional or Brand-Specific Domains

  • example.com, example.co.uk, example.de
  • Multiple brand sites under the same organization
  • Microsites for campaigns or product launches

App-to-Web and Web-to-App Flows

  • Mobile app deep links opening web views
  • Email links directing to web content
  • In-app browser sessions

Understanding Heap Cookies

Heap stores user identity in first-party cookies:

Cookie Name Purpose Typical Value
_hp2_id.{environment_id} Heap user identity Session-level user identifier
_hp2_ses_props.{environment_id} Session properties Session metadata and properties
_hp2_props.{environment_id} User properties Persistent user properties

By default, these cookies are scoped to the specific subdomain where Heap is loaded.

Shared Parent Domain Configuration

To share identity across subdomains, configure the cookie domain to a common parent:

window.heap=window.heap||[],heap.load=function(e,t){
  window.heap.appid=e,window.heap.config=t=t||{};
  // ... standard heap snippet ...

  // Set cookie domain to share across subdomains
  heap.config = {
    cookieDomain: '.example.com'  // Note the leading dot
  };
};
heap.load("YOUR-ENVIRONMENT-ID");

Configuration Options

Configuration Value Behavior
cookieDomain: '.example.com' Leading dot + parent domain Shares cookies across all subdomains
cookieDomain: 'example.com' No leading dot More restrictive; check browser behavior
cookieDomain: undefined Default Scoped to current subdomain only

Do not set a shared cookie domain when:

  • Domains are entirely separate (e.g., example.com and partner.com)
  • Privacy requirements demand isolated tracking
  • Different business units require separate analytics
  • GDPR/CCPA consent differs across subdomains

Cross-Domain Identity Passing

Third-Party Domain Flows

When users navigate to domains you don't control (payment processors, authentication providers), cookies won't persist. Use identity token passing:

Step 1: Capture Heap User ID Before Redirect

// Before redirecting to payment processor
function initiateCheckout() {
  const heapUserId = heap.userId;

  // Store in session or pass via URL
  sessionStorage.setItem('heap_user_id', heapUserId);

  // Redirect to payment processor with return URL
  const returnUrl = `https://example.com/confirmation?heap_uid=${heapUserId}`;
  window.location.href = `https://payment-provider.com/checkout?return=${encodeURIComponent(returnUrl)}`;
}

Step 2: Restore Identity on Return

// On confirmation page
const urlParams = new URLSearchParams(window.location.search);
const heapUserId = urlParams.get('heap_uid') || sessionStorage.getItem('heap_user_id');

if (heapUserId) {
  // Associate current session with previous user ID
  heap.identify(heapUserId);

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

Server-Side Identity Coordination

For flows where client-side tracking is unreliable, coordinate via server:

// Node.js/Express example
app.post('/api/checkout/initiate', async (req, res) => {
  const heapUserId = req.body.heap_user_id;
  const orderId = generateOrderId();

  // Store association in database
  await db.orders.create({
    order_id: orderId,
    heap_user_id: heapUserId,
    created_at: new Date()
  });

  // Redirect to payment processor
  res.json({
    redirect_url: `https://payment-provider.com/pay?order=${orderId}`
  });
});

// On return from payment processor
app.get('/confirmation', async (req, res) => {
  const orderId = req.query.order_id;
  const order = await db.orders.findOne({ order_id: orderId });

  // Pass Heap user ID to frontend
  res.render('confirmation', {
    heap_user_id: order.heap_user_id,
    order_details: order
  });
});

Frontend confirmation page:

// Restore identity from server-provided data
const heapUserId = document.querySelector('[data-heap-user-id]').dataset.heapUserId;
if (heapUserId) {
  heap.identify(heapUserId);
}

// Track conversion
heap.track('Purchase Completed', {
  order_id: '{{ order.order_id }}',
  value: {{ order.total }},
  currency: 'USD'
});

OAuth and Authentication Flows

Problem Scenario

OAuth redirects often clear cookies or navigate through domains where Heap isn't installed:

example.com → auth.provider.com → example.com/callback

Solution: Identity Preservation

Before OAuth Redirect

// Store Heap identity before redirecting
function initiateOAuth() {
  const heapUserId = heap.userId;

  // Store in sessionStorage
  sessionStorage.setItem('heap_pre_auth_id', heapUserId);

  // Or pass via state parameter (if OAuth provider supports)
  const state = btoa(JSON.stringify({
    heap_user_id: heapUserId,
    return_path: window.location.pathname
  }));

  window.location.href = `https://auth.provider.com/oauth?state=${state}&client_id=...`;
}

After OAuth Callback

// On /callback page
function handleOAuthCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const state = urlParams.get('state');

  let heapUserId;

  // Try to restore from state parameter
  if (state) {
    try {
      const stateData = JSON.parse(atob(state));
      heapUserId = stateData.heap_user_id;
    } catch (e) {
      console.warn('Could not parse OAuth state', e);
    }
  }

  // Fallback to sessionStorage
  heapUserId = heapUserId || sessionStorage.getItem('heap_pre_auth_id');

  if (heapUserId) {
    heap.identify(heapUserId);
    sessionStorage.removeItem('heap_pre_auth_id');
  }

  // Now identify with actual user ID from your system
  const authenticatedUserId = getUserIdFromSession();
  if (authenticatedUserId) {
    heap.identify(authenticatedUserId);
  }
}

// Call on page load
handleOAuthCallback();

iFrame and Widget Integration

Parent-Child Communication

When Heap-tracked content is embedded in iframes across domains:

Parent Page (example.com)

// Get Heap user ID in parent
const heapUserId = heap.userId;

// Send to iframe
const iframe = document.getElementById('widget-iframe');
iframe.contentWindow.postMessage({
  type: 'heap_identity',
  userId: heapUserId
}, 'https://widget.example.com');

Child iFrame (widget.example.com)

// Initialize Heap in iframe
heap.load("WIDGET-ENVIRONMENT-ID");

// Listen for identity from parent
window.addEventListener('message', (event) => {
  // Validate origin
  if (event.origin !== 'https://example.com') {
    return;
  }

  if (event.data.type === 'heap_identity' && event.data.userId) {
    heap.identify(event.data.userId);
  }
});

// Request identity from parent
window.parent.postMessage({ type: 'heap_identity_request' }, 'https://example.com');

Same-Origin iFrames

For same-origin iframes, cookies are automatically shared:

// No special configuration needed
// Heap cookies are accessible in iframe

// Optionally, ensure same environment ID
heap.load("YOUR-ENVIRONMENT-ID");

Decision Matrix: iFrame Tracking

Scenario Recommendation Implementation
Same subdomain Use shared cookies Configure cookieDomain: '.example.com'
Cross-origin, same organization Pass identity via postMessage Implement parent-child messaging
Third-party widget Separate Heap environment Use different environment ID
Minimal iframe usage Skip iframe tracking Track only parent page

Single Page Application (SPA) Considerations

Client-Side Routing

Heap autocaptures pageviews in SPAs, but cross-domain navigation requires attention:

// React Router example
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function HeapPageTracker() {
  const location = useLocation();

  useEffect(() => {
    // Heap automatically captures pageviews
    // But you can manually trigger if needed
    if (window.heap) {
      heap.track('Virtual Pageview', {
        path: location.pathname,
        search: location.search,
        title: document.title
      });
    }
  }, [location]);

  return null;
}

If SPA links navigate to external domains:

// Add Heap user ID to external links
function decorateExternalLinks() {
  const heapUserId = heap.userId;

  document.querySelectorAll('a[data-cross-domain]').forEach(link => {
    const url = new URL(link.href);
    url.searchParams.set('heap_uid', heapUserId);
    link.href = url.toString();
  });
}

// Run after SPA navigation
router.afterEach(() => {
  decorateExternalLinks();
});

Validation and Testing

Browser DevTools Inspection

  1. Open DevTools > Application > Cookies
  2. Locate Heap cookies (_hp2_id.*, _hp2_ses_props.*)
  3. Verify the Domain column shows .example.com (with leading dot)
  4. Confirm cookies persist across subdomain navigation

Monitor Network Requests

  1. Open DevTools > Network
  2. Filter for heapanalytics.com
  3. Navigate across domains
  4. Verify the same user_id appears in payload

Heap Live View

  1. Log into Heap dashboard
  2. Navigate to Live View
  3. Perform cross-domain navigation
  4. Confirm events appear under a single user session
  5. Verify no new sessions created at domain boundaries

Identity Merge Verification

Test identity preservation across domains:

// On domain A (www.example.com)
console.log('Domain A User ID:', heap.userId);
// Output: 1234567890

// Navigate to domain B (app.example.com)
console.log('Domain B User ID:', heap.userId);
// Output should match: 1234567890

// Check if identity persisted
if (previousUserId === heap.userId) {
  console.log('✓ Cross-domain identity preserved');
} else {
  console.error('✗ Identity lost across domains');
}

Automated Testing

// Cypress example
describe('Cross-domain tracking', () => {
  it('preserves user identity across subdomains', () => {
    cy.visit('https://www.example.com');

    // Get initial Heap user ID
    cy.window().then((win) => {
      cy.wrap(win.heap.userId).as('initialUserId');
    });

    // Navigate to subdomain
    cy.visit('https://app.example.com');

    // Verify same user ID
    cy.window().then((win) => {
      cy.get('@initialUserId').should('equal', win.heap.userId);
    });
  });
});

Troubleshooting

Symptom Likely Cause Solution
New session starts at domain boundary Cookie not shared Configure cookieDomain: '.example.com'
Different user ID after subdomain navigation Each subdomain has isolated cookies Set shared cookie domain in Heap snippet
Identity lost after payment redirect Third-party domain clears cookies Implement identity token passing via URL or server
OAuth return creates new user Identity not preserved during redirect Store Heap user ID before OAuth, restore after
iFrame events not attributed Cross-origin cookie blocking Use postMessage to pass identity to iframe
Safari/ITP breaking cross-domain Intelligent Tracking Prevention Use first-party subdomains, avoid third-party domains
GDPR consent differs across domains Separate consent requirements Use different Heap environments per domain
Identity merges incorrectly Multiple heap.identify() calls conflict Ensure consistent user ID format across domains

Privacy and Compliance Considerations

// Wait for consent before loading Heap
function loadHeapWithConsent() {
  // Check consent state
  const consent = getConsentState();

  if (consent.analytics === 'granted') {
    heap.load("YOUR-ENVIRONMENT-ID", {
      cookieDomain: '.example.com'
    });
  }
}

// Update when consent changes
window.addEventListener('consent_update', (event) => {
  if (event.detail.analytics === 'granted') {
    loadHeapWithConsent();
  } else {
    // Remove Heap cookies
    document.cookie.split(";").forEach((c) => {
      if (c.trim().startsWith('_hp2_')) {
        document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
      }
    });
  }
});

PII Redaction Across Domains

Ensure consistent redaction rules across all domains:

heap.load("YOUR-ENVIRONMENT-ID", {
  cookieDomain: '.example.com',
  // Redact sensitive fields globally
  redactText: true,
  redactAttributes: ['data-email', 'data-ssn', 'data-phone']
});

Configure suppression rules in Heap dashboard:

  1. Navigate to Settings > Privacy
  2. Add suppression rules for URL patterns
  3. Define redaction for element selectors
  4. Test with Live View to verify

Best Practices

  1. Use shared cookie domains for subdomains: Configure cookieDomain: '.example.com' when all subdomains belong to the same organization
  2. Implement identity passing for third-party redirects: Store and restore Heap user ID when navigating through external domains
  3. Test thoroughly in production-like environments: Use actual domain names in staging, not localhost
  4. Monitor session continuity: Regularly check for session breaks in Heap analytics
  5. Document identity flows: Maintain clear documentation of where and how identity is passed
  6. Respect privacy boundaries: Use separate environments when consent requirements differ
  7. Validate before launch: Test complete user journeys across all domain boundaries
  8. Plan for browser restrictions: Account for Safari ITP and other privacy features