Integrate Segment with Optimizely Web Experimentation

Loading...·12 min read

Segment is an analytics infrastructure platform that collects, routes, and transforms customer event data across hundreds of downstream tools. Integrating Segment with Optimizely Web Experimentation creates a bidirectional data flow: Segment can forward tracked events to Optimizely as conversion metrics, and Optimizely can send experiment decision data back to Segment for routing to warehouses, analytics tools, and engagement platforms.

There are two ways to set up this integration. Each approach handles both directions (events into Optimizely, decisions out to Segment) differently. Choose the one that fits your needs — do not enable both, as this will cause duplicate events.

Two Approaches: Native vs Custom

Option A: Native Segment Destination

Option B: Custom Analytics Integration

Setup

Toggle on in Segment dashboard — no code

JSON configuration in Optimizely Settings > Integrations

Events → Optimizely

Automatic. Segment forwards track, page, and identify calls via device-mode destination

Not handled. You must load the Optimizely snippet and use window.optimizely.push() directly

Decisions → Segment

Automatic "Experiment Viewed" event with basic properties

Custom track_layer_decision callback with full campaign context

Properties on decision events

experimentId, experimentName, variationId, variationName

All of the above plus campaignId, campaignName, isHoldback, and any custom fields

Configuration per experiment

None — applies globally when enabled

Per-experiment toggle in Manage Campaign > Integrations

Best for

Teams that want a quick setup and only need basic experiment data

Teams that need full decision context in warehouses or downstream tools

flowchart TD
    A["Need Segment + Optimizely integration?"] --> B{"Need campaign IDs, holdback status, or custom properties?"}
    B -->|No| C["Option A: Native Segment Destination"]
    B -->|Yes| D["Option B: Custom Analytics Integration"]
    C --> E["Toggle on in Segment dashboard"]
    D --> F["Add JSON integration in Optimizely"]

Prerequisites

Before starting either approach, confirm the following:

  • Segment analytics.js deployed on your site with event tracking active.

  • Optimizely Web Experimentation snippet installed on the same pages. The snippet must be present before Segment attempts to forward events.

  • Admin access to both the Segment workspace (Sources and Destinations configuration) and the Optimizely project (Settings > Integrations).

  • Optimizely Project ID available. Find it in the Optimizely dashboard under Settings > Project, or by running:

// Find your Optimizely Project ID
var projectId = window.optimizely && window.optimizely.get("data").projectId;
console.log("Optimizely Project ID:", projectId);

Option A: Native Segment Destination

This is the simplest approach. Segment provides a built-in Optimizely Web destination that handles both directions automatically — no custom code required.

How It Works

The Segment Optimizely Web destination operates in device mode, meaning it loads alongside the Optimizely snippet in the browser. Segment detects the window.optimizely object and pushes events directly into the Optimizely event queue.

In the other direction, when Optimizely makes a bucketing decision, the destination automatically fires an "Experiment Viewed" track call back into Segment with basic experiment properties.

flowchart LR
    subgraph SEG["Segment"]
        A["analytics.js"] --> B["Optimizely Web Destination"]
    end
    subgraph OP["Optimizely"]
        C["window.optimizely queue"] --> D["Experiment Results"]
        E["Bucketing Decision"]
    end
    B -->|"Track/Page events"| C
    B -->|"Auto-detects decisions"| F["'Experiment Viewed' track call"]
    F --> G["Segment Sources → Downstream Tools"]

Step 1: Configure the Destination

  1. Log into your Segment workspace.

  2. Navigate to Connections > Destinations.

  3. Click Add Destination and search for Optimizely Web.

  4. Select the source you want to connect (your website source).

  5. Enter your Optimizely Project ID in the destination settings.

  6. Configure the following settings:

Setting

Recommended Value

Description

Send experiment data to other tools

Enabled

Automatically sends an "Experiment Viewed" track call when a visitor is bucketed

Track Named Pages

Enabled

Forwards analytics.page("Category", "Name") calls to Optimizely

Track Categorized Pages

Enabled

Forwards categorized page calls

Custom experiment properties

As needed

Additional properties to include in "Experiment Viewed" events

  1. Toggle the destination to Enabled.

Step 2: Verify Event Flow

Once the destination is active, Segment forwards tracked events to Optimizely automatically.

A standard Segment track call that gets forwarded to Optimizely:

// Track a custom event in Segment
// This event is automatically forwarded to Optimizely
analytics.track("Button Clicked", {
  button_name: "Add to Cart",
  page: "product_detail",
  category: "electronics",
  revenue: 49.99
});

Segment maps its call types to Optimizely events as follows:

Segment Call Type

Optimizely Event Type

Notes

track

Custom event

Event name and properties forwarded directly

page

Page view event

Page name, URL, and properties forwarded

track (with revenue)

Revenue event

Revenue property forwarded as-is (no conversion)

Revenue handling in Segment is straightforward. Unlike some CDPs that auto-convert between dollars and cents, Segment forwards the revenue property directly to Optimizely. If your Segment events send revenue in dollars, Optimizely receives the value in dollars. Verify that your Optimizely metrics are configured to expect the same unit your Segment events use.

Properties on "Experiment Viewed" Events

The native destination sends these properties automatically:

Property

Example Value

experimentId

"12345678901"

experimentName

"Homepage Hero Test"

variationId

"98765432101"

variationName

"Variation A"

This is sufficient for most analytics use cases. If you need campaign-level data or holdback status, use Option B instead.

Limitations

  • No campaignId, campaignName, or isHoldback properties on decision events.

  • Cannot customize the event name (always "Experiment Viewed").

  • Cannot add custom properties beyond what the destination provides.

  • Applies globally — you cannot enable/disable per experiment.


Option B: Custom Analytics Integration

This approach gives you full control over what data is sent from Optimizely to Segment. You create a Custom Analytics Integration in Optimizely with a track_layer_decision callback that calls window.analytics.track() with all available decision context.

Important: This approach only handles the Optimizely → Segment direction (sending experiment decisions to Segment). It does not forward Segment events into Optimizely. If you also need Segment → Optimizely event forwarding, you will need to push events to window.optimizely manually or use the native destination for that direction only (with "Send experiment data to other tools" disabled to avoid duplicate decision events).

How It Works

flowchart LR
    subgraph OP["Optimizely"]
        A["Bucketing Decision"] --> B["track_layer_decision callback"]
    end
    B -->|"window.analytics.track()"| C["Segment Sources"]
    C --> D["Warehouses"]
    C --> E["Analytics Tools"]
    C --> F["Marketing Platforms"]

When Optimizely makes a bucketing decision, the track_layer_decision callback fires. Your custom code reads the full decision context — including campaign IDs, campaign names, and holdback status — and sends it to Segment via window.analytics.track().

Understanding the Available Variables

The track_layer_decision callback receives these variables:

Variable

Type

Description

campaignId

number

The campaign (layer) ID — a 16-digit numeric identifier

experimentId

number

The experiment ID within the campaign

variationId

number

The assigned variation ID

isHoldback

boolean

Whether the visitor is in the holdback group

campaign

object

Full campaign object with name, policy, and other metadata

extension

object

Custom settings from the form_schema fields

To get human-readable experiment and variation names, use the state.getDecisionObject() API:

// Inside track_layer_decision, get human-readable names
var decision = state.getDecisionObject({ campaignId: campaignId });
// decision contains:
// {
//   experiment: { id: "12345678901", name: "Homepage Hero Test" },
//   variation: { id: "98765432101", name: "Variation A" }
// }

Step 1: Create the JSON Integration

  1. In your Optimizely project, go to Settings > Integrations.

  2. Click Create Analytics Integration > Using JSON.

  3. Paste the following configuration. The options.track_layer_decision field includes the callback code directly, so no separate code step is needed:

{
  "plugin_type": "analytics_integration",
  "name": "Segment (Experiment Decisions)",
  "form_schema": [
    {
      "default_value": "Experiment Viewed",
      "field_type": "text",
      "name": "Event Name",
      "api_name": "eventName",
      "description": "The Segment event name for experiment decisions"
    }
  ],
  "description": "Sends Optimizely experiment decisions to Segment with full campaign context",
  "options": {
    "track_layer_decision": "var segEventName = extension.eventName || \"Experiment Viewed\";\n\nvar decision = state.getDecisionObject({ campaignId: campaignId });\nvar experimentName = decision && decision.experiment ? decision.experiment.name : String(experimentId);\nvar variationName = decision && decision.variation ? decision.variation.name : String(variationId);\nvar campaignName = campaign && campaign.name ? campaign.name : String(campaignId);\n\nvar properties = {\n  experimentId: String(experimentId),\n  experimentName: experimentName,\n  variationId: String(variationId),\n  variationName: variationName,\n  campaignId: String(campaignId),\n  campaignName: campaignName,\n  isHoldback: isHoldback\n};\n\nfunction sendToSegment() {\n  if (window.analytics && typeof window.analytics.track === \"function\") {\n    window.analytics.track(segEventName, properties);\n  }\n}\n\nif (window.analytics && typeof window.analytics.track === \"function\") {\n  sendToSegment();\n} else {\n  var maxWait = 5000;\n  var interval = 250;\n  var elapsed = 0;\n  var poll = setInterval(function() {\n    elapsed += interval;\n    if (window.analytics && typeof window.analytics.track === \"function\") {\n      clearInterval(poll);\n      sendToSegment();\n    } else if (elapsed >= maxWait) {\n      clearInterval(poll);\n    }\n  }, interval);\n}\n"
  }
}
  1. Click Save Integration.

