Overview
Cross-domain tracking in Mixpanel ensures user identity and session continuity when visitors navigate between multiple domains, subdomains, or applications you own. Unlike Google Analytics' linker parameter approach, Mixpanel relies on preserving the distinct_id and maintaining consistent device identification across domain boundaries through cookies, local storage, or explicit identifier passing.
Proper cross-domain tracking prevents user fragmentation, maintains accurate funnel analysis, and ensures attribution integrity when conversion paths span multiple properties.
When Cross-Domain Tracking Is Required
Multi-Domain Scenarios
- Separate checkout or payment domains: Main site on
www.example.com, checkout onsecure.example.comorcheckout.example.com - Third-party payment processors: Redirects to Stripe, PayPal, or other gateways that return users to confirmation pages
- Regional or brand domains:
example.com,example.co.uk,brand.example.com - Marketing microsites: Campaign-specific landing pages on different domains that funnel to main conversion flows
- Partner integrations: Co-branded experiences spanning partner domains
- Mobile app to web transitions: Deep links from native apps opening web views on different domains
When Cross-Domain Tracking May Not Be Needed
- Subdomains with shared root domain: If properly configured, cookies can span
www.example.comandblog.example.com - Same-domain SPAs: Single-page applications that don't cross domain boundaries
- Isolated properties: Completely separate products with independent user bases and analytics needs
Mixpanel Identity Management
distinct_id Fundamentals
Mixpanel tracks users through the distinct_id property:
- Anonymous tracking: On first visit, Mixpanel generates a random
distinct_id(device_id) - Identified tracking: After login/signup, you set
distinct_idto a known user identifier - Identity merge: The
aliasoridentifymethod links anonymous and identified IDs
Identity Persistence Mechanisms
Mixpanel stores distinct_id in multiple locations:
| Storage Method | Scope | Persistence | Cross-Domain |
|---|---|---|---|
Cookie (mp_<project_token>_mixpanel) |
Subdomain or root domain | Until expiration | Only if same root domain |
| localStorage | Specific origin | Persistent | No |
| Session storage | Specific origin | Session only | No |
Cross-Domain Configuration
Subdomain Tracking Setup
For tracking across subdomains of the same root domain:
// Initialize Mixpanel with cross-subdomain tracking
mixpanel.init('YOUR_PROJECT_TOKEN', {
cross_subdomain_cookie: true,
cookie_domain: '.example.com' // Note the leading dot
});
This configuration:
- Sets cookies at the root domain level (
.example.com) - Makes cookies accessible from
www.example.com,blog.example.com,checkout.example.com - Maintains
distinct_idautomatically across subdomain navigation
Secure Cookie Configuration
For HTTPS-only environments:
mixpanel.init('YOUR_PROJECT_TOKEN', {
cross_subdomain_cookie: true,
cookie_domain: '.example.com',
secure_cookie: true, // Requires HTTPS
cookie_name: 'mp_example_user' // Custom cookie name
});
Cross-Subdomain Cookie Options
| Option | Type | Description |
|---|---|---|
cross_subdomain_cookie |
boolean | Enable cross-subdomain tracking (default: false) |
cookie_domain |
string | Root domain for cookie (e.g., '.example.com') |
secure_cookie |
boolean | Require HTTPS for cookie (default: false) |
cookie_name |
string | Custom cookie name (default: 'mp_<token>_mixpanel') |
cookie_expiration |
number | Days until cookie expires (default: 365) |
True Cross-Domain Tracking (Different Root Domains)
Manual distinct_id Propagation
When navigating between completely different domains:
// Source domain (www.example.com)
function navigateWithIdentity(targetUrl) {
const distinctId = mixpanel.get_distinct_id();
const urlWithId = `${targetUrl}?mp_distinct_id=${encodeURIComponent(distinctId)}`;
window.location.href = urlWithId;
}
// Usage
document.getElementById('checkout-button').addEventListener('click', function(e) {
e.preventDefault();
navigateWithIdentity('https://checkout.otherdomain.com/cart');
});
// Destination domain (checkout.otherdomain.com)
mixpanel.init('YOUR_PROJECT_TOKEN');
// Read distinct_id from URL and set it
const urlParams = new URLSearchParams(window.location.search);
const crossDomainId = urlParams.get('mp_distinct_id');
if (crossDomainId) {
mixpanel.identify(crossDomainId);
// Clean up URL to remove parameter
const cleanUrl = window.location.pathname + window.location.hash;
window.history.replaceState({}, document.title, cleanUrl);
}
Server-Side Session Bridging
For sensitive flows where URL parameters are undesirable:
// Source domain - store distinct_id server-side
app.post('/checkout/initiate', async (req, res) => {
const distinctId = req.body.distinct_id;
const sessionToken = generateSecureToken();
// Store in Redis/database with expiration
await redis.setex(`mp_session:${sessionToken}`, 300, distinctId);
res.json({
checkoutUrl: `https://checkout.otherdomain.com?session=${sessionToken}`
});
});
// Destination domain - retrieve distinct_id
app.get('/checkout', async (req, res) => {
const sessionToken = req.query.session;
const distinctId = await redis.get(`mp_session:${sessionToken}`);
res.render('checkout', {
distinctId: distinctId
});
});
<!-- Destination page template -->
<script>
mixpanel.init('YOUR_PROJECT_TOKEN');
const distinctId = '<%= distinctId %>';
if (distinctId) {
mixpanel.identify(distinctId);
}
</script>
Authentication Flow Identity Management
Signup Flow
// Before signup - user is anonymous
mixpanel.track('Signup Form Viewed', {
source: 'homepage',
plan: 'free'
});
// After signup - create alias link
function handleSignup(userId, email) {
const previousId = mixpanel.get_distinct_id();
// Create alias (only call once per user)
mixpanel.alias(userId, previousId);
// Identify with new user ID
mixpanel.identify(userId);
// Set user properties
mixpanel.people.set({
'$email': email,
'$created': new Date().toISOString(),
'signup_source': 'web'
});
mixpanel.track('Signup Completed', {
method: 'email'
});
}
Login Flow
// Login on different domain
function handleLogin(userId) {
// Identify user (no alias needed for returning users)
mixpanel.identify(userId);
// Update last login
mixpanel.people.set({
'$last_login': new Date().toISOString()
});
mixpanel.track('Login', {
domain: window.location.hostname
});
}
Cross-Domain Login Decision Matrix
| Scenario | Action | Method |
|---|---|---|
| First-time signup | Link anonymous to user ID | mixpanel.alias(userId, anonymousId) then identify(userId) |
| Returning user login (same domain) | Identify user | mixpanel.identify(userId) |
| Returning user login (different domain) | Identify + pass userId | mixpanel.identify(userId) on both domains |
| Cross-domain after login | Propagate userId in URL/session | Pass userId parameter |
| Logout | Reset to new anonymous ID | mixpanel.reset() |
Third-Party Checkout Integration
Payment Provider Flow
When redirecting to Stripe, PayPal, or other payment processors:
// Before redirect to Stripe
function initiateCheckout() {
const distinctId = mixpanel.get_distinct_id();
// Track checkout initiation
mixpanel.track('Checkout Initiated', {
amount: 99.99,
currency: 'USD'
});
// Create Stripe session with return URL containing distinct_id
fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
distinct_id: distinctId,
success_url: `https://example.com/success?mp_id=${distinctId}&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `https://example.com/cart?mp_id=${distinctId}`
})
})
.then(res => res.json())
.then(data => {
window.location.href = data.url;
});
}
// On success page
window.addEventListener('load', function() {
const urlParams = new URLSearchParams(window.location.search);
const distinctId = urlParams.get('mp_id');
const sessionId = urlParams.get('session_id');
if (distinctId) {
mixpanel.identify(distinctId);
// Track purchase
fetch(`/api/get-order/${sessionId}`)
.then(res => res.json())
.then(order => {
mixpanel.track('Purchase Completed', {
order_id: order.id,
amount: order.amount,
currency: order.currency,
payment_provider: 'stripe'
});
});
}
});
First-Party Proxy Configuration
To maintain consistent tracking across domains and bypass ad blockers:
Nginx Proxy Setup
# Proxy Mixpanel tracking endpoint
location /mp/ {
proxy_pass https://api.mixpanel.com/;
proxy_set_header Host api.mixpanel.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_ssl_server_name on;
}
# Proxy Mixpanel decision endpoint
location /mp-decide/ {
proxy_pass https://decide.mixpanel.com/;
proxy_set_header Host decide.mixpanel.com;
proxy_set_header X-Real-IP $remote_addr;
}
JavaScript Configuration with Proxy
mixpanel.init('YOUR_PROJECT_TOKEN', {
api_host: 'https://yoursite.com', // Your domain
api_routes: {
track: '/mp/track',
engage: '/mp/engage',
groups: '/mp/groups',
record: '/mp/record'
},
cross_subdomain_cookie: true,
cookie_domain: '.yoursite.com'
});
Benefits of First-Party Proxying
| Benefit | Impact |
|---|---|
| Bypass ad blockers | Reduces tracking data loss by 20-40% |
| Maintain cookies across domains | Cookies from your domain aren't blocked |
| Improved attribution | Less fragmentation from blocking |
| GDPR compliance | First-party data collection |
iFrame and Widget Tracking
Embedded Widget Communication
For widgets embedded across different domains:
// Host page (example.com)
window.addEventListener('message', function(event) {
if (event.data.type === 'widget_request_identity') {
const distinctId = mixpanel.get_distinct_id();
// Send distinct_id to widget
const widgetFrame = document.getElementById('widget-iframe');
widgetFrame.contentWindow.postMessage({
type: 'identity_response',
distinct_id: distinctId
}, 'https://widget.otherdomain.com');
}
});
// Widget page (widget.otherdomain.com)
mixpanel.init('YOUR_PROJECT_TOKEN');
// Request identity from parent
if (window.parent !== window) {
window.parent.postMessage({
type: 'widget_request_identity'
}, '*');
}
// Receive identity
window.addEventListener('message', function(event) {
if (event.data.type === 'identity_response') {
const distinctId = event.data.distinct_id;
mixpanel.identify(distinctId);
// Now track widget events with parent's identity
mixpanel.track('Widget Loaded');
}
});
iFrame Security Considerations
- Use specific origins instead of
'*'inpostMessagefor security - Validate message origins before processing
- Only share identity data, not sensitive information
- Consider if iframe tracking genuinely adds analytical value
Edge Cases and Special Scenarios
SPA Navigation Across Subdomains
For single-page apps that navigate across subdomains:
// Initialize with SPA support
mixpanel.init('YOUR_PROJECT_TOKEN', {
cross_subdomain_cookie: true,
cookie_domain: '.example.com',
persistence: 'localStorage' // More reliable for SPAs
});
// Track virtual page views
function trackPageView(pageName) {
mixpanel.track('Page View', {
page: pageName,
url: window.location.href,
referrer: document.referrer
});
}
// Use with SPA router
router.afterEach((to, from) => {
trackPageView(to.name);
});
Mobile App to Web Deep Links
When users click links from mobile apps:
// Mobile app (React Native)
import mixpanel from 'mixpanel-react-native';
function openWebCheckout(userId, productId) {
const distinctId = userId;
const webUrl = `https://example.com/checkout/${productId}?mp_id=${distinctId}&source=mobile_app`;
// Track the transition
mixpanel.track('Web Checkout Opened', {
product_id: productId,
source: 'mobile_app'
});
Linking.openURL(webUrl);
}
// Web checkout page
mixpanel.init('YOUR_PROJECT_TOKEN');
const urlParams = new URLSearchParams(window.location.search);
const mobileUserId = urlParams.get('mp_id');
const source = urlParams.get('source');
if (mobileUserId && source === 'mobile_app') {
mixpanel.identify(mobileUserId);
mixpanel.track('Checkout Page Viewed', {
transition_source: 'mobile_app'
});
}
EU Data Residency
For EU data residency requirements with cross-domain tracking:
mixpanel.init('YOUR_PROJECT_TOKEN', {
api_host: 'https://api-eu.mixpanel.com',
cross_subdomain_cookie: true,
cookie_domain: '.example.com',
ignore_dnt: false // Respect Do Not Track
});
Validation and Testing
Pre-Deployment Testing Checklist
| Test Scenario | Expected Result | Validation Method |
|---|---|---|
| Navigate from domain A to B | Same distinct_id on both | Check Live View |
| Login on domain B after browsing A | Anonymous events merge with user | Profile timeline |
| Signup flow across domains | Alias created, no duplicate profiles | People page |
| Third-party checkout return | Session continues, purchase attributed | Funnel report |
| iFrame widget loads | Widget inherits parent distinct_id | Event properties |
| Ad blocker enabled | Events still tracked via proxy | Network tab |
Live View Validation
- Open Mixpanel Live View
- Start a test session on source domain
- Note the
distinct_idfor your test events - Navigate to destination domain
- Verify same
distinct_idappears in Live View - Check that session properties persist
Identity Merge Validation
// Add test helper to console
window.mpTest = {
getDistinctId: () => mixpanel.get_distinct_id(),
checkIdentity: () => {
console.log('Current distinct_id:', mixpanel.get_distinct_id());
console.log('Cookie value:', document.cookie.match(/mp_[^=]+_mixpanel=([^;]+)/)?.[1]);
}
};
// Test cross-domain flow
// On domain A:
mpTest.checkIdentity(); // Note the ID
// On domain B:
mpTest.checkIdentity(); // Should match domain A if configured correctly
Profile Merge Settings Verification
- Go to Project Settings > Identity Merge
- Check merge rules configuration:
- ID Merge: Merges profiles when
aliasis called - Original ID Merge: Legacy behavior
- None: No automatic merging (manual only)
- ID Merge: Merges profiles when
- Verify merge behavior matches expectations
Network Tab Inspection
Monitor requests during cross-domain navigation:
- Open DevTools > Network
- Filter for
mixpanel.comor your proxy endpoint - Navigate across domains
- Verify
distinct_idin request payloads matches - Check that
/decideendpoint responses are consistent
Cookie Inspection
Verify cookie settings in DevTools:
// Check cookie configuration
function inspectMixpanelCookie() {
const cookies = document.cookie.split(';');
const mpCookie = cookies.find(c => c.trim().startsWith('mp_'));
if (mpCookie) {
const [name, value] = mpCookie.split('=');
const decoded = JSON.parse(decodeURIComponent(value));
console.log('Mixpanel Cookie:', {
name: name.trim(),
distinct_id: decoded.distinct_id,
device_id: decoded.device_id
});
}
}
Funnel Report Validation
After implementing cross-domain tracking:
- Create a funnel that spans both domains
- Run the funnel for test users
- Verify conversion rates make sense
- Check for drop-offs at domain boundaries
- Compare before/after implementation metrics
Troubleshooting
| Symptom | Likely Cause | Solution |
|---|---|---|
| Different distinct_id on each domain | Cookies not shared | Set cross_subdomain_cookie: true and cookie_domain |
| distinct_id lost after redirect | URL parameter not passed | Add distinct_id to redirect URL |
| Duplicate profiles created | alias called multiple times | Call alias only once on signup, use identify for login |
| Cookie not persisting | Cookie domain mismatch | Verify cookie_domain matches actual domain structure |
| Events blocked on subdomain | Ad blocker targeting | Implement first-party proxy |
| Identity lost in iframe | Cross-origin restrictions | Use postMessage to share distinct_id |
| Checkout return loses session | Return URL missing distinct_id | Include distinct_id in success_url |
| Different distinct_id in mobile app vs web | No identifier bridging | Pass user ID from app to web URL |
| EU users not tracking | Wrong API endpoint | Use api_host: 'https://api-eu.mixpanel.com' |
| Session restarts after login | Reset called incorrectly | Only call reset() on logout |
| Properties inconsistent across domains | Super properties not synced | Re-register super properties after identify |
Common Implementation Errors
Calling alias Multiple Times
// WRONG - creates duplicate profiles
function handleLogin(userId) {
mixpanel.alias(userId); // Don't alias on every login
mixpanel.identify(userId);
}
// CORRECT - alias only on signup
function handleSignup(userId) {
const anonId = mixpanel.get_distinct_id();
mixpanel.alias(userId, anonId); // Only once
mixpanel.identify(userId);
}
function handleLogin(userId) {
mixpanel.identify(userId); // Just identify
}
Incorrect Cookie Domain
// WRONG - will not work across subdomains
mixpanel.init('TOKEN', {
cookie_domain: 'www.example.com' // Missing leading dot
});
// CORRECT
mixpanel.init('TOKEN', {
cookie_domain: '.example.com' // Includes all subdomains
});
Missing distinct_id Propagation
// WRONG - doesn't pass identity
window.location.href = 'https://checkout.example.com/cart';
// CORRECT
const distinctId = mixpanel.get_distinct_id();
window.location.href = `https://checkout.example.com/cart?mp_id=${distinctId}`;
Best Practices
- Use subdomain architecture when possible: Easier to maintain shared cookies than cross-domain parameter passing
- Implement first-party proxy: Reduces tracking loss from ad blockers
- Test alias behavior thoroughly: Incorrect aliasing creates unfixable duplicate profiles
- Document identity flow: Maintain clear documentation of when alias vs identify is called
- Monitor profile merge activity: Watch for unexpected duplicate profiles
- Use server-side for sensitive flows: Don't expose user IDs in URLs for security-critical flows
- Validate in staging first: Test cross-domain flows in staging environment
- Set up alerts: Monitor for sudden drops in cross-domain conversion metrics
- Respect user privacy: Honor DNT flags and consent preferences across all domains
- Clean up URL parameters: Remove identity parameters from URL after reading to avoid sharing