Analytics Testing Guide

How to Test Mixpanel Events with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Mixpanel events with Playwright. Queued event batching, the debug flag, distinct_id tracking, super properties, group analytics, and the silent failures that corrupt real analytics pipelines.

8B+

Mixpanel ingests over eight billion events per month across more than 9,000 paying customers, making silent tracking failures invisible until quarterly reviews surface data gaps.

Mixpanel annual metrics, 2025

0msDefault batch flush interval
0Event scenarios covered
0xDomain transitions in identify
0%Fewer lines with Assrt

Mixpanel Event Lifecycle

BrowserMixpanel JS SDKEvent QueueMixpanel /track APIMixpanel /engage APImixpanel.track('Button Clicked', props)Enqueue event with timestampFlush batch (POST /track)200 OK (1 = success)mixpanel.identify(userId)POST /engage ($set, $distinct_id)200 OKCallback fires (if registered)

1. Why Testing Mixpanel Events Is Harder Than It Looks

Mixpanel's JavaScript SDK does not fire HTTP requests the moment you call mixpanel.track(). Instead, it enqueues events into an internal buffer and flushes them in batches, typically every 50 milliseconds or when the queue reaches a size threshold. This batching behavior means a naive test that checks network requests immediately after a user action will find nothing. The event is sitting in memory, waiting for the next flush cycle.

The complexity extends beyond batching. Mixpanel uses adistinct_id to link events to users, and this identity changes at critical moments. Before login, the SDK generates an anonymous UUID. After you callmixpanel.identify(userId), the SDK merges the anonymous profile with the authenticated one. If your test does not validate that merge, you will not catch the scenario where pre-login events get orphaned on the anonymous profile, creating ghost users in your funnel reports.

Super properties add another layer. These are key/value pairs registered once viamixpanel.register() that get automatically attached to every subsequent track call. They persist in localStorage across page loads. If your SPA clears localStorage during logout but does not re-register super properties after login, every event after the second login will be missing critical context like plan_tier orcompany_name. These bugs only surface weeks later when analysts notice gaps in their breakdown reports.

Group analytics compounds the problem for B2B SaaS applications. Mixpanel lets you associate events with a group (a company, a workspace, a team) by callingmixpanel.set_group(). If the group key is not set before the first tracked event after login, those events cannot be retroactively grouped. The data is lost from group-level reports permanently. Finally, Mixpanel's debug flag (debug: true in init) changes the API endpoint from /track to/track?verbose=1, which returns detailed error messages instead of a bare1. Tests written against debug mode will not catch production payload issues where the response format differs.

Mixpanel Event Pipeline

🌐

User Action

Click, navigate, submit

⚙️

SDK Enqueue

mixpanel.track() buffers event

↪️

Batch Timer

50ms flush interval

🔔

POST /track

Base64 JSON payload

⚙️

Mixpanel API

Ingests and indexes

Reports

Available in ~60s

Identity Resolution Flow

🌐

Anonymous Visit

Auto-generated UUID

⚙️

Events Tracked

Attached to anon ID

🔒

User Signs Up

mixpanel.alias(userId)

User Logs In

mixpanel.identify(userId)

⚙️

Profile Merge

Anon events linked to user

A thorough Mixpanel test suite must intercept queued network requests, decode base64 payloads, validate property schemas, and verify identity resolution across the anonymous-to-authenticated transition. The sections below walk through each scenario with runnable Playwright TypeScript.

2. Setting Up a Reliable Test Environment

The goal is to intercept all Mixpanel requests without actually sending data to the Mixpanel API. You want your tests to be hermetic: no dependency on Mixpanel's servers, no risk of polluting production data, and no flakiness from network latency. The approach is to use Playwright's route interception to capture every request to api-js.mixpanel.com andapi.mixpanel.com, decode the payload, and store it for assertions.

