airbridge-mixpanel-explainer

v1v2 · +105 −27 · View latest

⋯ 606 unchanged lines
<header class="masthead">
  <div class="masthead-inner">
    <p class="kicker">Developer guide · v1.0 · Interactive</p>
    <p class="kicker">Developer guide · v1.1 (Chief PM review) · Interactive</p>
    <h1 class="title">AirBridge → Mixpanel Instrumentation Mapping</h1>
    <div class="meta-row">
      <span class="badge">Owner <b>Sourabh Mishra</b> (MMRP)</span>
⋯ 6 unchanged lines
      <span class="badge">Staging <b>4017466</b></span>
      <span class="badge">Dev <b>4002103</b></span>
      <span class="badge warn">Prod is behind — see §Naming</span>
      <span class="badge warn">v1.1 corrected 3 contract drifts — see below</span>
    </div>
  </div>
</header>
<nav class="jumpnav" aria-label="Section navigation">
  <div class="jumpnav-inner">
    <a href="#whatchanged">v1.1 changes</a>
    <a href="#tryit">Try it</a>
    <a href="#chain">The chain</a>
    <a href="#timing">Timing rules</a>
⋯ 8 unchanged lines
<main>
  <section id="whatchanged">
    <p class="section-label">Chief PM review — verified against committed enums v1.3.1</p>
    <h2>What changed in v1.1</h2>
    <p class="lede">v1.0 asserted things about the committed enums file that turned out not to be true. This version is corrected against the actual file, not asserted.</p>
    <div class="pin-list">
      <div class="pin">
        <p><strong>Fixed a fabricated claim.</strong> v1.0 said the <code>acquisition_source</code> rollup "lives in <code>mixxi_attribution_enums.json</code>." It doesn't — no such group exists in the committed file. Now an explicit open pin with a safe interim (write the raw <code>channel</code> value).</p>
      </div>
      <div class="pin">
        <p><strong>Fixed <code>funding</code> values.</strong> <code>organic</code>/<code>paid</code> → <strong><code>organic</code>/<code>sponsorship_funded</code></strong> — the only two values that exist in the enum.</p>
      </div>
      <div class="pin">
        <p><strong>Fixed event-prop names to the committed <code>param_map</code>.</strong> <code>persona</code>/<code>funding_type</code>/<code>distribution_layer</code>/<code>owner_team</code>/<code>cta</code> → <strong><code>mx_persona</code>/<code>mx_funding</code>/<code>mx_layer</code>/<code>mx_team</code>/<code>mx_cta</code></strong>. MMRP's dashboards query the <code>mx_*</code> names — the v1.0 names would land in Mixpanel but be invisible to every MMRP surface.</p>
      </div>
      <div class="pin">
        <p><strong>Added the missing <code>sub_param_ad_platform</code> → <code>mx_ad_platform</code> row</strong> (paid links only — present when <code>funding=sponsorship_funded</code>, omitted for organic).</p>
      </div>
      <div class="pin">
        <p><strong>Updated the <code>vote_cast</code> pin</strong> with Unravel ownership and the preferred server-side-emit resolution; added a new pin on the <code>signup_complete</code> payout re-point.</p>
      </div>
    </div>
  </section>
  <section id="tryit">
    <p class="section-label">Interactive — §3</p>
    <h2>Click any link parameter to see where it lands</h2>
    <p class="lede">This is a real example AirBridge link. Every parameter maps to something in Mixpanel — or explicitly doesn't. Click one to see the rule.</p>
    <div class="try-panel">
      <div class="try-header">
        <h3>Example link</h3>
        <div class="scenario-toggle" role="group" aria-label="Link type">
          <button type="button" data-scenario="affiliate" class="active">Affiliate link</button>
          <button type="button" data-scenario="marketing">Marketing link</button>
        <div style="display:flex; gap:10px; flex-wrap:wrap;">
          <div class="scenario-toggle" role="group" aria-label="Link type">
            <button type="button" data-scenario="affiliate" class="active">Affiliate link</button>
            <button type="button" data-scenario="marketing">Marketing link</button>
          </div>
          <div class="scenario-toggle" role="group" aria-label="Funding">
            <button type="button" data-funding="organic" class="active">Organic</button>
            <button type="button" data-funding="sponsorship_funded">Sponsorship-funded</button>
          </div>
        </div>
      </div>
⋯ 13 unchanged lines
      </div>
      <div class="detail" id="detail-panel"></div>
      <div style="padding: 0 22px 22px;">
        <div class="card" style="background: var(--warn-soft); border-color: transparent;">
          <h3 style="color: var(--warn); margin-bottom:6px;">Property-naming rule — do not drift</h3>
          <p style="margin:0; font-size:0.88rem;">All optional link-derived event props use the <strong>exact <code>mx_*</code> names</strong> from the <code>param_map</code> in <code>mixxi_attribution_enums.json</code>: <code>mx_channel</code>, <code>mx_placement</code>, <code>mx_phase</code>, <code>mx_episode</code>, <code>mx_recipe</code>, <code>mx_asset</code>, <code>mx_cta</code>, <code>mx_team</code>, <code>mx_funding</code>, <code>mx_ad_platform</code>, <code>mx_persona</code>, <code>mx_layer</code>, <code>mx_aff</code>. 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 three <strong>profile</strong> keys keep their own names: <code>acquisition_source</code>, <code>marketing_affiliate_id</code>, <code>airbridge_id</code>.</p>
        </div>
      </div>
    </div>
  </section>
⋯ 175 unchanged lines
    <h2>Open pins</h2>
    <div class="pin-list">
      <div class="pin">
        <span class="pin-tag">New in v1.1 — Owner: Sourabh/MMRP, ships as enums v1.4.0</span>
        <p><strong>Pin #0 — <code>channel → acquisition_source</code> rollup enum.</strong> Does not yet exist in the committed enums file. Until it ships: write the raw <code>channel</code> value into <code>acquisition_source</code> (<code>$set_once</code>, null→<code>unknown</code>) rather than guessing a coarse mapping — a wrong value written <code>$set_once</code> is permanent.</p>
      </div>
      <div class="pin">
        <span class="pin-tag">Pending — Punit (template owner)</span>
        <p><strong>Affiliate-code postback field — <code>partner</code> vs <code>sub_id</code>.</strong> Which AirBridge field the tracking template forwards to the MMRP receiver is console-defined and unconfirmed. Build against <code>campaign</code> as the in-app source of <code>marketing_affiliate_id</code> (that's stable); treat the postback field name as unconfirmed. Recommendation on record: forward as a dedicated <code>sub_id</code>, not <code>partner</code>.</p>
      </div>
      <div class="pin">
        <span class="pin-tag">In progress — parallel workstream</span>
        <p><strong>Branded domain <code>go.mixxi.ai</code>.</strong> Being stood up in parallel. Does not change the affiliate-code parameter contract. Deferred-deep-link attribution works regardless of domain; Universal Link / App Link direct-open on the new domain is a separate, freeze-gated entitlement change.</p>
        <p><strong>Branded domain <code>go.mixxi.ai</code>.</strong> Being stood up in parallel (CNAME pending DNS). Does not change the affiliate-code parameter contract. Deferred-deep-link attribution works regardless of domain; Universal Link / App Link direct-open on the new domain is a separate, freeze-gated entitlement change.</p>
      </div>
      <div class="pin">
        <span class="pin-tag">Unresolved</span>
        <p><strong><code>vote_cast</code> source</strong> — client Mixpanel vs. server-side Kafka <code>rising.vote.validated</code> — unresolved. Do not build <code>vote_cast</code> client-side until settled.</p>
        <span class="pin-tag">Updated in v1.1 — ownership: Unravel</span>
        <p><strong><code>vote_cast</code> source</strong> — client Mixpanel vs. server-side emit from the Kafka <code>rising.vote.validated</code> stream. The Kafka topic is now Unravel's (transferred with the Void gap). Preferred resolution on record: <strong>Unravel's backend consumes the validated stream and emits <code>vote_cast</code> server-side into Mixpanel</strong> (keeps votes trusted for payout release and keeps MMRP Mixpanel-only). Do not build <code>vote_cast</code> client-side until settled.</p>
      </div>
      <div class="pin">
        <span class="pin-tag">New in v1.1 — MMRP action item</span>
        <p><strong>Pin #4 — <code>signup_complete</code> as the payout event.</strong> This guide standardises on <code>signup_complete</code> (the only signup event that exists in any environment). MAP payout logic historically referenced <code>account_created</code>; re-pointing it to <code>signup_complete</code> is an MMRP action item, tracked separately. App-side: emit <code>signup_complete</code>, never <code>account_created</code>.</p>
      </div>
    </div>
  </section>
</main>
<footer class="provenance">
  Source: AirBridge → Mixpanel Mapping Guide v1.0 · 22 Jul 2026 · Ingestion &amp; Experimentation, MMRP · route Sourabh → Kapil → Prajwalit · single sink Mixpanel 4017469 · Confidential — Celerity Studios.
  Full source document stored in the Celerity org brain at <code>meetings/inbox/2026-07-22-airbridge-mixpanel-mapping-guide.md</code> (<code>status: proposed</code>).
  Source: AirBridge → Mixpanel Mapping Guide v1.1 (Chief PM review) · 22 Jul 2026 · Ingestion &amp; Experimentation, MMRP · route Sourabh → Kapil → Prajwalit · single sink Mixpanel 4017469 · Confidential — Celerity Studios.
  Full source document stored in the Celerity org brain at <code>meetings/inbox/2026-07-22-airbridge-mixpanel-mapping-guide.md</code> (<code>status: proposed</code>), updated in place from v1.0 to v1.1.
</footer>
<script>
(function () {
  var MAPPED = {
    channel: {
      example: "whatsapp",
      target: "acquisition_source",
      via: "enum rollup",
      via: "enum rollup — PROPOSED, pending commit",
      setWhere: "profile · $set_once",
      rule: "Roll the channel up to the coarse acquisition_source enum (organic · direct · paid_meta · paid_google · influencer · referral_research · referral_marketing · unknown). Keep the raw channel as an event prop if richer segmentation is wanted. Rollup table lives in lib/attribution/mixxi_attribution_enums.json — confirm against it, don't hardcode a guess."
      rule: "⚠️ v1.1 correction: this rollup does NOT yet exist in lib/attribution/mixxi_attribution_enums.json (v1.3.1 committed) — v1.0 wrongly claimed it did. It's Pin #0: don't hardcode a guessed channel→acquisition_source mapping. Safe interim: write the raw channel value into acquisition_source verbatim ($set_once, null→unknown) — it can be rolled up later server-side, but a wrong coarse value written $set_once is permanent."
    },
    sub_param_persona: {
      example: "style_aspirant",
      target: "persona",
      target: "mx_persona",
      setWhere: "event prop · optional",
      rule: "Lowercase enum. Carried straight through as an event property."
      rule: "Lowercase enum. v1.1: renamed from persona to mx_persona to match the committed param_map — MMRP's dashboards only query the mx_* name."
    },
    sub_param_funding: {
      example: "organic",
      target: "funding_type",
      target: "mx_funding",
      setWhere: "event prop · optional",
      rule: "Lowercase enum (organic / paid)."
      rule: "v1.1 correction: only two values exist — organic | sponsorship_funded. There is no \"paid\" value in the enum (v1.0 said organic/paid). Also renamed funding_type → mx_funding."
    },
    sub_param_ad_platform: {
      example: "meta",
      target: "mx_ad_platform",
      setWhere: "event prop · paid links only",
      rule: "New in v1.1 — was missing from v1.0 entirely. Present only when funding=sponsorship_funded; omit for organic links. Toggle “Funding” above to see it appear/disappear."
    },
    sub_param_layer: {
      example: "deep_link",
      target: "distribution_layer",
      target: "mx_layer",
      setWhere: "event prop · optional",
      rule: "Three-layer model: feed / deep_link / mobile_web."
      rule: "Three-layer model: feed / deep_link / mobile_web. v1.1: renamed from distribution_layer to mx_layer."
    },
    sub_param_team: {
      example: "delhi_gtm",
      target: "owner_team",
      target: "mx_team",
      setWhere: "event prop · optional",
      rule: "Lowercase enum identifying the owning GTM team."
      rule: "Lowercase enum identifying the owning GTM team. v1.1: renamed from owner_team to mx_team."
    },
    sub_param_cta: {
      example: "app_install",
      target: "cta",
      target: "mx_cta",
      setWhere: "event prop · optional",
      rule: "Lowercase enum describing the call-to-action that drove the tap."
      rule: "Lowercase enum describing the call-to-action that drove the tap. v1.1: renamed from cta to mx_cta."
    },
    og_tag_id: { example: "og_a91f", ignored: true },
    routing_short_id: { example: "rt_3382", ignored: true },
⋯ 49 unchanged lines
    }
  };
  var state = { scenario: "affiliate", selected: "channel" };
  var state = { scenario: "affiliate", funding: "organic", selected: "channel" };
  var chipRow = document.getElementById("chip-row");
  var chipRowExtra = document.getElementById("chip-row-extra");
  var detailPanel = document.getElementById("detail-panel");
  function paramData(key) {
    if (key === "campaign") return SCENARIOS[state.scenario].campaign;
    if (key === "deeplink") return SCENARIOS[state.scenario].deeplink;
    if (key === "airbridge_id") return EXTRA.airbridge_id;
    if (key === "sub_param_ad_platform") {
      var base = MAPPED.sub_param_ad_platform;
      if (state.funding === "organic") {
        return {
          example: "(omitted)",
          ignored: true,
          toLabel: "Omitted — organic link",
          rule: "Omitted for organic links — mx_ad_platform is only present when funding=sponsorship_funded. Toggle “Funding” to sponsorship-funded to see it appear."
        };
      }
      return base;
    }
    if (key === "sub_param_funding") {
      var f = MAPPED.sub_param_funding;
      return Object.assign({}, f, { example: state.funding });
    }
    return MAPPED[key];
  }
⋯ 13 unchanged lines
    chipRow.innerHTML = "";
    chipRow.appendChild(makeChip("channel"));
    chipRow.appendChild(makeChip("campaign"));
    ["sub_param_persona", "sub_param_funding", "sub_param_layer", "sub_param_team", "sub_param_cta"].forEach(function (k) {
    ["sub_param_persona", "sub_param_funding", "sub_param_ad_platform", "sub_param_layer", "sub_param_team", "sub_param_cta"].forEach(function (k) {
      chipRow.appendChild(makeChip(k));
    });
    chipRow.appendChild(makeChip("og_tag_id"));
⋯ 21 unchanged lines
    if (d.ignored) {
      detailPanel.innerHTML =
        '<div class="detail-flow"><span class="from mono">' + key + '</span><span class="arrow">→</span><span class="to ignored-text">Not forwarded to Mixpanel</span></div>' +
        '<p class="rule">AirBridge-generated, debug-only. Ignore in instrumentation — do not read or forward this field.</p>';
        '<div class="detail-flow"><span class="from mono">' + key + '</span><span class="arrow">→</span><span class="to ignored-text">' + (d.toLabel || "Not forwarded to Mixpanel") + '</span></div>' +
        '<p class="rule">' + (d.rule || "AirBridge-generated, debug-only. Ignore in instrumentation — do not read or forward this field.") + '</p>';
      return;
    }
⋯ 13 unchanged lines
    renderDetail();
  }
  document.querySelectorAll(".scenario-toggle button").forEach(function (btn) {
  document.querySelectorAll(".scenario-toggle button[data-scenario]").forEach(function (btn) {
    btn.addEventListener("click", function () {
      document.querySelectorAll(".scenario-toggle button").forEach(function (b) { b.classList.remove("active"); });
      document.querySelectorAll(".scenario-toggle button[data-scenario]").forEach(function (b) { b.classList.remove("active"); });
      btn.classList.add("active");
      state.scenario = btn.getAttribute("data-scenario");
      renderChips();
      renderDetail();
    });
  });
  document.querySelectorAll(".scenario-toggle button[data-funding]").forEach(function (btn) {
    btn.addEventListener("click", function () {
      document.querySelectorAll(".scenario-toggle button[data-funding]").forEach(function (b) { b.classList.remove("active"); });
      btn.classList.add("active");
      state.funding = btn.getAttribute("data-funding");
      renderChips();
      renderDetail();
    });
⋯ 7 unchanged lines