Integrate Segment with Optimizely Feature Experimentation

Loading...·9 min read

Optimizely Feature Experimentation uses SDK-based feature flags rather than a client-side snippet. Integrating with Segment sends experiment decision data into your analytics infrastructure, where it can be forwarded to warehouses, analytics tools, and engagement platforms through Segment's destination catalog.

Unlike Web Experimentation, which uses a JSON analytics plugin with track_layer_decision callbacks, Feature Experimentation uses SDK notification listeners. You register a DECISION notification listener on the Optimizely SDK instance that captures flag keys, variation keys, and experiment details, then sends the data to Segment via analytics.track().

This guide covers JavaScript/Node.js and Python implementations, Segment dashboard configuration, and common integration pitfalls.

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 decision data and sends it to Segment as an "Experiment Viewed" track call. Segment then routes this event to all connected destinations — warehouses, analytics tools, engagement platforms — with the experiment context attached.

sequenceDiagram
    participant App as Application Code
    participant SDK as Optimizely SDK
    participant Listener as DECISION Listener
    participant Seg as Segment SDK
    participant Dest as Downstream Destinations

    App->>SDK: user.decide("flag_key")
    SDK->>SDK: Evaluate flag rules
    SDK->>Listener: DECISION notification
    Listener->>Listener: Extract decision data
    Listener->>Listener: Filter for experiment types
    Listener->>Seg: analytics.track("Experiment Viewed", properties)
    Seg->>Dest: Route to warehouses, analytics, marketing tools

Decision Notification Data

The DECISION notification provides the following data through the callback arguments:

Field

Type

Description

type

string

The decision type: "flag"

userId

string

The user ID passed to the SDK

attributes

object

User attributes passed to the SDK

decisionInfo.flagKey

string

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

decisionInfo.enabled

boolean

Whether the flag is enabled for this user

decisionInfo.variationKey

string

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

decisionInfo.ruleKey

string

The rule that matched (experiment key or rollout key)

decisionInfo.decisionEventDispatched

boolean

Whether Optimizely sent an event to its own analytics

The decisionInfo object also includes variables (the flag variable values for this variation) and reasons (if the SDK is configured with decision logging enabled).

Platform Support

Platform

Optimizely SDK

Segment SDK

Integration Method

Browser (JavaScript)

@optimizely/optimizely-sdk

analytics.js or @segment/analytics-next

Client-side listener

Node.js

@optimizely/optimizely-sdk

analytics-node

Server-side listener

Python

optimizely-sdk

analytics-python

Server-side listener

Java

optimizely-java-sdk

analytics-java

Server-side listener

Go

optimizely-go-sdk

analytics-go

Server-side listener

Ruby

optimizely-sdk

analytics-ruby

Server-side listener

Server-side SDKs send events to Segment in cloud mode, which means events go directly to Segment's API without a browser. This is the recommended approach for backend services, API servers, and microservices.

Prerequisites

Before starting the integration:

  • Optimizely Feature Experimentation SDK installed and initialized for your platform (JavaScript SDK v5+, Node.js, Python, or other supported SDK).

  • Segment SDK installed and initialized for your platform (analytics.js for browser, analytics-node for Node.js, analytics-python for Python).

  • A feature flag with an experiment rule (A/B test or feature test) configured in your Optimizely project.

  • A Segment source created in your workspace for the application sending events.

  • Consistent user ID strategy across both SDKs. The userId passed to the Optimizely SDK should match the userId used in analytics.identify() and analytics.track() calls.

JavaScript / Node.js Implementation

The JavaScript implementation works for both browser and Node.js environments. The only difference is the Segment SDK import — analytics.js for browser, analytics-node for server.

Full Implementation

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

// Initialize Segment (Node.js)
// For browser: window.analytics is available after analytics.js loads
const analytics = new Analytics("YOUR_SEGMENT_WRITE_KEY");

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