Mixpanel Test Environment Setup Checklist

  • Create a separate Mixpanel project for test/staging environments
  • Set the project token via environment variable, not hardcoded
  • Disable Mixpanel in unit tests (mock the SDK); use route interception for E2E
  • Configure Playwright route handlers to intercept api-js.mixpanel.com/*
  • Write a payload decoder utility for base64-encoded track data
  • Set batch_requests: false in test init for synchronous request testing
  • Create test user accounts with known distinct_id values
  • Ensure localStorage is cleared between test suites to reset super properties

Environment Variables

.env.test

Mixpanel Route Interceptor Utility

This helper intercepts all Mixpanel API calls, decodes the base64 payloads, and collects them into an array your tests can query. It handles both the /track and/engage endpoints.

test/helpers/mixpanel-interceptor.ts

Playwright Configuration

Mixpanel's SDK initializes early in the page lifecycle. Set up route interception before navigating to the page under test, so you capture initialization events like$mp_web_page_view if autotrack is enabled.

playwright.config.ts
Install Dependencies
3

Intercepting a Basic Track Call

Straightforward

3. Scenario: Intercepting a Basic Track Call

Goal: Verify that clicking a button fires a specific Mixpanel event with the correct properties.

Preconditions: The Mixpanel SDK is initialized on the page. The button triggersmixpanel.track('CTA Clicked', { location: 'hero' }).

The key challenge is timing. Because Mixpanel batches events, you cannot check network requests immediately after the click. Instead, use Playwright's waitForRequest or poll the interceptor until the event appears. The interceptor approach is more reliable because it decodes the payload for you.

test/analytics/basic-track.spec.ts

What to assert beyond the UI: Verify the event name is exact (Mixpanel is case-sensitive, so"CTA Clicked" and"cta_clicked" create two separate events in your project). Check that thetoken property matches your test project token. Confirm default properties like$browser,$os, andmp_lib are present, which proves the SDK initialized correctly rather than a custom fetch call.

Basic Track: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { MixpanelInterceptor } from '../helpers/mixpanel-interceptor';

test('CTA click fires Mixpanel event', async ({ page }) => {
  const mp = new MixpanelInterceptor();
  await mp.attach(page);
  await page.goto('/');
  await page.getByRole('button', { name: 'Get Started' }).click();
  await page.waitForTimeout(200);
  const events = mp.findEvents('CTA Clicked');
  expect(events).toHaveLength(1);
  expect(events[0].properties).toMatchObject({
    location: 'hero',
  });
});
62% fewer lines
4

Validating the Identify and Alias Flow

Complex

4. Scenario: Validating the Identify and Alias Flow

Goal: Confirm that the anonymous-to-authenticated identity transition correctly merges user profiles. Pre-login events must be retroactively linked to the authenticated user.

Preconditions: A fresh browser context with no existing Mixpanel cookies. The application callsmixpanel.identify(userId) after successful login.

Mixpanel's identity management is the single most common source of analytics data corruption. When a new visitor lands on your site, the SDK assigns a random$device_id and uses it as the distinct_id. After login, your code calls identify() with the real user ID. The SDK then sends a$identify event to Mixpanel that tells the server to merge the anonymous profile into the authenticated one. If you accidentally callidentify() before any events are tracked, or call it with the wrong ID, the merge fails silently and you get duplicate user profiles.

test/analytics/identify-flow.spec.ts

What to assert beyond the UI: Check the$identifyevent's$anon_distinct_id matches the anonymous ID from pre-login events. Verify that all post-login events carry the authenticateddistinct_id. Confirm that $device_id persists across the identity transition (it should remain the original anonymous UUID even after identify).

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started
5

Testing Super Properties Across Pages

Moderate

5. Scenario: Testing Super Properties Across Pages

Goal: Verify that super properties registered after login persist across page navigations and appear on every subsequent track call.

Preconditions: After login, the application callsmixpanel.register({ plan_tier: 'pro', company_name: 'Acme Inc' }). Super properties are stored in localStorage under the keymp_<token>_mixpanel.

Super properties are powerful because they automatically append to every track() call without the developer needing to pass them explicitly. But they are also fragile. If your application clears localStorage during logout (a common security practice), the super properties vanish. If the next login does not re-register them before any event is tracked, those events go out without critical segmentation data.

test/analytics/super-properties.spec.ts

What to assert beyond the UI: After each navigation, verify super properties appear in every tracked event. After a hard reload, confirm localStorage still contains themp_<token>_mixpanelkey with the registered properties. After logout and re-login, verify that super properties are re-registered before the first post-login event fires.

Super Properties: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { MixpanelInterceptor } from '../helpers/mixpanel-interceptor';

test('super properties persist across pages', async ({ page }) => {
  const mp = new MixpanelInterceptor();
  await mp.attach(page);
  await page.goto('/login');
  await page.getByLabel('Email').fill('e2e-test@yourapp.com');
  await page.getByLabel('Password').fill('SuperSecret123!');
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.waitForURL('/dashboard');
  await page.getByRole('link', { name: 'Settings' }).click();
  await page.getByRole('button', { name: 'Save Changes' }).click();
  await page.waitForTimeout(200);
  const events = mp.findEvents('Settings Updated');
  expect(events[0].properties).toMatchObject({
    plan_tier: 'pro',
    company_name: 'Acme Inc',
  });
});
61% fewer lines
6

Group Analytics for B2B SaaS

Complex

6. Scenario: Group Analytics for B2B SaaS

Goal: Verify that events are correctly associated with a Mixpanel group (company, workspace, or team) so B2B reports aggregate data at the account level.

Preconditions: Your Mixpanel project has a group key called company_idconfigured in Project Settings. After login, the application callsmixpanel.set_group('company_id', 'acme_123').

Group analytics is essential for B2B SaaS products where a single company has multiple users. Mixpanel lets you define up to five group keys per project. When you callset_group(), the SDK adds the group key/value as a property on every subsequent event. The critical testing requirement is timing: the group must be set before any post-login events fire. If your application initializes Mixpanel, tracks a "Dashboard Loaded" event, and then calls set_group(), that first event will be missing the group association.

test/analytics/group-analytics.spec.ts

What to assert beyond the UI: Every post-login event must contain the company_idproperty. Verify the /engageendpoint receives a $group_setcall with the correct group profile properties. When a user switches workspaces, confirm the group key updates and subsequent events reflect the new group.

7

Asserting Queued Batch Flushes

Complex

7. Scenario: Asserting Queued Batch Flushes

Goal: Verify that multiple rapid events are batched into a single network request, and that all events in the batch have correct payloads.

Preconditions: Mixpanel SDK initialized with default batching (not batch_requests: false). The page has multiple interactive elements that fire distinct events.

Mixpanel's batching behavior is the primary source of test flakiness. When batch_requests is true (the default), the SDK collects events in memory and sends them in a single POST request when the batch interval (50ms) elapses or the queue hits a size limit. In production, this is efficient. In tests, it means you cannot assert on individual network requests per event. You need to either wait for the batch flush and then inspect the full payload, or disable batching entirely in your test configuration.

test/analytics/batch-flush.spec.ts

What to assert beyond the UI: Confirm that the event count in the interceptor matches the number of user actions. Check that timestamps increase monotonically. Verify that thesendBeacon fallback fires on page unload by navigating away immediately after an action and checking that the event was still captured.

Batch Flush: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { MixpanelInterceptor } from '../helpers/mixpanel-interceptor';

test('rapid events batched correctly', async ({ page }) => {
  const mp = new MixpanelInterceptor();
  await mp.attach(page);
  await page.goto('/onboarding');
  await page.getByRole('button', { name: 'Next' }).click();
  await page.getByRole('button', { name: 'Next' }).click();
  await page.getByRole('checkbox', { name: 'Enable notifications' }).check();
  await page.getByRole('button', { name: 'Complete Setup' }).click();
  await page.waitForTimeout(500);
  const stepEvents = mp.findEvents('Onboarding Step Completed');
  expect(stepEvents.length).toBeGreaterThanOrEqual(2);
  const completeEvent = mp.findEvents('Onboarding Completed');
  expect(completeEvent).toHaveLength(1);
});
56% fewer lines
8

Using Debug Mode and Verbose Logging

Moderate

8. Scenario: Using Debug Mode and Verbose Logging

Goal:Verify that Mixpanel's debug mode outputs detailed request/response information and that your test environment configuration matches production behavior.

Preconditions: Mixpanel initialized withdebug: true in the test environment. The page triggers at least one track call.

When debug: true is set in the Mixpanel init call, the SDK appends?verbose=1 to API endpoints and logs detailed information to the browser console. The verbose response format changes from a bare1 (success) or0 (failure) to a JSON object with status,error, andnum_records_importedfields. This is invaluable during development but creates a divergence: tests written against debug mode may pass while production silently drops events.

test/analytics/debug-mode.spec.ts
Debug Mode Console Output

What to assert beyond the UI:Capture console messages using Playwright'spage.on('console')listener and verify that [Mixpanel]logs contain the event name and properties. In a separate test with debug mode disabled, confirm that the verbose parameter is absent and the response format is the bare1. This ensures your test environment does not mask production issues.

9. Common Pitfalls That Silently Corrupt Your Analytics

Mixpanel tracking bugs are uniquely insidious because they do not crash your application or fail visibly. The UI works perfectly while the underlying analytics data drifts further from reality. These are the most common failures reported in Mixpanel's community forums and GitHub issues.

Common Mixpanel Testing Pitfalls

  • Calling identify() before any events are tracked, causing the $identify merge to have no anonymous events to link
  • Using inconsistent event names (case-sensitive): 'Button Clicked' vs 'button_clicked' creates duplicate events in reports
  • Clearing localStorage on logout without re-registering super properties on next login
  • Not setting group key before the first post-login event, permanently orphaning that event from group reports
  • Testing only in debug mode where verbose responses mask silent failures in production
  • Asserting on network requests immediately after track() instead of waiting for batch flush
  • Hardcoding distinct_id in tests instead of reading it from the SDK cookie, causing false passes
  • Forgetting that mixpanel.reset() clears both distinct_id and super properties, breaking tests that assume identity persists
  • Not testing the sendBeacon fallback on page unload, which uses a different payload encoding
  • Ignoring $mp_api_endpoint property that changes between api-js.mixpanel.com and a custom proxy

Pitfall Deep Dive: The Identify Before Track Problem

This is the number one cause of duplicate user profiles in Mixpanel. The SDK's identity merge depends on having anonymous events to link. If your application's initialization code callsmixpanel.identify(userId)in a useEffect that runs before any page view or interaction events are tracked, the$identify event has no$anon_distinct_id to merge. The result: a clean authenticated profile with zero pre-login funnel data, and an orphaned anonymous profile that inflates your unique user count.

Detecting Identity Merge Failures

Pitfall Deep Dive: Super Property Amnesia After Logout

Many applications call localStorage.clear()or mixpanel.reset() during logout to clean up user state. Both of these operations destroy the super properties stored by the Mixpanel SDK. Thereset() call is particularly aggressive: it generates a new anonymousdistinct_id, clears all super properties, and removes the Mixpanel cookie. If your application logs back in and does not callregister() again, every event in the new session will be missingplan_tier,company_name, and any other super properties your analysts depend on.

test/analytics/super-property-logout.spec.ts

10. Writing These Scenarios in Plain English with Assrt

The Playwright tests above are thorough but verbose. Each scenario requires importing the interceptor, attaching it, managing timeouts, and writing multiple assertions against decoded payloads. With Assrt, you describe the same scenario in plain English and let the engine handle the Mixpanel request interception, payload decoding, and property validation automatically.

Here is the identity merge scenario from Section 4 expressed as an Assrt test file. Notice how the intent is immediately clear to anyone on the team, including product managers and analysts who need to verify that tracking matches their event taxonomy.

tests/mixpanel-identify-merge.assrt

The Assrt file is 24 lines compared to 40+ lines of TypeScript for the equivalent Playwright test. More importantly, it reads like a specification document. When your product team updates the event taxonomy (renaming "Project Created" to "Project Initialized", for example), anyone can update the Assrt file without needing to understand route interception or base64 decoding.

Assrt handles the hard parts automatically: attaching the route interceptor before page load, waiting for batch flushes, decoding base64 payloads, and matching properties with flexible assertions. You focus on what to verify, not how to verify it.

Full Identity Merge: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { MixpanelInterceptor } from '../helpers/mixpanel-interceptor';

test('identify merges anonymous events', async ({ page }) => {
  const mp = new MixpanelInterceptor();
  await mp.attach(page);
  await page.goto('/');
  await page.getByRole('link', { name: 'Features' }).click();
  await page.waitForTimeout(200);
  const anonEvents = mp.findEvents('$mp_web_page_view');
  const anonId = anonEvents[0].properties.distinct_id;
  await page.goto('/login');
  await page.getByLabel('Email').fill('e2e-test@yourapp.com');
  await page.getByLabel('Password').fill('SuperSecret123!');
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.waitForURL('/dashboard');
  await page.waitForTimeout(500);
  const identifyEvents = mp.findEvents('$identify');
  expect(identifyEvents[0].properties.$anon_distinct_id).toBe(anonId);
  expect(identifyEvents[0].properties.$identified_id).toBe('user_12345');
  await page.getByRole('button', { name: 'Create Project' }).click();
  await page.waitForTimeout(200);
  const events = mp.findEvents('Project Created');
  expect(events[0].properties.distinct_id).toBe('user_12345');
  expect(events[0].properties.plan_tier).toBe('pro');
});
20% fewer lines

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk