Integrate mParticle with Optimizely Feature Experimentation

Loading...

Integrate mParticle with Optimizely Feature Experimentation

Optimizely Feature Experimentation uses SDK-based feature flags rather than a client-side snippet. Integrating with mParticle sends experiment decision data into your Customer Data Platform, where it can be forwarded to downstream analytics tools, used for audience segmentation, and combined with other customer events for cross-platform analysis.

Official mParticle kits exist for Android and iOS. For JavaScript, React, and React Native, no official kit is available — this guide provides custom implementations for each. Server-side SDKs (Node.js, Python, Java, Go) cannot integrate with mParticle directly from the server, but a hybrid workaround passes decisions to the client for forwarding.

How the Integration Works

When the Optimizely Feature Experimentation SDK evaluates a feature flag using the decide() method, it fires a DECISION notification. You register a listener for this notification that captures the flag key, variation key, and experiment details, then sends the data to mParticle as a custom event using mParticle.logEvent(). mParticle then forwards this event to any connected outputs — analytics tools, data warehouses, engagement platforms — with the experiment context attached.

flowchart LR
    A["Optimizely SDK initialized"] --> B["Register DECISION listener"]
    B --> C["user.decide('flag_key')"]
    C --> D["DECISION notification fires"]
    D --> E["Listener extracts decision data"]
    E --> F["mParticle.logEvent('Experiment Viewed')"]
    F --> G["mParticle Live Stream"]
    G --> H["Downstream tools"]

Platform Support Matrix

Not all Optimizely SDKs have official mParticle integration kits. The following table shows the current support status:

SDK

Support Level

Integration Method

Android

Official Kit

com.mparticle:android-optimizely-kit:5+

iOS

Official Kit

mParticle-Optimizely (CocoaPods / Carthage / SPM)

JavaScript SDK v6+

No Official Kit

Custom DECISION listener implementation

React SDK

No Official Kit

Custom module-level listener implementation

React Native SDK

No Official Kit

Custom listener with native bridge

Node.js

Not Supported

Hybrid workaround (server decision → client forwarding)

Python

Not Supported

Hybrid workaround

Java

Not Supported

Hybrid workaround

Go

Not Supported

Hybrid workaround

C# / Ruby / PHP

Not Supported

Hybrid workaround

Decision Notification Data

The DECISION notification provides the following data through the decisionInfo object:

Field

Type

Description

flagKey

string

The feature flag key (e.g., "checkout_redesign")

enabled

boolean

Whether the flag is enabled for this user

variationKey

string

The assigned variation (e.g., "variation_a")

ruleKey

string

The rule that matched (experiment or rollout key)

experimentId

string

The experiment ID (if an experiment rule matched)

variationId

string

The variation ID

decisionEventDispatched

boolean

Whether Optimizely sent an event to its own analytics

The decisionEventDispatched field indicates whether Optimizely dispatched a decision event to its own analytics backend. When false, Optimizely skipped sending the event (typically because the user was already counted, or the decision matched a rollout rule rather than an experiment). Your mParticle integration should still send the event in this case, because mParticle needs the experiment context associated with every relevant session.

Prerequisites

Before starting the integration:

  • Optimizely Feature Experimentation SDK installed for your platform (JavaScript SDK v5+, React SDK v3+, Android SDK, iOS SDK, or React Native SDK).

  • mParticle SDK installed and initialized for your platform.

  • A feature flag with an experiment rule configured in your Optimizely project.

  • Admin access to both the mParticle dashboard and the Optimizely project.

  • Consistent user ID strategy across both SDKs for accurate cross-platform identity resolution.

Android SDK Implementation (Official Kit)

The official mParticle Android kit for Optimizely handles DECISION notification listening automatically. Add the kit dependency and configure the connection in the mParticle dashboard.

Gradle Dependency

Add the kit to your app-level build.gradle:

dependencies {
    implementation("com.mparticle:android-optimizely-kit:5+")
}

DECISION Notification Handler

The kit registers a DECISION listener automatically when initialized. To customize the event name or add additional attributes, register your own listener:

import com.optimizely.ab.OptimizelyClient
import com.optimizely.ab.notification.DecisionNotification
import com.mparticle.MParticle
import com.mparticle.MPEvent

// Access the Optimizely client through the kit
val optimizelyClient: OptimizelyClient? = OptimizelyKit.getOptimizelyClient()

optimizelyClient?.notificationCenter?.addNotificationHandler(
    DecisionNotification::class.java
) { notification ->
    val decisionInfo = notification.decisionInfo

    // Build mParticle event with experiment data
    val attributes = mapOf(
        "flag_key" to (decisionInfo["flagKey"] as? String ?: ""),
        "variation_key" to (decisionInfo["variationKey"] as? String ?: ""),
        "enabled" to (decisionInfo["enabled"]?.toString() ?: "false"),
        "experiment_id" to (decisionInfo["experimentId"] as? String ?: ""),
        "rule_key" to (decisionInfo["ruleKey"] as? String ?: "")
    )

    val event = MPEvent.Builder("Experiment Viewed", MParticle.EventType.Other)
        .customAttributes(attributes)
        .build()

    MParticle.getInstance()?.logEvent(event)
}

The event name "Experiment Viewed" is a convention — you can customize it to match your organization's naming standards. The kit handles initialization timing, ensuring the listener is registered before any decide() calls are made.

iOS SDK Implementation (Official Kit)

The official mParticle iOS kit is available through CocoaPods, Carthage, and Swift Package Manager.

CocoaPods Installation

Add to your Podfile:

pod 'mParticle-Optimizely'

Then run pod install.

For Carthage, add github "mparticle-integrations/mparticle-apple-integration-optimizely" to your Cartfile. For Swift Package Manager, add the package URL from the mParticle integrations repository.

Swift DECISION Notification Handler

import OptimizelySwiftSDK
import mParticle_Apple_SDK

// Access the Optimizely client through the kit
if let optimizelyClient = MPKitOptimizely.optimizelyClient {

    optimizelyClient.notificationCenter?.addDecisionNotificationListener { (type, userId, attributes, decisionInfo) in
        guard type == "flag" else { return }

        let flagKey = decisionInfo["flagKey"] as? String ?? ""
        let variationKey = decisionInfo["variationKey"] as? String ?? ""
        let enabled = decisionInfo["enabled"] as? Bool ?? false
        let experimentId = decisionInfo["experimentId"] as? String ?? ""
        let ruleKey = decisionInfo["ruleKey"] as? String ?? ""

        let eventAttributes: [String: Any] = [
            "flag_key": flagKey,
            "variation_key": variationKey,
            "enabled": String(enabled),
            "experiment_id": experimentId,
            "rule_key": ruleKey
        ]

        if let event = MPEvent(name: "Experiment Viewed", type: .other) {
            event.customAttributes = eventAttributes
            MParticle.sharedInstance().logEvent(event)
        }
    }
}

The iOS kit manages SDK initialization order and ensures the Optimizely client is available before listeners are registered. If you need to customize when the listener activates, register it in your app delegate's didFinishLaunching after mParticle initialization.

JavaScript SDK Implementation (Custom)

The JavaScript SDK v6+ has no official mParticle kit. The following implementation registers a DECISION notification listener manually and sends events to mParticle.

import { createInstance, enums } from "@optimizely/optimizely-sdk";

// Initialize the Optimizely SDK
const optimizely = createInstance({
  sdkKey: "<YOUR_SDK_KEY>",
});

// Helper: send experiment decision to mParticle
function sendToMParticle(flagKey, variationKey, enabled, experimentId, ruleKey) {
  if (typeof window === "undefined" || !window.mParticle) return;

  window.mParticle.logEvent(
    "Experiment Viewed",
    window.mParticle.EventType.Other,
    {
      flag_key: flagKey,
      variation_key: variationKey,
      enabled: String(enabled),
      experiment_id: experimentId || "",
      rule_key: ruleKey || ""
    }
  );
}

// Register the DECISION notification listener BEFORE any decide() calls
optimizely.notificationCenter.addNotificationListener(
  enums.NOTIFICATION_TYPES.DECISION,
  ({ type, userId, attributes, decisionInfo }) => {
    // Only process feature flag decisions
    if (type !== "flag") return;

    const { flagKey, enabled, variationKey, ruleKey, experimentId } = decisionInfo;

    sendToMParticle(flagKey, variationKey, enabled, experimentId, ruleKey);
  }
);

// Make a decision (triggers the listener)
optimizely.onReady().then(() => {
  const user = optimizely.createUserContext("<USER_ID>", {
    plan_type: "premium",
    country: "US",
  });

  const decision = user.decide("checkout_redesign");
  console.log("Variation:", decision.variationKey);
  console.log("Enabled:", decision.enabled);
});

Listener Registration Timing

The DECISION listener must be registered before any decide() calls. If the listener is registered after a decide() call, that decision is lost and never sent to mParticle. The correct order is:

  1. createInstance() — initialize the SDK

  2. addNotificationListener() — register the DECISION listener

  3. onReady() / decide() — make feature flag decisions

Late registration is the most common cause of missing experiment events in mParticle. If you see decisions in Optimizely but no corresponding events in mParticle, check the registration order first.

React SDK Implementation (Custom)

The React SDK wraps the JavaScript SDK with React-specific hooks and providers. The notification listener must be registered at the module level (outside of components) to ensure it captures all decisions, including those triggered during server-side rendering or initial component mount.

import React from "react";
import {
  createInstance,
  OptimizelyProvider,
  useDecision,
  enums,
} from "@optimizely/react-sdk";

// Initialize the SDK at module level
const optimizely = createInstance({
  sdkKey: "<YOUR_SDK_KEY>",
});

// Helper to send events to mParticle
function sendToMParticle(flagKey: string, variationKey: string, enabled: boolean, experimentId: string) {
  if (typeof window === "undefined" || !window.mParticle) return;

  window.mParticle.logEvent(
    "Experiment Viewed",
    window.mParticle.EventType.Other,
    {
      flag_key: flagKey,
      variation_key: variationKey,
      enabled: String(enabled),
      experiment_id: experimentId || ""
    }
  );
}

// Register listener at module level, BEFORE any component renders
optimizely.notificationCenter.addNotificationListener(
  enums.NOTIFICATION_TYPES.DECISION,
  ({ type, decisionInfo }: any) => {
    if (type !== "flag") return;

    const { flagKey, enabled, variationKey, experimentId } = decisionInfo;
    sendToMParticle(flagKey, variationKey, enabled, experimentId);
  }
);

// App component with OptimizelyProvider
function App() {
  return (
    <OptimizelyProvider
      optimizely={optimizely}
      user={{ id: "<USER_ID>", attributes: { plan_type: "premium" } }}
    >
      <CheckoutPage />
    </OptimizelyProvider>
  );
}

// Feature flag component using useDecision hook
function CheckoutPage() {
  const [decision] = useDecision("checkout_redesign");

  // useDecision calls decide() internally,
  // which triggers the DECISION listener automatically

  if (decision.enabled) {
    return <NewCheckout variation={decision.variationKey} />;
  }

  return <OriginalCheckout />;
}

The useDecision hook calls decide() internally, which triggers the DECISION notification listener. You do not need to call sendToMParticle manually inside the component — the module-level listener handles it.

Registering the listener inside a React component (via useEffect) instead of at the module level is a common mistake. Component-level registration may happen after the first decide() call, causing missed events.

React Native Implementation (Custom)

For React Native, use the mParticle React Native SDK (react-native-mparticle) alongside the Optimizely React SDK:

import { createInstance, enums } from "@optimizely/react-sdk";
import MParticle from "react-native-mparticle";