// Wait for SDK to be ready
optimizely.onReady().then(() => {
  // Register the DECISION notification listener BEFORE any decide() calls
  optimizely.notificationCenter.addNotificationListener(
    enums.NOTIFICATION_TYPES.DECISION,
    (notification) => {
      const { type, userId, attributes, decisionInfo } = notification;

      // Only process flag decisions (not other decision types)
      if (type !== "flag") return;

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

      // Only send events for experiment rules, not rollouts
      // Rollout rules have ruleKey but no experiment assignment
      if (!ruleKey || !variationKey) return;

      // Build properties for Segment
      const properties = {
        flagKey: flagKey,
        variationKey: variationKey,
        enabled: enabled,
        ruleKey: ruleKey,
        optimizelyUserId: userId,
      };

      // Include user attributes if present
      if (attributes && Object.keys(attributes).length > 0) {
        properties.userAttributes = attributes;
      }

      // Send to Segment
      analytics.track({
        userId: userId,
        event: "Experiment Viewed",
        properties: properties,
      });
    }
  );

  // Now make flag decisions — the listener fires automatically
  const user = optimizely.createUserContext("user-123", {
    plan: "premium",
    country: "US",
  });

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

Browser-Specific Implementation

For browser environments using analytics.js, replace the Segment initialization with the global window.analytics object:

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

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

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

      const { flagKey, variationKey, enabled, ruleKey } = decisionInfo;
      if (!ruleKey || !variationKey) return;

      // Use window.analytics (Segment analytics.js)
      if (window.analytics && typeof window.analytics.track === "function") {
        window.analytics.track("Experiment Viewed", {
          flagKey: flagKey,
          variationKey: variationKey,
          enabled: enabled,
          ruleKey: ruleKey,
        });
      }
    }
  );
});

Filtering by Decision Type

Not every DECISION notification corresponds to an experiment. Optimizely fires DECISION notifications for rollouts (gradual feature releases) as well as A/B tests. If you only want to track experiment decisions in Segment, filter by the rule type:

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

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

    // Skip if no rule matched (flag evaluated to default)
    if (!ruleKey) return;

    // Skip if no variation assigned (rollout with no experiment)
    if (!variationKey) return;

    // Send to Segment
    analytics.track({
      userId: userId,
      event: "Experiment Viewed",
      properties: {
        flagKey: flagKey,
        variationKey: variationKey,
        enabled: enabled,
        ruleKey: ruleKey,
      },
    });
  }
);

The ruleKey corresponds to the experiment key or rollout key in your Optimizely project. You can use this to filter for specific experiments if needed.

Python Implementation

The Python SDK follows the same pattern: register a notification listener, extract decision data, and send to Segment.

import optimizely
from optimizely import notification_center
import analytics

# Initialize Segment
analytics.write_key = "YOUR_SEGMENT_WRITE_KEY"

# Initialize Optimizely SDK
optimizely_client = optimizely.Optimizely(
    sdk_key="YOUR_SDK_KEY"
)

# Define the DECISION notification listener
def on_decision(notification_type, args):
    decision_type = args.get("type")
    if decision_type != "flag":
        return

    user_id = args.get("user_id")
    decision_info = args.get("decision_info", {})

    flag_key = decision_info.get("flag_key")
    variation_key = decision_info.get("variation_key")
    enabled = decision_info.get("enabled")
    rule_key = decision_info.get("rule_key")

    # Skip if no experiment rule matched
    if not rule_key or not variation_key:
        return

    # Send to Segment
    analytics.track(
        user_id=user_id,
        event="Experiment Viewed",
        properties={
            "flagKey": flag_key,
            "variationKey": variation_key,
            "enabled": enabled,
            "ruleKey": rule_key,
        }
    )

# Register the listener
notification_id = optimizely_client.notification_center.add_notification_listener(
    notification_center.NotificationType.DECISION,
    on_decision
)

# Make flag decisions — the listener fires automatically
user = optimizely_client.create_user_context("user-123", {"plan": "premium"})
decision = user.decide("checkout_redesign")