The form_schema creates a configurable event name field in the Optimizely UI. The default is "Experiment Viewed", but you can customize it per experiment if needed.

Step 2: How the Callback Works

The track_layer_decision callback fires each time Optimizely makes a bucketing decision. The code:

  1. Reads the configurable event name from extension.eventName.

  2. Uses state.getDecisionObject() to resolve human-readable experiment and variation names from the numeric IDs.

  3. Builds a properties object with full experiment context.

  4. Checks if window.analytics (Segment) is loaded. If loaded, sends immediately. If not, polls every 250ms for up to 5 seconds before giving up.

The polling mechanism handles the common race condition where the Optimizely snippet evaluates experiments before Segment's analytics.js has finished loading. Without this, early bucketing decisions would be silently lost.

Step 3: Enable the Integration

After saving the integration:

  1. Toggle the integration to Enabled in Settings > Integrations.

  2. Check Enable for all new experiments to apply automatically.

  3. For existing experiments, go to Manage Campaign > Integrations and enable the Segment integration.

Properties Sent to Segment

Each decision event includes these properties:

Property

Example Value

Description

experimentId

"1234567890123456"

16-digit experiment ID (sent as string to avoid precision loss)

experimentName

"Homepage Hero Test"

Human-readable experiment name

variationId

"9876543210987654"

16-digit variation ID (sent as string)

variationName

"Variation A"

Human-readable variation name

campaignId

"5555555555555555"

Campaign (layer) ID

campaignName

"Homepage Optimization"

Campaign name

isHoldback

false

Whether visitor is in holdback group

All IDs are cast to strings using String() to prevent JavaScript precision loss with 16-digit numbers. JavaScript Number can only safely represent integers up to 2^53, and Optimizely IDs can exceed this — sending them as numbers risks truncation or rounding in downstream tools.


Verification

Regardless of which option you chose, verify that events flow correctly.

Console Check

// Verify both SDKs are loaded
console.log("Segment loaded:", typeof window.analytics !== "undefined");
console.log("Optimizely loaded:", typeof window.optimizely !== "undefined");

// Check active experiments
var state = window.optimizely && window.optimizely.get("state");
if (state) {
  var campaigns = state.getCampaignStates({ isActive: true });
  for (var id in campaigns) {
    var c = campaigns[id];
    console.log("Campaign " + id + ":", {
      experimentId: c.experiment && c.experiment.id,
      variationId: c.variation && c.variation.id,
      variationName: c.variation && c.variation.name
    });
  }
}

Segment Debugger

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

  2. Click the Debugger tab.

  3. Open your site in a new browser tab and navigate to a page with an active experiment.

  4. Look for "Experiment Viewed" events in the debugger. Check that the properties match what you expect for your chosen option.

Expected Data in Downstream Tools

