Mixpanel: Event Tracking, Identity Resolution, and JQL | OpsBlu Docs

Mixpanel: Event Tracking, Identity Resolution, and JQL

Implement Mixpanel event tracking with mixpanel.track(), distinct_id identity resolution, simplified ID merge, JQL queries, Lexicon data governance,...

How Mixpanel Works

Mixpanel is an event-based analytics platform. Every tracked interaction is an event with a name, a timestamp, a set of properties, and a distinct_id that identifies the user. There are no sessions, pageviews, or hit types as first-class concepts. Sessions are computed at query time by grouping events with gaps of less than a configurable timeout (default 30 minutes).

The data model has three entity types:

  • Events -- Timestamped records of user actions. Each event has a name (Signup, Purchase, Page View) and a flat dictionary of properties (string, number, boolean, list, or datetime values). Events are immutable after ingestion.

  • User Profiles -- Persistent records of user attributes. Profile properties describe the user's current state: plan type, email, signup date, total spend. Unlike events, profiles are mutable and updated in place.

  • Group Profiles -- Persistent records for account-level entities (companies, workspaces, teams). Used for B2B analytics where you need to aggregate behavior across all users in an account.

When Mixpanel receives an event, it stores it in a columnar database indexed by project, time, and distinct_id. Queries in the Mixpanel UI (funnels, retention, flows, etc.) scan the event stream and apply filters, breakdowns, and aggregations at query time. There is no pre-aggregation step, which means you can slice data by any property combination without defining it ahead of time. This is a schema-on-read approach for queries, though Mixpanel does validate data types at ingestion.

Data appears in the Mixpanel UI within seconds of ingestion. There is no batch processing delay.

Installing the Mixpanel SDK

JavaScript SDK

Install via npm or script tag:

npm install mixpanel-browser
import mixpanel from 'mixpanel-browser';

mixpanel.init('YOUR_PROJECT_TOKEN', {
  debug: false,                    // Set true during development
  track_pageview: true,            // Auto-track page views
  persistence: 'localStorage',     // 'cookie' or 'localStorage'
  api_host: 'https://api-js.mixpanel.com',  // Default; override for EU residency
  ignore_dnt: false                // Respect Do Not Track header
});

Or via script tag:

<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="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===f.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//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');
</script>

Server-Side (Node.js)

const Mixpanel = require('mixpanel');
const mixpanel = Mixpanel.init('YOUR_PROJECT_TOKEN');

mixpanel.track('Subscription Renewed', {
  distinct_id: 'USER_123',
  plan: 'annual',
  revenue: 599.00,
  currency: 'USD'
});

Server-Side (Python)

from mixpanel import Mixpanel

mp = Mixpanel('YOUR_PROJECT_TOKEN')

mp.track('USER_123', 'Subscription Renewed', {
    'plan': 'annual',
    'revenue': 599.00,
    'currency': 'USD'
})

Event Tracking and Data Collection

Tracking Events

// Basic event
mixpanel.track('Button Clicked');

// Event with properties
mixpanel.track('Item Purchased', {
  item_id: 'SKU_456',
  item_name: 'Pro Widget',
  price: 29.99,
  currency: 'USD',
  category: 'Widgets'
});

// Track with a callback (useful for navigation events)
mixpanel.track('Link Clicked', { url: '/pricing' }, { send_immediately: true });

Super Properties

Super properties are attached to every subsequent event automatically. Use them for values that rarely change during a session:

// Set super properties (persist across events)
mixpanel.register({
  'Platform': 'Web',
  'App Version': '2.4.1',
  'User Plan': 'premium'
});

// Set once (only if not already set)
mixpanel.register_once({
  'First Referrer': document.referrer,
  'First Landing Page': window.location.pathname
});

// Remove a super property
mixpanel.unregister('Deprecated Property');

Super properties are stored in the cookie or localStorage (depending on persistence setting) and included in the payload of every track call.

User Profile Properties

User profiles are separate from events. Set profile properties with the people API:

