Forward Amplitude Events to Optimizely Web Experimentation
TL;DR
Try it live
Run this integration in the browser: fire an event on a test page and watch the payload it forwards, field by field, as it is sent.
A site that already tracks its funnel in Amplitude does not need a second tracking layer to run experiments. Optimizely Web Experimentation measures whatever custom events it receives through its own JavaScript queue, and the events you care about are already being emitted — by the Amplitude SDK, from code your team has already reviewed, with names your analysts already agree on. The work is not re-instrumentation. It is deciding which of those Amplitude events should also count as an Optimizely conversion, and mirroring exactly those.
This guide covers the two ways to mirror them in the browser: adding an Optimizely call next to the Amplitude call in your own analytics helper, and installing an Amplitude SDK plugin that mirrors events automatically without touching any call site. It is the inbound counterpart to sending Optimizely decisions into Amplitude, and the Feature Experimentation version of the same job is covered in forwarding Amplitude events to Feature Experimentation.
How Inbound Event Forwarding Works
Optimizely Web Experimentation exposes a global command queue. Anything pushed onto it with a type of event is recorded as a custom event against the current visitor, and attributed to whichever experiments and variations that visitor is already in. There is no identity to reconcile and no API key to manage in the page: the snippet already knows who the visitor is from its own cookie, so a forwarded event only has to name the event and, optionally, carry tags.
flowchart LR
A[Visitor completes an action] --> B[Amplitude SDK track call]
B --> C{Mirroring point}
C -->|Option A: your analytics helper| D[Optimizely queue push]
C -->|Option B: Amplitude SDK plugin| D
D --> E[Optimizely snippet records custom event]
E --> F[Metrics on the experiment Results page]What the Optimizely Event Queue Accepts
The push takes an object with a type of event, an eventName matching the API name of a custom event created in the Optimizely UI, and two optional bags. Tags carry the reserved keys, revenue as an integer number of cents and value as a number. Properties carry the descriptive fields a metric can later be filtered by, up to fifteen per event, and each one has to be created in the Optimizely UI before it will be stored — five predefined names plus ten custom slots.
Two limits follow from this and shape everything below. Only events named in Optimizely count, so the mapping between the two taxonomies is explicit rather than automatic. And the snippet has to be on the page and loaded before the push happens, or the queue absorbs the call and nothing is recorded.
Choosing Between the Two Options
These are alternatives. Pick one for a given event and stay with it.
Consideration | Option A: mirror at the call site | Option B: Amplitude SDK plugin |
|---|---|---|
Where the mirror lives | Inside your analytics helper | Inside the Amplitude SDK pipeline |
Events covered | The ones you edit | Every event the SDK sends, minus your filter |
Visible at the call site | Yes, in the same function | No, one install point far away |
Effort to add an event | Edit the map, ship the bundle | Edit the map, ship the bundle |
Risk of over-forwarding | Low, nothing forwards by default | Real, the allowlist is the only guard |
Requires Browser SDK 2 | No | Yes, the plugin interface is 2.x |
Option A is the better default when a handful of conversions matter and you want a reviewer to see both calls together. Option B earns its place when the Amplitude calls are scattered across many bundles or emitted by autocapture, because the plugin sees events the helper never does.
The choice is not permanent, but it should be deliberate per event rather than per team. A site can reasonably mirror its three checkout conversions at the call site, where the code is read most often, and leave everything else alone. What it must not do is enable both mechanisms for the same event name, which is the failure the Gotchas section returns to.
Naming the Two Taxonomies
The mapping is the part that outlives the code, so write it down where both systems can be checked against it. Amplitude event names are typically title case with spaces, chosen by whoever instrumented the feature; Optimizely API names are snake case, constrained by the UI, and frozen once a metric depends on them.
Two rules save trouble later. Never reuse an Optimizely API name for a different Amplitude event, even after the first one is retired — historical metrics keep pointing at it. And never rename an Amplitude event without checking the map, because a rename that misses the mapping table silently stops the mirror without stopping the Amplitude tracking, which is the hardest version of this bug to notice.
Prerequisites
The Optimizely Web Experimentation snippet deployed on the pages where the events fire. A push on a page without the snippet is silently dropped.
Custom events created in Optimizely for every event you intend to forward, with their API names noted. Create any event properties you want to filter metrics by at the same time.
The Amplitude Browser SDK already installed and initialised. Option B additionally requires Browser SDK 2.x, which is where the current plugin interface lives.
Agreement on the mapping. Amplitude event names are usually human-readable strings with spaces; Optimizely API names are usually snake case. Write the mapping down once, in one file, and let both options read it.
Option A: Mirror Events at the Call Site
Most teams already funnel Amplitude calls through a helper rather than calling the SDK directly from components. That helper is the right place for the mirror: one function, one mapping table, no changes anywhere else.
Wrapping Your Analytics Helper
import * as amplitude from '@amplitude/analytics-browser';
// Amplitude event name -> Optimizely custom event API name
const FORWARDED_EVENTS = {
'Checkout Completed': 'purchase_completed',
'Add To Cart': 'add_to_cart',
'Newsletter Signup': 'newsletter_signup',
};
export function track(eventName, properties = {}) {
amplitude.track(eventName, properties);
const optimizelyEventName = FORWARDED_EVENTS[eventName];
if (!optimizelyEventName) return;
window['optimizely'] = window['optimizely'] || [];
window['optimizely'].push({
type: 'event',
eventName: optimizelyEventName,
tags: buildTags(properties),
properties: buildProperties(properties),
});
}
The allowlist is doing the important work. Without it every Amplitude event would need a matching Optimizely event to exist, and any that did not would be quietly discarded — a failure mode that looks exactly like the integration working.
Mapping Revenue and Event Properties
Optimizely's tags and properties are not interchangeable. Revenue and value are reserved tag keys used to compute metrics; everything descriptive belongs in properties, where the metric builder can filter on it.
function buildTags(properties) {
const tags = {};
if (typeof properties.revenue === 'number') {
// Optimizely records revenue in cents, as an integer.
tags.revenue = Math.round(properties.revenue * 100);
}
if (typeof properties.quantity === 'number') {
tags.value = properties.quantity;
}
return tags;
}
function buildProperties(properties) {
// Only properties created in the Optimizely UI are stored.
return {
Category: properties.category,
Subcategory: properties.subcategory,
SKU: properties.sku,
};
}
Rounding to an integer is not optional. Optimizely discards a revenue tag that is not an integer, so a cart total of 54.99 sent unconverted produces a metric that reports no revenue at all rather than an obviously wrong one.
Option B: Forward With an Amplitude SDK Plugin
Browser SDK 2 lets you register a plugin whose execute method runs for every event the SDK processes. An enrichment plugin returns the event unchanged and uses the visit as a side channel to push the mirrored event onto the Optimizely queue. Nothing at the call site changes, and events emitted by parts of the codebase you do not own are covered too.
Writing the Enrichment Plugin
import * as amplitude from '@amplitude/analytics-browser';
const FORWARDED_EVENTS = {
'Checkout Completed': 'purchase_completed',
'Add To Cart': 'add_to_cart',
'Newsletter Signup': 'newsletter_signup',
};
const optimizelyForwarder = {
name: 'optimizely-forwarder',
type: 'enrichment',
async setup() {
window['optimizely'] = window['optimizely'] || [];
return undefined;
},
async execute(event) {
const eventName = FORWARDED_EVENTS[event.event_type];
if (!eventName) return event;
const properties = event.event_properties || {};
const tags = {};
if (typeof properties.revenue === 'number') {
tags.revenue = Math.round(properties.revenue * 100);
}
window['optimizely'].push({
type: 'event',
eventName: eventName,
tags: tags,
});
// An enrichment plugin must return the event, or the SDK drops it.
return event;
},
};
amplitude.add(optimizelyForwarder);
amplitude.init('YOUR_AMPLITUDE_API_KEY');
The return event at the end is not decoration. An enrichment plugin whose execute returns nothing removes the event from the Amplitude queue, so a forwarder written carelessly stops the analytics it was meant to leave alone. Returning null deliberately is how you drop an event; returning undefined by accident does the same thing.
Registering the Plugin Before Init
Register the plugin before init, as above. The SDK calls setup when you add the plugin or on first initialisation, whichever happens later, and only events processed after registration reach execute — anything tracked in the window between init and a late amplitude.add call is never mirrored.
Because the plugin runs inside the SDK rather than on the page, it also runs for identify and revenue events. Filter on event.event_type and let everything else through untouched, which the allowlist above already does.
Filtering Which Events Cross Over
The plugin sees every event, which is its advantage and its main risk. If your project has autocapture enabled, page views, clicks and form submissions all pass through execute. Two guards are worth adding.
const IGNORED_PREFIXES = ['[Amplitude]', '$'];
function shouldForward(eventType) {
if (!eventType) return false;
if (IGNORED_PREFIXES.some((prefix) => eventType.startsWith(prefix))) return false;
return Object.prototype.hasOwnProperty.call(FORWARDED_EVENTS, eventType);
}
The prefix check keeps Amplitude's own instrumented events out of the mapping table entirely, and the hasOwnProperty check stops an event named after an object prototype member from matching by accident.
Verifying the Forwarded Events
Checking the Browser Console
Trigger the action on a page with an active experiment and inspect what the snippet recorded.
var state = window.optimizely && window.optimizely.get('state');
console.log('Optimizely loaded:', !!state);
console.log('Active campaigns:', state && state.getCampaignStates({ isActive: true }));
// Fire the mapped event by hand and confirm the network request follows.
window['optimizely'].push({ type: 'event', eventName: 'purchase_completed' });
Filter the network tab for requests to the Optimizely logging endpoint. A conversion produces one request carrying the event name. If the manual push produces a request and the real user action does not, the problem is in your mapping or in the SDK, not in Optimizely.
Checking the Results Page
Add the custom event as a metric on a running experiment and complete the flow as a test visitor. The conversion should appear against your variation within a few minutes. Zero conversions with a visible network request means the event name does not match an event that exists in the project — the API name has to match exactly, including case.
Validating With an A/A Test
Before a forwarded event carries a real decision, prove it is unbiased. Run an A/A test — two identical variations at even traffic — with the mirrored event as its primary metric, and leave it running long enough to accumulate a few thousand visitors per arm.
What you are looking for is a flat result. If one arm shows a consistent lift on an event that no code path distinguishes, the mirror is firing unevenly: a common cause is the forwarding call sitting inside variation code rather than in shared code, so only one arm ever pushes it. A second cause is a race, where the mirror runs before the snippet has activated on slower connections that happen to correlate with a variation's extra payload.
An A/A test also gives you a baseline for the discrepancy you should expect between Amplitude's count of the event and Optimizely's. Record that number. Every later investigation into "the numbers do not match" starts by comparing against it rather than against zero.
Gotchas
Do Not Run Both Options at Once
If the analytics helper mirrors an event and the plugin mirrors it as well, the same conversion is pushed twice and Optimizely counts the duplicate as a second event for that visitor. Conversion rates rise, the confidence interval narrows around the wrong number, and nothing in either dashboard flags it. Choose one mechanism per event name, and when you migrate from one to the other, remove the old path in the same release that adds the new one.
Events Fired Before the Snippet Loads
The Optimizely queue absorbs pushes made before the snippet finishes loading only if the global array exists. Initialising it with the standard guard before pushing is what makes an early call survive.
<head>
<!-- 1. Optimizely snippet, as high in the head as practical -->
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>
<!-- 2. Amplitude SDK and the forwarder -->
<script src="https://cdn.amplitude.com/libs/analytics-browser-2.11.4-min.js.gz"></script>
</head>
A visitor who converts on the landing page before either script parses is lost to both systems, which is one more reason to keep the two loading close together.
Single Page Applications Keep the Queue Alive
In a single page application the snippet loads once and the queue persists across route changes. That is convenient for forwarding, but it means a mirrored event fired during a client-side navigation is attributed to whatever experiment state is current at that moment. Push the event at the point the action completes rather than batching mirrors until the next page load.
Ad Blockers Hit the Two Systems Differently
Privacy extensions frequently block one analytics host and not the other. When they block Amplitude, Option B stops mirroring entirely because the SDK never runs; when they block Optimizely, the pushes queue up and go nowhere. Neither produces an error you can catch, so treat a persistent gap between the two systems as expected rather than as a bug to chase.
Troubleshooting
The Event Fires but No Conversion Appears
Confirm the event exists in the Optimizely project and that the API name matches the string you push, exactly. Confirm the visitor is actually in an experiment — a push from a visitor who does not meet any audience condition is recorded but has nothing to attribute to. Then confirm the metric is attached to the experiment you are reading.
Event Properties Are Missing From the Metric
Properties must be created in the Optimizely UI before they are stored, and there are only fifteen slots per event. Anything sent under a name that has not been created is dropped without an error. Check the property names in the UI against the keys your buildProperties function emits.
Revenue Is Zero or Wildly Wrong
Almost always a units problem. Revenue is an integer number of cents, so a float is discarded and a value already in cents that gets multiplied again is a hundred times too large. Log the exact tag object you push for one real conversion and read the number.
The Plugin Stopped Amplitude From Tracking
An enrichment plugin that does not return the event drops it. If Amplitude volume falls the moment the forwarder ships, check that every path through execute — including the early return for unmapped events — returns the event object.
Related Reading
Send Optimizely decisions into Amplitude — the outbound direction, for segmenting Amplitude by variation.
Forward Amplitude events to Feature Experimentation — the same job on the SDK side.
Event properties and user attributes — which values belong in tags and which describe the visitor.
Optimizely: the event push reference and custom events.
Optimizely: configure revenue tracking.
Amplitude: SDK plugins and Browser SDK 2.