const optimizely = createInstance({
  sdkKey: "<YOUR_SDK_KEY>",
});

// Register DECISION listener for React Native
optimizely.notificationCenter.addNotificationListener(
  enums.NOTIFICATION_TYPES.DECISION,
  ({ type, decisionInfo }: any) => {
    if (type !== "flag") return;

    const { flagKey, enabled, variationKey, experimentId, ruleKey } = decisionInfo;

    // Use the React Native mParticle SDK
    MParticle.logEvent(
      "Experiment Viewed",
      MParticle.EventType.Other,
      {
        flag_key: flagKey,
        variation_key: variationKey,
        enabled: String(enabled),
        experiment_id: experimentId || "",
        rule_key: ruleKey || ""
      }
    );
  }
);

The React Native mParticle SDK bridges to the native Android and iOS mParticle SDKs. Event names and attributes are forwarded identically to the web implementation.

Server-Side Workaround (Hybrid Approach)

Server-side SDKs (Node.js, Python, Java, Go, C#, Ruby, PHP) cannot call mParticle.logEvent() because the mParticle Web SDK runs in the browser. For server-rendered applications that need mParticle integration, use a hybrid approach: make the decision server-side, then pass the decision data to the client for forwarding to mParticle.

// Server-side (Node.js / Express example)
const optimizely = require("@optimizely/optimizely-sdk");

const client = optimizely.createInstance({
  sdkKey: "<YOUR_SDK_KEY>",
});

app.get("/page", async (req, res) => {
  await client.onReady();
  const user = client.createUserContext(req.cookies.userId, {
    plan_type: req.session.planType,
  });

  const decision = user.decide("checkout_redesign");

  // Pass decision data to the template
  res.render("page", {
    mpDecision: JSON.stringify({
      flag_key: decision.flagKey,
      variation_key: decision.variationKey,
      enabled: decision.enabled,
      experiment_id: decision.ruleKey || ""
    }),
    showNewCheckout: decision.enabled && decision.variationKey === "variation_a"
  });
});
<!-- Client-side: read server decision and send to mParticle -->
<script>
  var mpDecision = JSON.parse('{{mpDecision}}');
  if (window.mParticle && window.mParticle.logEvent) {
    window.mParticle.logEvent(
      "Experiment Viewed",
      window.mParticle.EventType.Other,
      {
        flag_key: mpDecision.flag_key,
        variation_key: mpDecision.variation_key,
        enabled: String(mpDecision.enabled),
        experiment_id: mpDecision.experiment_id
      }
    );
  }
</script>

This pattern works for any server-side SDK. The server makes the decision and renders the appropriate experience, then the client-side script sends the decision metadata to mParticle. The mParticle event arrives slightly later than the page render, but within the same session.

Advanced: Revenue Event Handling

Revenue tracking requires attention to unit conversion. The official Android and iOS kits handle currency conversion automatically, but custom implementations must convert manually.

Platform

mParticle Revenue Unit

Optimizely Revenue Unit

Conversion

Android (Official Kit)

Dollars

Cents

Automatic (kit handles ×100)

iOS (Official Kit)

Dollars

Cents

Automatic (kit handles ×100)

JavaScript (Custom)

Dollars

Cents

Manual (you must ×100)

React (Custom)

Dollars

Cents

Manual (you must ×100)

Server-Side (Hybrid)

Dollars

Cents

Manual (you must ×100)

For custom implementations, multiply revenue by 100 when sending to Optimizely:

// Custom revenue tracking — manual conversion required
const revenueInDollars = 49.99;
const revenueInCents = Math.round(revenueInDollars * 100); // 4999

const user = optimizely.createUserContext("<USER_ID>");
user.trackEvent("purchase", { revenue: revenueInCents });

When sending revenue data to mParticle, use the original dollar amount. mParticle expects dollars in its Commerce Events.

Advanced: Experiment vs Rollout Tracking

Feature flags can have both experiment rules and rollout rules. The decisionEventDispatched field helps distinguish between them:

optimizely.notificationCenter.addNotificationListener(
  enums.NOTIFICATION_TYPES.DECISION,
  ({ type, decisionInfo }) => {
    if (type !== "flag") return;

    const { flagKey, enabled, variationKey, ruleKey, decisionEventDispatched } = decisionInfo;

    // Distinguish experiment from rollout
    const ruleType = decisionEventDispatched ? "experiment" : "rollout";

    if (window.mParticle && window.mParticle.logEvent) {
      window.mParticle.logEvent(
        "Experiment Viewed",
        window.mParticle.EventType.Other,
        {
          flag_key: flagKey,
          variation_key: variationKey || "off",
          enabled: String(enabled),
          rule_type: ruleType,
          rule_key: ruleKey || ""
        }
      );
    }
  }
);

Sending rollout decisions to mParticle is recommended even though they are not experiments. mParticle uses this data for session association, audience building, and downstream analytics. Knowing that a user was in a rollout (and which percentage) provides valuable context for behavioral analysis.

A/A Testing Validation

Before trusting the integration for production experiments, validate it with an A/A test to confirm data flows correctly and no systematic bias exists.

A/A Test Methodology

  1. Create a feature flag in Optimizely (e.g., mp_validation_test).

  2. Add an experiment rule with two identical variations (both enable the same feature, no code differences).

  3. Set traffic allocation to 50/50.

  4. Deploy the SDK with the DECISION listener active.

  5. Run for at least 7 days or until 1,000 users per variation.

  6. In mParticle, filter "Experiment Viewed" events by variation and compare event counts. Both variations should show roughly equal participation.

  7. If using downstream tools (Amplitude, Mixpanel), verify that experiment attributes appear correctly in those platforms.

flowchart TD
    A["Create feature flag with A/A experiment rule"] --> B["Deploy SDK with DECISION listener"]
    B --> C["Run for 7+ days / 1000+ users per variation"]
    C --> D{"Events appearing in mParticle Live Stream?"}
    D -->|Yes| E{"Event counts within 5% between variations?"}
    E -->|Yes| F["Integration validated — proceed with real experiments"]
    E -->|No| G["Check listener timing and user ID consistency"]
    D -->|No| H["Check mParticle SDK initialization and listener registration"]
    G --> I["Re-run A/A test"]
    H --> I

Validating the Integration

After setup, verify that decision data reaches mParticle correctly.

Console Verification

Open the browser console on a page where a feature flag is evaluated:

// Verify mParticle SDK is loaded
console.log("mParticle loaded:", typeof window.mParticle !== "undefined");

// Verify Optimizely SDK is loaded
console.log("Optimizely loaded:", typeof window.optimizelyClientInstance !== "undefined");

// Manually trigger a decision and check the listener fires
// (The listener should log to mParticle automatically)

mParticle Live Stream

Check the mParticle Live Stream dashboard for "Experiment Viewed" events. Key details to verify:

  • Event name matches your configuration ("Experiment Viewed" by default).

  • Event attributes include flag_key, variation_key, enabled, and experiment_id.

  • Events arrive within 3-5 minutes of the decision being made.

If events do not appear, check the mParticle SDK debug output for errors.

SDK Debug Logging

Enable debug logging on the Optimizely SDK to confirm decisions are being made and notifications are dispatched:

const optimizely = createInstance({
  sdkKey: "<YOUR_SDK_KEY>",
  logLevel: "DEBUG",
});

The debug output shows each decision evaluation, including which rule matched, which variation was assigned, and whether the notification was dispatched. Look for DECISION notification entries in the log output.

Analyzing Experiments in mParticle

Once experiment decisions flow into mParticle, use mParticle's audience and forwarding capabilities for analysis.

Audiences by Variation

Create mParticle audiences segmented by experiment participation:

  1. Go to Audiences in the mParticle dashboard.

  2. Create an audience with an event-based rule targeting "Experiment Viewed" events.

  3. Add attribute filters for flag_key and variation_key to target specific variations.

  4. Name audiences descriptively: "Checkout Redesign - Variation A", "Checkout Redesign - Control".

These audiences update in real time and can be forwarded to any connected output.

Forwarding to Downstream Tools

mParticle experiment events and audiences can be forwarded to connected outputs:

  • Amplitude or Mixpanel: Analyze experiment impact on retention, engagement, and feature adoption.

  • BigQuery or Snowflake: Run cross-experiment queries and build custom dashboards.

  • Braze or Iterable: Trigger personalized messaging based on experiment variation.

  • Segment or Rudderstack: Further route experiment data across your stack.

Each downstream tool receives the full event with all attributes, enabling variation-level analysis without additional integration work.

Revenue Attribution

When using mParticle audiences with experiment variation filters, you can attribute revenue to specific variations across all connected commerce and analytics tools. This provides a unified view of experiment revenue impact beyond what Optimizely's built-in results show.

Troubleshooting

Listener Not Firing

If the DECISION notification listener never executes:

  • Registration timing: The listener must be registered before any decide() calls. Register immediately after createInstance(), before onReady() resolves.

  • Wrong notification type: Verify you are listening for enums.NOTIFICATION_TYPES.DECISION, not TRACK or another type.

  • React: registered inside component: Register the listener at module level, not inside a useEffect hook. Component-level registration may happen after the first decide() call.

  • SDK not initialized: Ensure createInstance() has completed and the datafile has been fetched. Use optimizely.onReady() to confirm before calling decide().

// Correct registration order
const optimizely = createInstance({ sdkKey: "<YOUR_SDK_KEY>" });

// Register listener IMMEDIATELY after createInstance
optimizely.notificationCenter.addNotificationListener(
  enums.NOTIFICATION_TYPES.DECISION,
  myListenerCallback
);

// Now safe to make decisions
optimizely.onReady().then(() => {
  const user = optimizely.createUserContext("user123");
  const decision = user.decide("my_flag");
});

Events Not Reaching mParticle

If the listener fires but events do not appear in mParticle:

  • mParticle SDK not loaded: Verify window.mParticle (web) or the native mParticle SDK (mobile) is available when the listener executes. The listener may fire before the mParticle SDK initializes.

  • Expected delay: mParticle Live Stream has a 3-5 minute processing delay. Events do not appear immediately after being sent.

  • User ID mismatch: If mParticle and Optimizely use different user identifiers, events are sent but cannot be reconciled. Verify that both SDKs use the same user ID.

Server-Side SDKs Not Supported

Server-side SDKs (Node.js, Python, Java, Go, C#, Ruby, PHP) do not have official mParticle kits. The DECISION listener fires on the server, but there is no mParticle SDK to send events to.

Use the hybrid workaround documented in the Server-Side Workaround section: make the decision server-side, pass the decision data to the client via a template variable or API response, and send the mParticle event from the browser.

Revenue Conversion Issues

Revenue discrepancies between mParticle and Optimizely are typically caused by unit differences:

  • Official kits (Android/iOS): Revenue conversion is handled automatically. mParticle sends dollars, the kit converts to cents for Optimizely.

  • Custom implementations (JS/React/RN): You must multiply revenue by 100 manually when tracking events in Optimizely. mParticle Commerce Events use dollars.

If revenue numbers are off by exactly 100x, check whether conversion is being applied twice (once by your code, once by the SDK) or not at all.

decisionEventDispatched: false

This field is false when:

  • The user was already counted for this flag+rule in the current session.

  • sendFlagDecisions is set to false in the SDK configuration.

  • The decision matched a rollout rule rather than an experiment rule.

Your mParticle listener should still send the event when decisionEventDispatched is false. mParticle needs the experiment context associated with every session where the flag is evaluated, regardless of whether Optimizely dispatched its own analytics event.