print(f"Flag enabled: {decision.enabled}")
print(f"Variation: {decision.variation_key}")

# Flush Segment events before process exits
analytics.flush()

Python Batch Considerations

For high-throughput Python services, the Segment analytics-python library batches events by default (sends every 100 events or every 0.5 seconds). If your service processes many flag decisions per second, tune the batch settings:

import analytics

analytics.write_key = "YOUR_SEGMENT_WRITE_KEY"
analytics.max_queue_size = 10000  # Increase queue size for high throughput
analytics.send = True             # Ensure sending is enabled
analytics.debug = False           # Disable debug logging in production

Always call analytics.flush() before process shutdown to ensure all queued events are sent.

Segment Dashboard Configuration

Choosing the Right Destination

Segment offers two Optimizely destinations. Use the correct one:

Destination

Use For

Mode

Optimizely Web

Web Experimentation (client-side snippet)

Device mode (browser)

Optimizely Full Stack

Feature Experimentation (SDK-based)

Cloud mode (server-to-server)

For Feature Experimentation, select the Optimizely Full Stack destination if you want Segment to forward events back to Optimizely. This destination sends conversion events (track calls) to Optimizely's event API, enabling you to use Segment-tracked events as metrics in your Optimizely experiments.

Configure the Full Stack Destination

  1. In your Segment workspace, navigate to Connections > Destinations.

  2. Click Add Destination and search for Optimizely Full Stack.

  3. Select the source that sends your application events.

  4. Enter your Optimizely SDK Key in the destination settings.

  5. Map Segment track event names to Optimizely event keys. This mapping determines which Segment events count as conversions in Optimizely experiments.

  6. Toggle the destination to Enabled.

Event Mapping

The Full Stack destination maps Segment calls to Optimizely API calls:

Segment Call

Optimizely API Call

Notes

track

Track event

Event key must match an Optimizely event

identify (with userId)

User attribute update

Attributes forwarded for audience targeting

page

Not mapped by default

Requires custom event mapping

User ID Consistency

The most common integration failure is inconsistent user IDs between the Optimizely SDK and Segment. The userId passed to optimizely.createUserContext(userId) must match the userId used in analytics.track({ userId }).

// Consistent user ID across both SDKs
const userId = "user-123";

// Optimizely uses this userId for bucketing
const user = optimizely.createUserContext(userId, { plan: "premium" });

// Segment uses the same userId for tracking
analytics.identify(userId, { plan: "premium" });

// The DECISION listener sends the same userId to Segment
// (it reads userId from the notification, which comes from createUserContext)
const decision = user.decide("checkout_redesign");

If your application uses different identifiers (e.g., an internal user ID for Optimizely and an email for Segment), events will not join correctly in downstream tools. Standardize on a single identifier or use Segment's identity resolution to merge profiles.

Anonymous Users

For browser applications with anonymous users, you may not have a stable userId available. In this case:

  • Use Segment's anonymousId as the Optimizely user ID.

  • Or generate a stable identifier (e.g., a UUID stored in a cookie) and use it for both SDKs.

// Use Segment's anonymousId for Optimizely
const anonymousId = analytics.user().anonymousId();
const user = optimizely.createUserContext(anonymousId, {});
const decision = user.decide("checkout_redesign");

Cloud-Mode Notification Listener Limitation

Server-side notification listeners run in your application process, not in the browser. This means:

  • Events go directly to Segment's API via the server-side Segment SDK. They do not pass through analytics.js in the browser.

  • Browser-side Segment destinations (device-mode integrations like Google Analytics client-side tag or Facebook Pixel) will not receive these events directly. They will only receive the events if the destination also supports cloud mode.

  • Latency is lower because events go server-to-server without browser overhead.

  • No ad blocker interference since events are sent from your server.

If you need experiment data in browser-side destinations, consider a hybrid approach: send the decision from the server to the client (via API response or WebSocket), then call window.analytics.track() on the client side.