Once events reach Segment, they are forwarded to all connected destinations:

  • Amplitude / Mixpanel: "Experiment Viewed" appears as an event with experiment properties available for segmentation and funnel analysis.

  • BigQuery / Snowflake: Events land in your warehouse tables with all properties as columns, enabling SQL-based experiment analysis.

  • Google Analytics 4: Properties map to event parameters (subject to GA4's 25-parameter limit per event).

  • Braze / Iterable: Experiment data available for triggered messaging and personalization based on variation assignment.

A/A Testing Validation

Before relying on the integration for production experiments, run an A/A test to validate that data flows correctly.

  1. Create a new A/B test in Optimizely with two identical variations (no code changes).

  2. Set traffic allocation to 50/50.

  3. If using Option B, enable the Segment integration for this experiment.

  4. Run the test for at least 7 days or until you reach 1,000 visitors per variation.

  5. Validate that decision events appear in Segment with correct properties.

  6. Both variations should show statistically similar behavior. Differences greater than 5% on any metric suggest a timing or ID mapping issue.

flowchart TD
    A["Create A/A test in Optimizely"] --> B["Enable integration (Option A or B)"]
    B --> C["Run for 7+ days / 1000+ visitors per variation"]
    C --> D{"Events flowing correctly?"}
    D -->|Yes| E{"Metrics within 5% between variations?"}
    E -->|Yes| F["Integration validated — proceed with real experiments"]
    E -->|No| G["Check user ID mapping and event timing"]
    D -->|No| H["Check destination settings and snippet loading"]
    G --> I["Re-run A/A test"]
    H --> I

Gotchas and Common Pitfalls

Do Not Enable Both Options

If you enable the native Segment destination (with "Send experiment data to other tools" on) AND the Custom Analytics Integration, you will receive duplicate "Experiment Viewed" events in Segment. Pick one approach. If you need the native destination for Segment → Optimizely event forwarding but want richer decision events, disable "Send experiment data to other tools" in the Segment destination settings and use the Custom Analytics Integration for decisions only.

16-Digit ID Precision

Optimizely experiment, variation, and campaign IDs are 16-digit numbers. JavaScript Number type safely handles integers up to Number.MAX_SAFE_INTEGER (2^53 - 1 = 9007199254740991, a 16-digit number), but some downstream tools may truncate or round large numbers. The Custom Analytics Integration (Option B) handles this by casting IDs to strings. The native destination (Option A) sends IDs as strings by default.

Async Loading Race Condition

The Optimizely snippet typically loads and evaluates experiments before Segment's analytics.js finishes loading. This means window.analytics.track() may not be available when decisions fire.

  • Option A: The native destination handles this internally.

  • Option B: The JSON configuration above includes a polling mechanism that retries every 250ms for up to 5 seconds. If your analytics.js load time regularly exceeds 5 seconds, increase the maxWait value.

Preview Mode Does Not Fire Integration Code

Optimizely Preview Mode allows you to force a specific variation for testing, but it does not fire Custom Analytics Integration callbacks (Option B). The native destination (Option A) also does not fire in Preview Mode.

To test the full integration flow, use QA mode or add yourself to a URL-targeted experiment that activates on a specific query parameter.

Data Masking and PII

The decision events from both options send only experiment and variation data — no PII. However, if you modify the Option B callback to include user traits, ensure you comply with your organization's data governance policies and any Segment Privacy settings.

Troubleshooting

Events Not Reaching Optimizely (Option A only)

  • Destination not in device mode: The Segment Optimizely Web destination must run in device mode (client-side). Cloud mode cannot push events to window.optimizely. Verify the destination is configured for device mode in the Segment dashboard.

  • Optimizely snippet not loaded: Segment's device-mode integration looks for window.optimizely. If the snippet has not loaded yet, events are queued but may be lost on page navigation. Load the Optimizely snippet in the <head> tag before analytics.js.

  • Event name mismatch: Optimizely custom events are case-sensitive. Ensure the Segment track event name matches the Optimizely event name exactly.

  • Blocked by ad blockers: Both the Segment and Optimizely scripts may be blocked by browser ad blockers. Test in a clean browser profile with extensions disabled.

Experiment Decisions Not Reaching Segment

Option A:

  • Verify "Send experiment data to other tools" is enabled in the Segment destination settings.

  • Check the Segment debugger for "Experiment Viewed" events while browsing a page with an active experiment.

Option B:

  • analytics.js not loaded: The callback calls window.analytics.track(). If analytics.js has not loaded when the callback fires, the polling mechanism retries for up to 5 seconds. If your analytics.js loads slower, increase the timeout.

  • Callback syntax error: A JavaScript error in the track_layer_decision code prevents execution. Open the browser console and look for errors. Common issues include unescaped quotes in the JSON or missing semicolons.

  • Integration not enabled for experiment: The Custom Analytics Integration must be toggled on in Settings > Integrations AND enabled for the specific experiment in Manage Campaign > Integrations.

  • Preview Mode active: Preview Mode does not fire integration callbacks. Use QA mode or a live experiment for testing.

User ID Consistency

Segment uses anonymousId (auto-generated) and userId (set via analytics.identify()) to identify users. Optimizely uses its own visitor ID. If you need cross-platform identity resolution:

  • Call analytics.identify(userId) before or alongside Optimizely snippet loading.

  • For Option A, configure the Segment destination to pass the userId as the Optimizely user ID attribute.

  • Verify the mapping by comparing analytics.user().anonymousId() and window.optimizely.get("state").getVisitorId() in the console.

Missing Properties in Downstream Tools

If decision events appear in Segment but properties are missing in downstream destinations:

  • Schema enforcement: Some Segment destinations enforce schemas. If the destination has not seen campaignId or other properties before, it may reject them. Check the destination's schema settings and add the properties if needed.

  • Property name mapping: Some destinations rename properties. For example, Google Analytics 4 lowercases parameter names. Verify the property mapping in the destination settings.

  • Null values: If state.getDecisionObject() returns null (rare, but possible during Optimizely initialization), the properties will contain the raw numeric ID as a fallback string instead of the human-readable name.

Optimizely tips, straight to your inbox

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