// Set user profile properties
mixpanel.people.set({
  '$name': 'Jane Martinez',
  '$email': 'jane@example.com',
  'Plan': 'Premium',
  'Company': 'Acme Corp',
  'Signup Date': '2026-01-15T00:00:00'
});

// Set only if not already set
mixpanel.people.set_once({
  'First Login': new Date().toISOString()
});

// Increment a numeric property
mixpanel.people.increment('Login Count', 1);

// Append to a list property
mixpanel.people.append('Viewed Features', 'Dashboard');

// Track a charge (for revenue analysis)
mixpanel.people.track_charge(29.99, {
  '$time': new Date().toISOString(),
  'Plan': 'Pro Monthly'
});

Profile properties power Mixpanel's user-level reports: you can filter events by profile properties, build cohorts based on profile values, and export user lists.

Identity and User Tracking

The Distinct ID

Every event in Mixpanel must have a distinct_id. On the client side, the SDK generates a random UUID as the anonymous distinct_id and stores it in a cookie or localStorage. When the user logs in, you call identify to link the anonymous ID to a known user ID.

Simplified ID Merge

Mixpanel uses a Simplified ID Merge model. When you call identify, Mixpanel creates a permanent link between the anonymous ID and the identified user ID:

// Before login: events are tracked with anonymous distinct_id (auto-generated)
mixpanel.track('Page View', { page: '/pricing' });

// On login: link anonymous ID to known user ID
mixpanel.identify('USER_123');

// After identify: all subsequent events use 'USER_123' as distinct_id
mixpanel.track('Dashboard Viewed');

How Simplified ID Merge works:

  1. Before identify, events have the auto-generated anonymous distinct_id
  2. identify('USER_123') creates a mapping: anonymous_id -> USER_123
  3. All future events from this device use USER_123
  4. Historical events with the anonymous ID are retroactively attributed to USER_123 in queries

The merge is cluster-based: Mixpanel groups all IDs (anonymous and identified) that have ever been linked into an identity cluster. Any event with any ID in the cluster is attributed to the same user.

Handling Logout

// On logout: reset the distinct_id to prevent cross-user attribution
mixpanel.reset();

reset() clears the current distinct_id, super properties, and generates a new anonymous ID. This prevents the next user's events from being attributed to the previous user.

Server-Side Identity

For server-side tracking, always pass the canonical user ID as distinct_id:

// Server-side: always use the known user ID
mixpanel.track('Subscription Renewed', {
  distinct_id: 'USER_123',
  plan: 'annual'
});

Group Analytics

Group analytics lets you analyze behavior at the account or organization level:

// Associate a user with a group
mixpanel.set_group('company', 'Acme Corp');

// Set group profile properties
mixpanel.get_group('company', 'Acme Corp').set({
  'Plan': 'Enterprise',
  'Employee Count': 500,
  'Industry': 'Technology'
});

// Track an event with group context
mixpanel.track_with_groups('Feature Activated', {
  feature: 'Advanced Reports'
}, {
  'company': 'Acme Corp'
});

Group analytics enables queries like "which companies have the highest feature adoption" or "average events per company per week." Groups require a paid Mixpanel plan.

API and Data Export

Ingestion API

Send events directly to Mixpanel's ingestion endpoint:

# Send events via the Import API
curl -X POST "https://api.mixpanel.com/import?strict=1&project_id=YOUR_PROJECT_ID" \
  -H "Authorization: Basic BASE64_SERVICE_ACCOUNT_SECRET" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "event": "Server Purchase",
      "properties": {
        "distinct_id": "USER_123",
        "time": 1709568000,
        "$insert_id": "unique-event-id-123",
        "amount": 150.00,
        "currency": "USD"
      }
    }
  ]'

The $insert_id is a deduplication key. If Mixpanel receives two events with the same $insert_id within 5 days, it drops the duplicate. Always set this for server-side imports.

The strict=1 parameter enables strict validation: Mixpanel rejects the entire batch if any event has invalid data types or missing required fields.

Query API (JQL)