Validating the Integration

Server-Side Validation (Node.js)

Add logging to your notification listener to verify decisions are being captured:

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

    console.log("[Optimizely Decision]", {
      userId,
      flagKey: decisionInfo.flagKey,
      variationKey: decisionInfo.variationKey,
      ruleKey: decisionInfo.ruleKey,
      enabled: decisionInfo.enabled,
    });

    // ... send to Segment
  }
);

Segment Debugger

  1. In your Segment workspace, navigate to Connections > Sources > your application source.

  2. Click the Debugger tab.

  3. Trigger a flag decision in your application.

  4. Look for "Experiment Viewed" events in the debugger with the expected properties.

End-to-End Verification Checklist

Check

How to Verify

Expected Result

SDK initialization

Check logs for "Optimizely SDK ready"

SDK initializes without errors

Listener registration

Log inside the listener callback

Listener fires on every decide() call

Decision data

Log decisionInfo in the listener

Contains flagKey, variationKey, ruleKey

Segment event sent

Check Segment debugger

"Experiment Viewed" event with correct properties

Downstream delivery

Check warehouse or analytics tool

Event appears with all properties intact

Troubleshooting

No "Experiment Viewed" Events in Segment

If the Segment debugger shows no "Experiment Viewed" events:

  • Listener not registered before decide(): The notification listener must be registered before any decide() calls. If you call decide() during SDK initialization, ensure the listener is added in the onReady() callback before any flag evaluations.

  • SDK not ready: The SDK must be initialized and ready before notifications fire. Use optimizely.onReady() to wait for initialization.

  • Wrong notification type: Ensure you are listening for enums.NOTIFICATION_TYPES.DECISION, not ACTIVATE (which is deprecated) or TRACK.

  • Segment write key invalid: Verify your Segment write key is correct and the source is not disabled.

  • Python: events not flushed: The Python Segment library batches events. Call analytics.flush() to force sending, especially in scripts or short-lived processes.

Listener Fires but Properties Are Missing

If events appear in Segment but with incomplete properties:

  • No ruleKey: The flag decision matched a default rule (no experiment or rollout). Filter these out with if (!ruleKey) return.

  • No variationKey: The user was not bucketed into any variation. This can happen if traffic allocation is less than 100% or the user does not meet audience conditions.

  • Attributes not included: User attributes are only available in the notification if they were passed to createUserContext(). Verify attributes are being set correctly.

Duplicate Events

If you see duplicate "Experiment Viewed" events in Segment:

  • Multiple listeners registered: Each call to addNotificationListener adds a new listener. If your code runs in a hot-reloading development environment, you may be registering multiple listeners. Store the notification ID and remove old listeners:

// Store the listener ID and remove before re-registering
let listenerId = null;

function registerListener(optimizely) {
  if (listenerId !== null) {
    optimizely.notificationCenter.removeNotificationListener(listenerId);
  }

  listenerId = optimizely.notificationCenter.addNotificationListener(
    enums.NOTIFICATION_TYPES.DECISION,
    (notification) => {
      // ... handle notification
    }
  );
}
  • Multiple SDK instances: If your application creates multiple Optimizely SDK instances (common in microservice architectures), each instance fires its own notifications. Ensure you register the listener on the correct instance.

Events Reaching Segment but Not Downstream

If events appear in the Segment debugger but not in downstream destinations:

  • Destination not connected: Verify the downstream destination is enabled and connected to the correct source.

  • Schema violations: Some destinations enforce event schemas. Check the destination's event delivery logs for rejected events.

  • Property type mismatch: If a downstream tool expects experimentId as a number but receives a string, it may reject the property. Check the destination's type requirements.

  • Rate limiting: High-volume experiment decisions may trigger rate limits on downstream APIs. Check the Segment delivery logs for HTTP 429 responses.

Optimizely tips, straight to your inbox

Practical guides and patterns for experimentation practitioners. No spam, unsubscribe anytime.