Developer guide · v1.3 · Interactive

AirBridge → Mixpanel Instrumentation Mapping

Owner Sourabh Mishra (MMRP) Route Sourabh → Kapil → Prajwalit Audience iOS + Android (Unravel / Prajwalit, Punit)
Sink: Mixpanel only Prod 4017469 Staging 4017466 Dev 4002103 Episode 0 / Discovery · 31 Jul Enums v1.4.0 · file wins over prose

The raw channel moved to its own field

Earlier circulation wrote a raw channel into acquisition_source. Do not do that. The raw, lossless value belongs in acquisition_channel; acquisition_source may contain only an enum from the committed v1.4.0 rollup.

acquisition_channel

Raw first-touch source such as instagram or whatsapp. Write with $set_once.

acquisition_source

Canonical enum bucket only. Never a raw channel string. Write with $set_once.

marketing_affiliate_id

Affiliate code from campaign, for affiliate links only. MAP join key.

Click any link parameter to see where it lands

This is a real example AirBridge link. Every parameter maps to something in Mixpanel — or explicitly doesn't. Click one to see the rule.

Example link

Also part of the mapping — not URL query params
Maps to Mixpanel Not forwarded — debug only

Property-naming rule — do not drift

All optional link-derived event props use the exact mx_* names from the param_map in mixxi_attribution_enums.json: mx_channel, mx_placement, mx_phase, mx_episode, mx_recipe, mx_asset, mx_cta, mx_team, mx_funding, mx_ad_platform, mx_persona, mx_layer, mx_aff. MMRP's Channels tab and readout query these exact names — a prop written under any other name will exist in Mixpanel but be invisible to every MMRP surface. The profile keys are acquisition_channel, acquisition_source, marketing_affiliate_id, and airbridge_id. acquisition_source is enum-only.

Touch semantics: profile vs. super props

The rule that's easy to get backwards: profile fields never change after first touch, but the mx_* super props update on every new tagged open — and organic opens don't clear them. Step through three sessions for the same user to see it happen.

Play through: one user, three sessions

The chain — what happens on a click

The app owns everything inside the device. The server-side join is MMRP's — but it only works if the app sets the join keys, which is why this is a developer spec.

User
Taps a go.mixxi.ai / abr.ge link (carries channel, campaign, sub-params).
AirBridge → OS
Redirect + store fallback, then the OS deep-link resolver opens the app (or triggers install → first launch).
App — consent gate
DPDP consent gate. Nothing fires until affirmative consent.
App — SDK init order
Mixpanel initializes first (distinct_id ready), then AirBridge SDK initializes.
AirBridge → App
Deferred deep-link params on first launch, or immediate params if already installed.
App → Mixpanel
Sets acquisition_channel (raw) and acquisition_source (enum) with $set_once, plus marketing_affiliate_id and airbridge_id.
App → Mixpanel
Tracks signup_complete / live_watch_anchor with join props attached.
AirBridge → MMRP
Server postback carries deterministic attribution data.
MMRP
Joins the postback to Mixpanel via airbridge_id / marketing_affiliate_id.

Identity & timing model

Four rules, in this order. Breaking the order causes silent attribution loss — no error, just a missing join.

  1. Consent gate is absolute (DPDP). No SDK initializes, no storage is written, and no event fires before affirmative consent. Deny-by-default.

  2. Mixpanel initializes before attribution work. A distinct_id must exist. Order: consent → Mixpanel init → AirBridge init → read link → set props.

  3. Handle deferred deep links. New installs receive params through AirBridge's deferred callback; existing installs use the immediate handler. Handle both.

  4. Profile attribution is first-touch immutable. acquisition_channel, acquisition_source, and affiliate credit use $set_once. A wrong enum can never be corrected for that user.

Step-by-step (app side)

Use the real SDK method names per platform. The order and property names are the contract.

// Phase 1: before code-freeze, write raw channel only to acquisition_channel.
if (!userHasConsented()) return;

mixpanel.init(MIXPANEL_TOKEN);
mixpanel.identify(stableDistinctId());
airbridge.init(AIRBRIDGE_APP_TOKEN);

