Mixpanel is an event-based product analytics platform focused on user behavior analysis through funnels, retention curves, and cohort comparisons. Like Amplitude, Mixpanel's value depends entirely on event quality and identity resolution. The critical difference is Mixpanel's identity model: the distinct_id system and the identify/alias flow have historically been a source of implementation bugs that permanently corrupt user profiles. Mixpanel has simplified this with their "Simplified ID Merge" system, but understanding the identity model remains the most important part of a correct implementation.
Why Proper Implementation Matters
Identity Resolution Mistakes are Permanent
Mixpanel's identity system assigns every user a distinct_id:
- Before login:
distinct_idis an auto-generated anonymous ID (UUID) - After login: You call
mixpanel.identify('user-123')to merge the anonymous ID with your internal ID - This merge is permanent and irreversible
- If you call
identify()with the wrong user (shared device, wrong timing), all anonymous events are permanently attributed to the wrong person - Un-merging requires Mixpanel support intervention and may result in data loss
Event Naming Affects All Downstream Analysis
Every Mixpanel analysis (funnels, retention, flows) references events by name:
- Event names are case-sensitive: "Sign Up" and "sign_up" are different events
- Renaming events in Lexicon only changes the display name, not the underlying data
- Saved reports, boards, and alerts reference the original event name
- A taxonomy change mid-implementation splits historical data
Pricing Is Event-Based
Mixpanel's pricing tiers are based on monthly tracked users (MTU) or event volume:
- Every
track()call counts toward your quota - Page views, if tracked, count as events
- Server-side events count toward your quota
- Profile updates (
people.set()) do not count as events but have storage implications
Pre-Implementation Planning
Access and Permissions
Mixpanel Account:
- Sign in at
mixpanel.com - Navigate to your organization and project
- Request Owner or Admin access for implementation
Required Access:
| Platform | Role | Purpose |
|---|---|---|
| Mixpanel | Owner/Admin | Project settings, API credentials, Lexicon |
| Mixpanel | Analyst | Report creation, cohort management |
| Application | Developer | SDK integration |
| Server | Developer | HTTP API integration |
Project Credentials (per environment):
| Credential | Where to Find | Use |
|---|---|---|
| Project Token | Settings > Project Settings | Client-side SDK initialization |
| API Secret | Settings > Project Settings | Server-side API authentication |
| Service Account | Settings > Service Accounts | Automated imports and exports |
Data Residency:
- US servers: Default (
api.mixpanel.com) - EU servers: Must be selected at project creation (
api-eu.mixpanel.com) - Cannot be changed after project creation
- EU residency required for GDPR compliance for EU-based companies
Event Taxonomy Design
Design your taxonomy before implementation:
| Event Name | Category | Key Properties | Description |
|---|---|---|---|
| Page Viewed | Navigation | page_name, page_url, page_type | User views any page |
| Sign Up Started | Onboarding | signup_method | User initiates registration |
| Sign Up Completed | Onboarding | signup_method, plan_type | User completes registration |
| Feature Used | Product | feature_name, feature_context | User engages a product feature |
| Item Added to Cart | Commerce | item_id, item_name, price, quantity | User adds product to cart |
| Purchase Completed | Commerce | order_id, revenue, currency, items | User completes purchase |
| Subscription Started | Commerce | plan_type, billing_period, mrr | User starts subscription |
Naming Conventions:
- Event names:
Title Casewith spaces ("Purchase Completed", not "purchase_completed") - Property names:
snake_case("item_name", "plan_type") - Consistent across client and server implementations
Identity Strategy
Simplified ID Merge (recommended, default for new projects):
Anonymous Visit
└─> distinct_id = "$device:UUID-123" (auto-generated)
└─> Page Viewed, Button Clicked (anonymous events)
└─> User logs in
└─> mixpanel.identify("USER-456")
└─> distinct_id becomes "USER-456"
└─> Anonymous events are automatically merged
└─> All future events use "USER-456"
Rules:
- Call
identify()only after confirmed authentication - Never use email as
distinct_id(emails change; use internal user ID) - Call
mixpanel.reset()on logout to generate a new anonymous ID - For shared devices: Always
reset()between users
Original ID Merge (legacy projects):
- Uses
alias()instead ofidentify()for first-time user creation - More complex and error-prone
- Migrate to Simplified ID Merge if possible (check project settings)
Implementation Walkthrough
Step 1: Install Mixpanel JavaScript SDK
npm Installation (recommended for SPAs):
npm install mixpanel-browser
import mixpanel from 'mixpanel-browser';
mixpanel.init('YOUR_PROJECT_TOKEN', {
debug: process.env.NODE_ENV === 'development',
track_pageview: true, // Auto-track page views
persistence: 'localStorage', // 'cookie' or 'localStorage'
// For EU data residency:
// api_host: 'https://api-eu.mixpanel.com',
ignore_dnt: false, // Respect Do Not Track header
// For proxy setup (bypass ad blockers):
// api_host: 'https://analytics.yoursite.com',
});
<script type="text/javascript">
(function(f,b){if(!b.__SV){var e,g,i,h;window.mixpanel=b;b._i=[];
b.init=function(e,f,c){function g(a,d){var b=d.split(".");2==b.length&&
(a=a[b[0]],d=b[1]);a[d]=function(){a.push([d].concat(Array.prototype.slice.call(
arguments,0)))}}var a=b;"undefined"!==typeof c?a=b[c]=[]:c="mixpanel";a.people=
a.people||[];a.toString=function(a){var d="mixpanel";"mixpanel"!==c&&(d+="."+c);
a||(d+=" (stub)");return d};a.people.toString=function(){return a.toString(1)+
".people (stub)"};i="disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");
for(h=0;h<i.length;h++)g(a,i[h]);var j="set set_once union unset remove delete".split(" ");
a.get_group=function(){function b(c){d[c]=function(){call2_args=arguments;call2=[c].concat(
Array.prototype.slice.call(call2_args,0));a.push([e,call2])}}for(var d={},e=["get_group"].concat(
Array.prototype.slice.call(arguments,0)),c=0;c<j.length;c++)b(j[c]);return d};
b._i.push([e,f,c])};b.__SV=1.2;e=f.createElement("script");e.type="text/javascript";
e.async=!0;e.src="https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
g=f.getElementsByTagName("script")[0];g.parentNode.insertBefore(e,g)}})(document,window.mixpanel||[]);
mixpanel.init('YOUR_PROJECT_TOKEN', {
track_pageview: true,
persistence: 'localStorage'
});
</script>
Step 2: Implement User Identity
// After successful authentication
function onUserLogin(user) {
// Identify the user - merges anonymous profile with identified profile
mixpanel.identify(user.id); // e.g., 'USR-12345'
// Set user profile properties
mixpanel.people.set({
'$email': user.email, // Special property (used for notifications)
'$name': user.name, // Special property (display name)
'$created': user.createdAt, // Special property (signup date)
'plan_type': user.plan, // Custom property
'company': user.company,
'role': user.role
});
// Set once (only if not already set)
mixpanel.people.set_once({
'first_login_date': new Date().toISOString(),
'initial_referrer': document.referrer,
'initial_utm_source': getUTMParam('utm_source')
});
}
// On logout
function onUserLogout() {
mixpanel.track('Logged Out');
mixpanel.reset(); // Clears distinct_id and super properties
}
Step 3: Track Events with Properties
// Page view (if not using auto-tracking)
mixpanel.track('Page Viewed', {
page_name: document.title,
page_url: window.location.href,
page_type: getPageType(), // 'product', 'blog', 'pricing'
referrer: document.referrer
});
// Product interaction
mixpanel.track('Item Added to Cart', {
item_id: 'SKU-12345',
item_name: 'Premium Widget',
category: 'Widgets',
price: 49.99,
quantity: 1,
currency: 'USD'
});
// Purchase with revenue
mixpanel.track('Purchase Completed', {
order_id: 'ORD-2024-12345',
revenue: 129.98,
currency: 'USD',
item_count: 2,
discount_code: 'SAVE10',
payment_method: 'credit_card',
items: [
{ id: 'SKU-12345', name: 'Premium Widget', price: 49.99, quantity: 1 },
{ id: 'SKU-67890', name: 'Deluxe Gadget', price: 79.99, quantity: 1 }
]
});
// Also track revenue on the user profile for LTV calculation
mixpanel.people.track_charge(129.98, {
order_id: 'ORD-2024-12345',
currency: 'USD'
});
// Feature usage
mixpanel.track('Feature Used', {
feature_name: 'Export Dashboard',
feature_context: 'analytics_page',
export_format: 'csv'
});
Step 4: Register Super Properties
Super properties are automatically appended to every event:
// Set super properties after login (persisted across sessions)
mixpanel.register({
'user_plan': 'pro',
'app_version': '2.4.1',
'platform': 'web'
});
// Set super properties that only apply once (first value wins)
mixpanel.register_once({
'first_touch_utm_source': getUTMParam('utm_source'),
'first_touch_utm_medium': getUTMParam('utm_medium'),
'first_touch_utm_campaign': getUTMParam('utm_campaign')
});
// Remove a super property
mixpanel.unregister('deprecated_property');
Step 5: Server-Side Event Tracking
For backend events that cannot be captured client-side:
const Mixpanel = require('mixpanel');
const mixpanel = Mixpanel.init('YOUR_PROJECT_TOKEN', {
// For EU data residency:
// host: 'api-eu.mixpanel.com',
});
// Track server-side event
function trackServerEvent(distinctId, eventName, properties) {
mixpanel.track(eventName, {
distinct_id: distinctId,
...properties,
$source: 'server' // Tag as server-generated
});
}
// Update user profile from server
function updateUserProfile(distinctId, properties) {
mixpanel.people.set(distinctId, properties);
}
// Example: Subscription renewal from Stripe webhook
async function handleStripeRenewal(subscription) {
const userId = subscription.metadata.mixpanel_distinct_id;
trackServerEvent(userId, 'Subscription Renewed', {
plan_type: subscription.plan.id,
revenue: subscription.plan.amount / 100,
currency: subscription.currency.toUpperCase(),
billing_period: subscription.plan.interval,
renewal_count: parseInt(subscription.metadata.renewal_count || '1')
});
// Track charge on profile for LTV
mixpanel.people.track_charge(userId, subscription.plan.amount / 100, {
plan_type: subscription.plan.id,
type: 'renewal'
});
// Update profile
updateUserProfile(userId, {
'subscription_status': 'active',
'last_renewal_date': new Date().toISOString(),
'total_renewals': parseInt(subscription.metadata.renewal_count || '1')
});
}
// Batch import for historical data
const importBatch = [
{ event: 'Purchase Completed', properties: { distinct_id: 'USR-1', revenue: 99, time: 1704067200 }},
{ event: 'Purchase Completed', properties: { distinct_id: 'USR-2', revenue: 149, time: 1704153600 }}
];
mixpanel.import_batch(importBatch, { strict_mode: true });
Step 6: Configure Lexicon
Lexicon is Mixpanel's data dictionary. Curate it to maintain taxonomy quality:
In Mixpanel, go to Data Management > Lexicon
For each event:
- Set Status: Expected, Live, or Deprecated
- Add Description: Business context and when this event fires
- Set Owner: Team or individual responsible
- Tag related events (e.g., all commerce events)
For each property:
- Set Data Type: String, Number, Boolean, DateTime, List
- Add Description: What values are expected
- Mark as Required or Optional
- Set Example Values
Configure Data Views to hide deprecated or internal events from non-technical users
Step 7: Set Up Group Analytics (B2B)
For account-level analytics in B2B products:
// Define the group (account/company)
mixpanel.set_group('company', 'ACME Corp');
// Set group profile properties
mixpanel.get_group('company', 'ACME Corp').set({
'plan': 'enterprise',
'employee_count': 500,
'industry': 'technology',
'account_manager': 'jane@yourcompany.com'
});
// Events automatically include group membership
mixpanel.track('Feature Used', {
feature_name: 'Advanced Export'
// 'company' group automatically appended
});
Group Analytics must be enabled in project settings and requires a paid plan.
Verification and QA
Live View
- In Mixpanel, go to Events (or use the search bar to find "Live View")
- Filter by your test user's
distinct_id - Perform actions on your website
- Verify in real-time:
- Events appear with correct names (case-sensitive)
- Properties are populated with correct values and types
distinct_idis correct (anonymous before login, user ID after)- Super properties are appended to every event
Identity Validation
- Create a new browser session (incognito)
- Browse several pages (anonymous events)
- Log in as a test user
- In Mixpanel User Profiles, search for your test user
- Verify:
- Anonymous events are merged with the identified profile
- Profile properties are set correctly
- No duplicate profiles exist for the same user
- Log out and verify
reset()generates new anonymous ID - Log in as a different user -- verify no cross-contamination
Lexicon Validation
- Go to Data Management > Lexicon
- Check for:
- Unexpected events: Events not in your taxonomy (typos, test events)
- Undescribed events: Events without descriptions or owners
- Type mismatches: Properties with unexpected data types
- Deprecated events: Old events still receiving data
Common Issues
| Issue | Cause | Fix |
|---|---|---|
| Events not appearing | Wrong project token | Verify token in init() matches project settings |
| Identity merge failure | identify() called before login confirmed |
Only call after successful authentication |
| Duplicate user profiles | reset() not called on logout |
Add reset() to logout flow |
| Super properties missing | register() called before init() |
Ensure SDK initialized before registering |
| EU compliance failure | Using US endpoint | Set api_host: 'https://api-eu.mixpanel.com' |
| Server events missing | Wrong distinct_id | Ensure server uses same user ID as client-side identify() |
| Profile charges wrong | Currency not normalized | Always use same currency code |
| Events blocked by ad blocker | Default Mixpanel domain blocked | Set up first-party proxy |
First-Party Proxy Setup
To bypass ad blockers, route Mixpanel requests through your own domain:
# Nginx reverse proxy configuration
location /mp/ {
proxy_pass https://api.mixpanel.com/;
proxy_set_header Host api.mixpanel.com;
proxy_set_header X-Real-IP $remote_addr;
}
location /mp-decide/ {
proxy_pass https://decide.mixpanel.com/;
proxy_set_header Host decide.mixpanel.com;
}
// Point SDK to your proxy
mixpanel.init('YOUR_TOKEN', {
api_host: 'https://yoursite.com/mp'
});
Deployment Artifacts
- Project tokens and API secrets: Per environment, stored securely
- Tracking plan: Events, properties, types, and descriptions (sync with Lexicon)
- Identity strategy document:
identify()timing,reset()behavior, shared device handling - Super property definitions: Which properties to register and when
- Server-side integration: Import API endpoint, authentication, payload format
- Lexicon export: Event and property definitions with owners
- Proxy configuration (if applicable): Nginx/CDN config for first-party tracking
- Data residency: US or EU endpoint configuration
Linked Runbooks
- Install or Embed the Tag or SDK -- SDK installation methods
- Event Tracking -- Event and property configuration
- Data Layer Setup -- Data layer for Mixpanel events
- Cross-Domain Tracking -- Identity management across domains
- Server-Side vs Client-Side -- JavaScript SDK vs. Import API decisions
Change Log and Owners
- Document who manages Lexicon curation and event approval
- Track super property changes with rationale
- Maintain project token rotation schedule
- Record identity strategy changes (especially migration from Original to Simplified ID Merge)
- Review quarterly: event volume, Lexicon health, profile merge accuracy