Loading tutorials…
Loading tutorials…
Mixpanel doesn't fail because events break — it fails because event names drift. Three engineers, three opinions, three versions of 'signup' over a year. Here's how to ship instrumentation that holds up.
Who this is forFounders, PMs, and engineering leads who have Mixpanel installed and now need to instrument the events that matter. If you've fired 50+ events but your funnel reports show conflicting numbers across surfaces, this is the reset.
What you'll need
Step 1
Open a doc, list every event your team needs in the next 6 months. Pick one naming convention. Get sign-off from PM + Eng before instrumentation starts.
Open a shared doc (Notion, Confluence, Google Doc). Create columns: Event Name, Trigger (where in the app), Properties (the data we attach), Surface (web/iOS/Android/server), Owner.
Categorize events into tiers. Tier 1 (must have for business): Account Created, Subscription Started, Subscription Cancelled, Purchase Completed. Tier 2 (product loop): Workspace Created, Invite Sent, First Action Completed, Feature X Used. Tier 3 (nice-to-have): UI clicks for funnels you might build later.
Pick ONE naming convention and write it at the top of the doc. 'verb_object_snake_case' (signup_completed, plan_upgraded) is the modern default. 'Title Case Object Verb' (Account Created, Plan Upgraded) is the historical Mixpanel default. Both work — pick one and stop debating.
For each event, list 3-7 properties. Example for 'subscription_started': plan_tier (string: free/pro/enterprise), billing_interval (string: monthly/annual), price_usd (number), discount_code (string or null), source (string: where the upgrade came from — pricing page, in-app prompt, sales call).
Get explicit sign-off from a PM AND the engineering lead before any instrumentation code gets written. The cost of disagreeing in a doc is 30 minutes; the cost of disagreeing after 50 events are live is 4-6 weeks of cleanup.
Step 2
Mixpanel has reserved property names (distinct_id, $email, $name, etc.). Decide which Mixpanel reserved props you map to AND your own naming convention for custom props.
Mixpanel's reserved People properties start with $: $email, $name, $first_name, $last_name, $phone, $created, $avatar. Map your standard user data to these — they unlock email-based search, profile display, and integrations with email tools.
For custom properties, pick snake_case (recommended) or camelCase. Be consistent. Mixed conventions break SQL exports and downstream warehouse syncs.
Avoid property names that conflict with Mixpanel internals: time, distinct_id, mp_*, $insert_id (these are reserved). If you accidentally send them, Mixpanel may ignore or overwrite.
Numbers should be numbers, not strings. price_usd should be `49.00`, not `'$49'` or `'49.00'`. If you push strings, Mixpanel can't aggregate (sum, avg) and your dashboards show $0 totals.
Booleans should be booleans, not 'true'/'false' strings. Property `is_paid_user: true` works; `is_paid_user: 'true'` doesn't.
Arrays are supported for properties — e.g., `features_used: ['export', 'api', 'integrations']`. Use them sparingly; they make breakdown reports messier than flat properties.
Step 3
Client-side: UI interactions, page views, anything tied to user session. Server-side: revenue, plan changes, anything that must not be tampered with.
Client-side events fire from the browser or mobile app via the JS/Swift/Kotlin SDK. They're cheap to add but lose 5-15% of data to ad blockers, network errors, and abandoned sessions.
Server-side events fire from your backend via the Node/Python/Ruby SDK or the HTTP Track API. They're more reliable but require backend work. Use them for events where data loss is unacceptable.
Tier 1 events that MUST be server-side: Purchase Completed (fires from your Stripe/Recharge webhook), Subscription Started, Subscription Renewed, Subscription Cancelled, Refund Processed. These represent money and cannot rely on the browser staying open.
Tier 2 events that CAN be server-side or client-side: Account Created (server-side is better — fires reliably from your auth callback), Email Verified (server-side from your verification handler).
Tier 3 events that are usually client-side: page views, button clicks, scroll depth, video plays, form submits. These are cheap and acceptable to lose a few percent of.
When firing server-side, you need to pass the user's distinct_id (the same one the client uses) so events merge into one user profile. The pattern: client sets distinct_id on signup → backend stores it → backend fires server events with that distinct_id.
Step 4
mixpanel.track('event_name', { property: value }). Match the spec exactly. Run every event through code review against the spec doc.
JavaScript: `mixpanel.track('signup_completed', { plan_tier: 'free', source: 'organic_search', referral_code: null });`
Always include the canonical properties from your spec — never one-off ad-hoc properties. If an engineer wants to add a new property, it goes in the spec doc first.
Server-side Node: `mixpanel.track('purchase_completed', { distinct_id: user.id, plan_tier: 'pro', billing_interval: 'monthly', price_usd: 49.00, currency: 'USD' });` The distinct_id is required server-side (no implicit user context).
For revenue events specifically, also call `mixpanel.people.track_charge(distinct_id, amount, { ... })` — this populates the user's Lifetime Value (LTV) automatically and powers Mixpanel's revenue reports.
Wrap your tracking calls in a thin helper (lib/analytics.ts) so engineers can't drift from the spec. Helper function signature: `track(event: KnownEventName, properties: PropertyMapForEvent)`. TypeScript enforces the spec.
Run every new event through code review. The PR template should have a checkbox: 'Does this event match the spec doc?' Reviewer says no, PR doesn't merge.
Step 5
mixpanel.register({ ... }) sets properties that automatically attach to every subsequent event. Use for plan_tier, app_version, environment.
Super properties save you from re-passing the same context on every event. Set them once when context loads (page load, user signs in, app starts).
JavaScript: `mixpanel.register({ app_version: '2.4.1', environment: 'production', plan_tier: user.plan, account_age_days: user.accountAgeDays });`
These properties now attach to EVERY event the user fires. You can filter every funnel/cohort/retention chart by plan_tier without instrumenting it on each event.
Use mixpanel.register_once() for properties that should only set if not already set (first_touch_source, original_referrer). Use mixpanel.register() for properties that should update each time (current_plan_tier).
Don't put PII in super properties unless intentional — they ride along with everything, including events you might later share or export. Email and phone should live in People Profile properties (mixpanel.people.set), not super properties.
Step 6
Data Management → Lexicon. Add every Tier 1 + Tier 2 event with a description, owner, and tracked-status flag.
In the left sidebar, click Data Management → Lexicon. You'll see every event Mixpanel has received in the last 30 days.
Click each event you care about. Add: a one-sentence description ('Fires when a user completes the signup flow and verifies their email'), a category (Funnel, Activation, Revenue), an owner (PM or Eng lead).
Mark each event as Verified. Verified events show up first in chart-builder pickers and signal to your team 'this is the canonical event'. Unverified events show with a warning.
Use Lexicon to hide deprecated events. When you rename 'signup' to 'signup_completed', mark the old one as Hidden so it doesn't pollute new dashboards.
Also document property types in Lexicon — click a property to see its type (string/number/boolean/etc.) and description. Mismatched types (some events send price as string, some as number) get flagged here.
Step 7
Fire every Tier 1 event from a real test session. Verify in Live View. Build a simple Insights chart for each to confirm aggregation works.
Open Live View (Events → Live View tab). Filter to your test distinct_id.
Fire each Tier 1 event by walking through the real product flow. Confirm each appears in Live View within 60 seconds with all properties populated.
Click each event in Live View to expand. Verify property types (number vs string), null handling, and that no extra unintended properties leaked in.
Build a basic Insights chart for each event: Reports → Insights → +New → pick the event → save. The chart should show counts for the test day. If it shows zero or null, the event isn't being recorded properly even if Live View showed it.
Build a basic funnel: Funnels → +New → pick 3 Tier 1 events in sequence. Even with one test user, the funnel should show that single user moving through. If users disappear between steps, the events aren't tied to the same distinct_id (see user-identification tutorial).
Common mistakes
Firing events with inconsistent naming ("Sign Up" vs "signup_completed" vs "Signup")
What goes wrong: Three engineers ship three variants over six months. Your funnel from 'pricing_viewed' → 'signup_completed' silently misses 30% of users who fired 'Sign Up' instead. Conversion rate looks 1.2% when reality is 1.8%. Six months of CAC/LTV decisions made on broken math.
How to avoid: Pick one naming convention and document it in the spec doc AND Lexicon. Run a one-time `CREATE EVENT FROM EVENT` transformation (Data Management → Events → Transform) to merge the variants. Delete the source events. Add lint rules to your analytics helper to prevent drift.
Pushing numbers as strings
What goes wrong: price_usd is sent as '49.00' (string) instead of 49.00 (number). Mixpanel can't sum or average strings. Your 'Total Revenue This Month' chart shows zero. Your VP of Product asks 'why is the revenue chart broken' in standup — you don't have an answer for two days.
How to avoid: Numbers must be numbers, booleans must be booleans, arrays must be arrays. Wrap your track helper in TypeScript with strict typed property maps. Validate in Live View by clicking each event and checking the property type icon (Σ for number, T for string).
Firing revenue events client-side
What goes wrong: purchase_completed fires from your /thank-you page after Stripe redirect. 12% of users close the tab before redirect or have an ad blocker that strips api.mixpanel.com. Mixpanel revenue is $84K; Stripe revenue is $96K. Your CFO doesn't trust Mixpanel anymore.
How to avoid: Fire revenue events server-side from your Stripe webhook (or Recharge, Paddle, Chargebee webhook). Pass the distinct_id from a metadata field. Mixpanel revenue should match payment processor revenue within 1-2%.
Tracking everything as page views
What goes wrong: Engineering enables `track_pageview: true` and considers instrumentation done. You have 4 million Page Viewed events per month and zero useful funnels because Mixpanel can't tell the difference between 'viewed pricing' and 'clicked Start Free Trial'.
How to avoid: Use page views as a low-tier event for traffic analysis only. For every meaningful action, fire a named event (button_clicked, signup_started, plan_selected). Funnel steps must be explicit named events, not page views.
Property bloat — 40 properties per event
What goes wrong: Engineers attach 'every available variable' to every event 'just in case'. Events become 4KB JSON blobs. You exceed Mixpanel's 25KB per-event limit on some events and they get dropped silently. Your dashboards intermittently miss data.
How to avoid: Cap events at 5-10 properties each. Move user-context properties to People Profile (mixpanel.people.set) where they update once per user, not once per event. Use super properties for cross-event context.
No code-level enforcement of the spec
What goes wrong: The spec doc exists but engineers ignore it under deadline pressure. Six months later, only 60% of your events match the spec. Cleanup is a 4-week project that nobody volunteers to lead.
How to avoid: Build a typed analytics helper (track helper with TypeScript event-name + property union types). Add a PR template question: "Did this match the spec?" Audit Lexicon monthly and flag any event not in the spec doc.
Recap
Done — what's next
How to set up a Mixpanel project from scratch
Read the next tutorial
Hand it off
Event taxonomy is the foundation of every Mixpanel report you'll ever build. Done right once, it pays back for years. Done wrong, it pollutes every dashboard until someone runs the painful migration. A vetted product analytics specialist on EverestX can design + implement + Lexicon-document a 25-event taxonomy in 2-3 weeks for $800-2,000 total at $14-16/hr.
See specialist rates
Events describe what happened (signup_completed, purchase_completed). People Profile properties describe who the user is (email, plan, lifecycle_stage). Events are immutable and timestamped; properties are mutable and represent the current state. Use mixpanel.track for events; mixpanel.people.set for profile.
Most product-led SaaS companies have 25-75 active events. Fewer than 15 means you don't have enough visibility into the product loop. More than 100 usually means there's drift and duplication. Quality over quantity — better to have 30 well-defined events than 200 ambiguous ones.
If you only use Mixpanel for analytics, install the JS SDK directly — fewer moving parts. If you use 3+ tools that need the same event stream (Mixpanel + Customer.io + a data warehouse), Segment (or Rudderstack) is worth the cost. Don't pay for Segment to send events to only Mixpanel.
Partially. Mixpanel's Data Management → Events page has a Transform feature that lets you create new events from existing ones (rename, merge variants). It applies going forward AND to historical data within retention. You cannot truly delete events — only hide them in Lexicon.
Mixpanel charges by Monthly Tracked Users (MTUs), not by event count. An MTU is any unique user (distinct_id) who fires at least one event in a calendar month. You can fire unlimited events per user. The pricing implication: instrumenting more events on the same user is free; tracking more users (via marketing) costs more.
Mixpanel
Spinning up a Mixpanel project takes 20 minutes. Spinning it up so the data is still trustworthy in 18 months — that's the work most teams skip and pay for later in event-renaming sprints and broken dashboards.
Mixpanel
User identification is the single most-broken part of Mixpanel in DIY installs. Anonymous users never merge with logged-in users, retention curves look like 8% when reality is 40%, and every cohort is wrong. Here's the pattern that actually works.
Amplitude
Bad event tracking is the most common reason Amplitude projects fail. Here is the naming convention, the SDK code, and the Data Guard rules that keep your taxonomy clean for years — not weeks.
Google Analytics 4
GA4 renamed Conversions to Key Events in late 2024. The name change matters less than what most owners get wrong: marking too many things as Key Events, which destroys Smart Bidding and inflates every report.