JQL (JavaScript Query Language) lets you write arbitrary JavaScript functions that run against the event stream:

// JQL query: count events by type in the last 7 days
function main() {
  return Events({
    from_date: '2026-02-25',
    to_date: '2026-03-04'
  })
  .groupBy(['name'], mixpanel.reducer.count());
}
// JQL: compute average revenue per user for purchases
function main() {
  return Events({
    from_date: '2026-02-25',
    to_date: '2026-03-04',
    event_selectors: [{ event: 'Purchase' }]
  })
  .groupByUser(mixpanel.reducer.numeric_summary('amount'))
  .reduce(mixpanel.reducer.numeric_summary('value.sum'));
}

JQL queries are executed via the Query API:

curl "https://mixpanel.com/api/2.0/jql" \
  -H "Authorization: Basic BASE64_SERVICE_ACCOUNT_SECRET" \
  --data-urlencode 'script=function main() { return Events({from_date:"2026-03-01",to_date:"2026-03-04"}).groupBy(["name"], mixpanel.reducer.count()); }'

Data Export

Export raw event data:

# Export raw events for a date range
curl "https://data.mixpanel.com/api/2.0/export?from_date=2026-03-01&to_date=2026-03-04" \
  -H "Authorization: Basic BASE64_SERVICE_ACCOUNT_SECRET"

Returns newline-delimited JSON (NDJSON). Each line is a single event with all properties.

Warehouse Connectors

Mixpanel supports direct export to Snowflake, BigQuery, and data lakes via its warehouse connector. Events and user profiles sync on a configurable schedule (hourly or daily).

Data Governance with Lexicon

Lexicon is Mixpanel's data governance layer. It provides:

  • Event and property definitions: Document what each event means, who owns it, and what properties it should have
  • Data type enforcement: Flag events with properties that don't match expected types
  • Volume monitoring: Track event volume trends to detect instrumentation regressions
  • Visibility controls: Hide deprecated events and properties from the UI so analysts don't use stale data

Access Lexicon under the Data Management section. You can bulk-import definitions via CSV or manage them through the Lexicon API.

Common Issues

Issue Cause Fix
Events tracked but not appearing in reports Project token is for a different project, or events are being sent to the wrong data residency endpoint Verify the project token in Project Settings; for EU data residency, set api_host to https://api-eu.mixpanel.com
Identity merge not working identify called with the anonymous ID instead of the known user ID, or called after reset() was already called Pass only the known user ID to identify; call identify before reset
Duplicate events $insert_id not set on server-side imports, or SDK retrying failed requests Always set $insert_id for server-side events; the client SDK handles deduplication automatically
Super properties not appearing on events register called after track, or properties cleared by reset() Call register before track; re-register super properties after login if reset() was called on logout
User profile not updating people.set called before identify, so profile updates go to the anonymous ID Call identify before setting profile properties; or pass distinct_id explicitly in server-side profile updates
Event properties showing wrong data type Property sent as string instead of number (e.g., "29.99" instead of 29.99) Ensure numeric values are sent as numbers, not strings; Mixpanel infers the type from the first value received
High event volume from bots No bot filtering applied Use Mixpanel's built-in bot filtering (enabled by default in newer projects); add $ignore: true to events you want to exclude
JQL query timing out Query scans too many events or uses inefficient reducers Narrow the date range; use event_selectors to filter before grouping; avoid scanning more than 90 days

Platform-Specific Considerations

Data residency: Mixpanel offers US and EU data residency. EU projects store data in EU servers and require setting the API host to api-eu.mixpanel.com. This must be configured at project creation time and cannot be changed later.

Rate limits: The Ingestion API allows up to 2GB per minute per project. The Query API allows 60 concurrent queries per project. JQL queries have a 120-second timeout.

Event limits: Free plan allows 20M events per month. Growth and Enterprise plans scale by volume. Events older than the retention window (default 5 years) are deleted.

Property limits: Events support up to 255 properties per event. Property names are limited to 255 characters. String values are limited to 255 characters (longer strings are truncated).