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 oncheckout.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
Cookie Domain Configuration
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 |
When NOT to Use Shared Cookie Domain
Do not set a shared cookie domain when:
- Domains are entirely separate (e.g.,
example.comandpartner.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
});
});
// 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;
}
Link Decoration for External Navigation
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
Check Cookie Configuration
- Open DevTools > Application > Cookies
- Locate Heap cookies (
_hp2_id.*,_hp2_ses_props.*) - Verify the Domain column shows
.example.com(with leading dot) - Confirm cookies persist across subdomain navigation
Monitor Network Requests
- Open DevTools > Network
- Filter for
heapanalytics.com - Navigate across domains
- Verify the same
user_idappears in payload
Heap Live View
- Log into Heap dashboard
- Navigate to Live View
- Perform cross-domain navigation
- Confirm events appear under a single user session
- 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
GDPR and Cookie Consent
// 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:
- Navigate to Settings > Privacy
- Add suppression rules for URL patterns
- Define redaction for element selectors
- Test with Live View to verify
Best Practices
- Use shared cookie domains for subdomains: Configure
cookieDomain: '.example.com'when all subdomains belong to the same organization - Implement identity passing for third-party redirects: Store and restore Heap user ID when navigating through external domains
- Test thoroughly in production-like environments: Use actual domain names in staging, not localhost
- Monitor session continuity: Regularly check for session breaks in Heap analytics
- Document identity flows: Maintain clear documentation of where and how identity is passed
- Respect privacy boundaries: Use separate environments when consent requirements differ
- Validate before launch: Test complete user journeys across all domain boundaries
- Plan for browser restrictions: Account for Safari ITP and other privacy features