Integrate Segment with Optimizely Web Experimentation
TL;DR
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 | Not handled. You must load the Optimizely snippet and use |
Decisions → Segment | Automatic "Experiment Viewed" event with basic properties | Custom |
Properties on decision events |
| All of the above plus |
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
Log into your Segment workspace.
Navigate to Connections > Destinations.
Click Add Destination and search for Optimizely Web.
Select the source you want to connect (your website source).
Enter your Optimizely Project ID in the destination settings.
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 |
Track Categorized Pages | Enabled | Forwards categorized page calls |
Custom experiment properties | As needed | Additional properties to include in "Experiment Viewed" events |
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 |
|---|---|
|
|
|
|
|
|
|
|
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, orisHoldbackproperties 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 |
|---|---|---|
| number | The campaign (layer) ID — a 16-digit numeric identifier |
| number | The experiment ID within the campaign |
| number | The assigned variation ID |
| boolean | Whether the visitor is in the holdback group |
| object | Full campaign object with |
| object | Custom settings from the |
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
In your Optimizely project, go to Settings > Integrations.
Click Create Analytics Integration > Using JSON.
Paste the following configuration. The
options.track_layer_decisionfield 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"
}
}
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:
Reads the configurable event name from
extension.eventName.Uses
state.getDecisionObject()to resolve human-readable experiment and variation names from the numeric IDs.Builds a properties object with full experiment context.
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:
Toggle the integration to Enabled in Settings > Integrations.
Check Enable for all new experiments to apply automatically.
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 |
|---|---|---|
|
| 16-digit experiment ID (sent as string to avoid precision loss) |
|
| Human-readable experiment name |
|
| 16-digit variation ID (sent as string) |
|
| Human-readable variation name |
|
| Campaign (layer) ID |
|
| Campaign name |
|
| 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
In your Segment workspace, navigate to Connections > Sources > your website source.
Click the Debugger tab.
Open your site in a new browser tab and navigate to a page with an active experiment.
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.
Create a new A/B test in Optimizely with two identical variations (no code changes).
Set traffic allocation to 50/50.
If using Option B, enable the Segment integration for this experiment.
Run the test for at least 7 days or until you reach 1,000 visitors per variation.
Validate that decision events appear in Segment with correct properties.
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
maxWaitvalue.
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_decisioncode 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
userIdas the Optimizely user ID attribute.Verify the mapping by comparing
analytics.user().anonymousId()andwindow.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
campaignIdor 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.
Also available for
Related articles
Optimizely tips, straight to your inbox
Practical guides and patterns for experimentation practitioners. No spam, unsubscribe anytime.