airbridge.onDeferredDeeplink(params => applyAttribution(params));
airbridge.onDeeplink(params => applyAttribution(params));

function applyAttribution(params) {
  mixpanel.people.setOnce({ acquisition_channel: params.channel });
  // Phase 2: also set acquisition_source from the v1.4.0 JSON rollup.
  // Never write a raw channel into acquisition_source.
  mixpanel.people.set({ airbridge_id: airbridge.getAirbridgeId() });
  if (isAffiliate(params)) {
    mixpanel.people.setOnce({ marketing_affiliate_id: params.campaign });
  }
  mixpanel.register({
    mx_channel: params.channel, mx_phase, mx_episode, mx_recipe, mx_asset,
    mx_placement, mx_funding, mx_ad_platform, mx_persona, mx_layer,
    mx_team, mx_cta, ...(isAffiliate(params) && { mx_aff: params.campaign })
  }); // overwrite on tagged opens; omit absent params
}

mixpanel.track("signup_complete", { new_otp_unique_account: <bool> });
// NOT account_created

Phase 1 → Phase 2

  • Phase 1: raw channel → acquisition_channel; leave acquisition_source unset.
  • Phase 2: write the enum rollup from JSON v1.4.0; do not hardcode it.
  • Both: validate on fresh devices, Dev first, then Staging and Production.

Server-side join — why the app props matter

The AirBridge server postback hits MMRP at /api/ingestion/airbridge/*. MMRP joins that postback to the Mixpanel funnel using airbridge_id (and marketing_affiliate_id for affiliate credit). The join is deterministic-only — if the app didn't set these keys, the postback and the funnel can't be joined and the install looks organic.

Affiliate credit follows a no-clawback ledger: money is only released on met conditions, never reversed — so a missing join key means an affiliate is silently underpaid. That's why §2/§3 are strict.

Naming & parity

DPDP hard constraints

Verify — one decisive end-to-end test

Use a clean device (no prior install), per platform and per environment. Events are not backfillable.

  1. Test an affiliate link with RISING-E0-TESTONLY, and separately a marketing link: instagram / phase1_tease__ep01_discovery / trend__testasset_01 / funding=organic.
  2. Tap → install → consent → sign up → open an episode.
  3. Confirm signup_complete, a second event with full mx_*, raw acquisition_channel, enum-only acquisition_source, and airbridge_id.
  4. Affiliate case: confirm exact marketing_affiliate_id and the AirBridge postback join.
  5. Tap a second tagged link. Its event context must change while first-touch profile keys remain unchanged.

Launch check

Production is behind. Verify all three environments before Episode 0 / Discovery airs on 31 July 2026.

Open pins

v1.3 · contract v1.4.0

Rollup-in-JSON. Confirm the v1.4.0 file encodes the rollup ruleset as data, not only enum members. The app team must be on v1.4.0, not v1.0.0.

Pending — Punit

Affiliate-code postback field — partner vs sub_id. Console-defined and unconfirmed. campaign remains the stable in-app source; recommendation is a dedicated sub_id.

Freeze-gated

go.mixxi.ai. Cosmetic for deferred attribution; direct Universal/App Links are a separate entitlement change.

Unravel backend

vote_cast. Do not build client-side. Preferred path: Unravel backend emits it from validated Kafka votes.

Changelog — what changed

v1.3 supersedes v1.0–v1.2 and resolves the old rollup guidance against the committed v1.4.0 contract.

Rollup corrected. Affiliate presence wins; paid traffic keys off ad_platform, not channel. Funding is only organic or sponsorship_funded.

Two profile props. Write raw acquisition_channel and enum acquisition_source, both first-touch and $set_once.

Code-freeze sequencing added. Phase 1 writes raw-only to acquisition_channel; Phase 2 adds the JSON-driven enum rollup.

Launch context corrected. Episode 0 / Discovery airs 31 July. Verify in Production because events are not backfillable.

Made with ❤️ by unravel
Public — anyone with the link