Integrate FullStory with Optimizely Web Experimentation
TL;DR
FullStory is a digital experience analytics platform built around full session replay — it records every click, scroll, rage-click, and page transition a visitor makes, then layers funnels, conversions, and frustration signals on top of those recordings. Integrating FullStory with Optimizely Web Experimentation attaches experiment and variation data to each recorded session, so you can replay exactly how visitors in a given variation behaved, filter funnels by variation, and quantify the qualitative "why" behind an experiment's win or loss.
This guide covers two integration methods. The first is Optimizely's officially documented FullStory integration, which uses the Custom Analytics Integration Visual Editor and FullStory's legacy FS.event() API to send an "Experiment" event. The second is a custom JSON integration that uses FullStory's current V2 Browser API (FS('trackEvent', …) and FS('setProperties', …)) through Optimizely's track_layer_decision callback, giving you control over property naming and user-level segmentation. A dedicated section explains why the API version matters — Optimizely's published snippet predates FullStory's V2 rewrite, and copying it verbatim onto a modern FullStory install will silently send nothing.
How the Integration Works
When Optimizely buckets a visitor into an experiment or personalization campaign, it fires a decision callback. The custom integration captures this callback and sends data to FullStory in two forms: a discrete trackEvent call named "Experiment Viewed" carrying campaign metadata, and user-scoped properties set via setProperties so every future session for that identified user inherits the experiment context. FullStory then indexes this data for OmniSearch, making it possible to find and replay sessions by experiment name or variation.
flowchart LR
A[Visitor lands on page] --> B[Optimizely makes bucketing decision]
B --> C[Decision callback fires]
C --> D["FS('setProperties', type:'user') sets experiment context"]
C --> E["FS('trackEvent', 'Experiment Viewed')"]
D --> F[User properties attach to the session]
E --> G[FullStory event stream]
F --> H[FullStory: OmniSearch, segments, replays]
G --> H
H --> I[Filter funnels and replays by variation]
Unlike a pure product-analytics tool, FullStory's primary value here is the session recording. The experiment property is not just a dimension for charts — it is a search facet that lets you pull up the actual video of every visitor who saw Variation B and abandoned the checkout.
Custom Integration Event Properties
The following properties are sent with the "Experiment Viewed" event in the custom integration:
Property | Type | Description |
|---|---|---|
| string | The campaign (layer) ID in Optimizely |
| string | The experiment ID within the campaign |
| string | Human-readable experiment name |
| string | The assigned variation ID |
| string | Human-readable variation name or |
| boolean | Whether the visitor is in the holdback group |
The experiment_name/variation_name pair is also set as a user property (keyed [Optimizely] ExperimentName) so it is available for segmentation and persists on the user across sessions.
Prerequisites
Before configuring either integration method, confirm the following:
FullStory recording snippet is installed and capturing sessions. You can install it site-wide in your own
<head>, or paste it into Optimizely Settings > JavaScript (Project JavaScript) as Optimizely's documentation describes.Optimizely Web Experimentation snippet is deployed on the same pages.
Mask descriptive names is disabled (Optimizely Settings > Privacy), otherwise FullStory receives numeric IDs instead of readable experiment and variation names.
You have access to the FullStory application to verify incoming events via OmniSearch.
You have admin access to your Optimizely project (Settings > Integrations) for either integration method.
Load Order
FullStory's snippet defines a global command function — by default named FS, configurable through window._fs_namespace. The snippet queues calls made before the main library finishes downloading, so FS('trackEvent', …) issued early is buffered and replayed once FullStory initializes. Even so, the custom integration uses window.optimizely.get('utils').waitUntil() to defer execution until the FS function exists, which guards against the case where the FullStory script is blocked or delayed entirely.
A typical site-wide <head> configuration looks like this:
<head>
<!-- 1. FullStory V2 recording snippet (loads first).
Copy the complete snippet from FullStory > Settings > FullStory Setup.
It hardcodes your org ID and defines the FS command function below. -->
<script>
window['_fs_host'] = 'fullstory.com';
window['_fs_script'] = 'edge.fullstory.com/s/fs.js';
window['_fs_org'] = 'YOUR_ORG_ID';
window['_fs_namespace'] = 'FS';
// ... remainder of FullStory's official V2 snippet (the FS command-queue
// function) follows here — paste it verbatim from your account ...
</script>
<!-- 2. Optimizely Web snippet (loads second) -->
<script src="https://cdn.optimizely.com/js/YOUR_PROJECT_ID.js"></script>
</head>
Copy the full snippet — including the command function body shown above only in outline — from FullStory > Settings > FullStory Setup; it already contains your org ID. Do not reconstruct the snippet by hand, as FullStory revises it between SDK versions. If you install it through Optimizely Project JavaScript instead, paste it without the surrounding <script> tags, as Optimizely's documentation notes. The only part the integration depends on is the global command function, named FS by default and configurable via window['_fs_namespace'].
Choosing an Integration Method
Optimizely's Documented Integration | Custom JSON Integration | |
|---|---|---|
API used | Legacy | Current |
Works on a modern FullStory install | No — V1 methods are removed | Yes |
Setup | Visual Editor, paste snippet | Paste JSON plugin |
Event name |
|
|
User-level segmentation | No | Yes ( |
Holdback handling | Not in default snippet | Yes |
Important: If your account is on FullStory's V2 snippet (any install created after the V1 deprecation), use Method 2. The V1
FS.event()/window['_fs_namespace']()accessor used in Optimizely's published snippet no longer exists and the calls fail silently. Method 1 is documented here for completeness and for the small number of legacy V1 accounts.
Method 1: Optimizely's Documented Integration (Legacy V1 API)
This is the integration exactly as published in Optimizely's developer documentation. It uses the Custom Analytics Integration Visual Editor and FullStory's legacy FS.event() API.
In FullStory, go to Settings and copy your recording snippet.
In Optimizely, go to Settings > JavaScript and paste the FullStory snippet into the Project JavaScript field — without the
<script>tags. FullStory now records sessions for your visitors.In Optimizely Settings > Privacy, uncheck Mask descriptive names in project code and third-party integrations and click Save.
Go to Settings > Integrations and click Create Analytics Integration.
Select Using Visual Editor, name it (for example, FullStory IDs), and click Create Custom Analytics Integration.
Paste the following into the custom code field and click Save:
(function () {
function _fs() { return window[window['_fs_namespace']]; }
var utils = window.optimizely.get('utils');
utils.waitUntil(function () {
return typeof _fs() === 'function';
}).then(function () {
var campaignStates = window.optimizely.get('state').getCampaignStates({ isActive: true });
for (var campaignId in campaignStates) {
var c = campaignStates[campaignId];
if (c.isInCampaignHoldback !== true) {
var payload = {};
payload.campaign = {};
payload.campaign.id_str = campaignId;
payload.campaign.name_str = c.campaignName;
if (c.experiment) {
payload.experiment = {};
payload.experiment.id_str = c.experiment.id;
payload.experiment.name_str = c.experiment.name;
}
if (c.variation) {
payload.variation = {};
payload.variation.id_str = c.variation.id;
payload.variation.name_str = c.variation.name;
}
_fs().event('Experiment', payload, 'Optimizely');
}
}
});
})();
Enable the integration under Settings > Integrations by toggling Enable Integration on.
For each experiment, open it, go to Manage Experiments > Integrations, select the Tracked checkbox for the FullStory integration, and click Save.
Note the _str suffixes on every property: FullStory's V1 API used type-tagged property names (_str, _int, _real, _bool) to enforce a value's type. The V2 API drops this convention in favor of an optional schema object, which is one of several reasons the two methods are not interchangeable.
You can then search FullStory's OmniSearch for "experiment," "campaign," or "variation" and the corresponding values to find matching sessions.
Method 2: Custom JSON Integration (Current V2 API)
The custom JSON integration uses FullStory's current V2 Browser API and Optimizely's track_layer_decision callback. This is the method to use on any modern FullStory install.
Creating the JSON Integration
In your Optimizely project, go to Settings > Integrations.
Click Create Analytics Integration > Using JSON.
Paste the following configuration:
{
"plugin_type": "analytics_integration",
"name": "FullStory (Custom V2)",
"form_schema": [],
"description": "Sends Optimizely experiment decisions to FullStory using the V2 Browser API as a trackEvent and user properties",
"options": {
"track_layer_decision": "var state = window['optimizely'].get('state');\nvar fnName = window['_fs_namespace'] || 'FS';\nvar expName = String(campaignId);\nvar varName = String(variationId);\n\nif (state) {\n try {\n var decisionObj = state.getDecisionObject({ campaignId: campaignId });\n if (decisionObj) {\n expName = decisionObj.experiment || expName;\n varName = decisionObj.variation || varName;\n }\n } catch (e) {}\n}\n\nvar variationValue = isHoldback ? 'holdback' : varName;\nvar propertyKey = '[Optimizely] ' + expName;\n\nvar utils = window['optimizely'].get('utils');\nutils.waitUntil(function() {\n return typeof window[fnName] === 'function';\n}).then(function() {\n var FS = window[fnName];\n\n var userProps = {};\n userProps[propertyKey] = variationValue;\n FS('setProperties', { type: 'user', properties: userProps });\n\n FS('trackEvent', {\n name: 'Experiment Viewed',\n properties: {\n campaign_id: String(campaignId),\n experiment_id: String(experimentId),\n experiment_name: expName,\n variation_id: String(variationId),\n variation_name: variationValue,\n is_holdback: isHoldback\n }\n });\n});\n"
}
}
Click Save Integration.
Toggle the integration to Enabled in Settings > Integrations.
Optionally check Enable for all new experiments.
For existing experiments, open each experiment's Manage Experiments > Integrations tab and enable the "FullStory (Custom V2)" integration.
What the Callback Does
The options.track_layer_decision callback fires each time Optimizely makes a bucketing decision. Here is a breakdown of the logic:
Resolving the FullStory namespace: The callback reads window['_fs_namespace'] (defaulting to FS) rather than hard-coding FS. FullStory lets customers rename the global function, and a hard-coded name breaks the integration on those accounts.
var fnName = window['_fs_namespace'] || 'FS';
Retrieving human-readable names: state.getDecisionObject({ campaignId }) looks up experiment and variation names from Optimizely's state API. If Mask descriptive names is enabled, the lookup returns numeric IDs and the callback falls back to them.
var state = window.optimizely.get('state');
var expName = String(campaignId);
var varName = String(variationId);
if (state) {
try {
var decisionObj = state.getDecisionObject({ campaignId: campaignId });
if (decisionObj) {
expName = decisionObj.experiment || expName;
varName = decisionObj.variation || varName;
}
} catch (e) {}
}
Waiting for FullStory to be ready: optimizely.get('utils').waitUntil() defers execution until the FS function exists. The snippet's own command queue buffers early calls, but waitUntil avoids referencing an undefined global if the script is blocked.
Setting user properties: FS('setProperties', { type: 'user', properties }) attaches the experiment context to the identified user. The property is keyed [Optimizely] ExperimentName with the variation name (or "holdback") as the value.
var userProps = {};
userProps['[Optimizely] ' + expName] = variationValue;
FS('setProperties', { type: 'user', properties: userProps });
Tracking the event: FS('trackEvent', { name, properties }) records a discrete "Experiment Viewed" event with full campaign metadata. This event is what OmniSearch and FullStory's metrics index, enabling funnels and conversions that start from experiment entry.
FS('trackEvent', {
name: 'Experiment Viewed',
properties: {
campaign_id: String(campaignId),
experiment_id: String(experimentId),
experiment_name: expName,
variation_id: String(variationId),
variation_name: variationValue,
is_holdback: isHoldback
}
});
Verifying the Integration
After enabling either integration method, verify that data reaches FullStory before treating results as reliable.
Console Verification
Open your browser's developer console on a page with an active experiment:
// Check if FullStory is loaded (respect a custom namespace)
var fnName = window._fs_namespace || 'FS';
console.log('FullStory loaded:', typeof window[fnName] === 'function');
// Check Optimizely experiment state
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,
isHoldback: c.isInCampaignHoldback
});
}
}
// Manually fire a test event to confirm the V2 API is wired up
window[fnName]('trackEvent', { name: 'Integration Test', properties: { source: 'console' } });
FullStory OmniSearch
In the FullStory application, open Sessions and use OmniSearch.
Browse a page with an active experiment in a separate tab to generate a session.
Search for the event Experiment Viewed (custom integration) or Experiment (Optimizely's documented integration).
Add a property filter for
experiment_nameorvariation_nameand confirm the values match the variation you were assigned.Open a matching session and confirm you can watch the replay for that variation.
FullStory User Card
To verify user properties are set (custom integration only):
Open a session from the verification run and click into the User Card.
Look for a property named
[Optimizely] Your Experiment Name.Confirm the value matches your assigned variation.
Analyzing Experiments in FullStory
Segments and OmniSearch
FullStory Segments save a set of search criteria for reuse. Create one segment per variation:
In OmniSearch, build a query for users who fired Experiment Viewed where
experiment_nameequals your experiment ANDvariation_nameequals "Variation A".Save it as a segment, for example "Checkout Redesign — Variation A".
Repeat for the control.
Apply these segments across replays, funnels, and metrics to compare behavior between groups.
Funnels and Conversions
Funnels filtered by variation are the most direct way to measure experiment impact on a conversion sequence:
Go to Metrics > Funnels and create a funnel (e.g., Product View → Add to Cart → Checkout → Purchase).
Apply your variation segment, or add a funnel-level filter on the
variation_nameproperty.Compare step-by-step conversion rates between control and treatment, and click any drop-off to watch the sessions where users abandoned.
The ability to jump from a funnel drop-off straight into the session replay is FullStory's distinct advantage over chart-only analytics tools.
Frustration Signals
FullStory automatically detects rage clicks, dead clicks, and error clicks. Filter these signals by your variation segment to see whether a treatment introduces friction that a conversion metric alone would not reveal — a variation can win on clicks while quietly generating more rage clicks on a downstream page.
Session Replay Review
The simplest and highest-value workflow: filter sessions to a single variation and watch a sample of replays end to end. This surfaces the qualitative reasons behind a quantitative result — confusing copy, a mis-rendered element, or an unexpected navigation pattern that the experiment introduced.
Gotchas
V1 vs V2 API — The Most Common Failure
Optimizely's published FullStory snippet uses the legacy FS.event() API and the window[window['_fs_namespace']]() accessor. FullStory's V2 API replaced these with the FS('trackEvent', …) command syntax. If you paste Optimizely's documented snippet onto a V2 install, the integration runs without throwing a visible error but sends nothing. Confirm which API your account exposes before choosing a method: in the browser console, V2 makes FS a function you call as FS('trackEvent', …) (typeof FS === 'function'), whereas V1 exposed object methods such as FS.event(…) and FS.setUserVars(…). The script URL is not a reliable signal — both versions can load edge.fullstory.com/s/fs.js. Use Method 2 for V2.
Mask Descriptive Names Hides Readable Values
If Mask descriptive names is enabled in Optimizely's privacy settings, FullStory receives 16-digit numeric IDs instead of experiment and variation names, making OmniSearch effectively unusable for experiment analysis. Disable it — but note that doing so exposes your variation names in client-side source code.
16-Digit IDs Can Be Mistaken for Card Numbers
Optimizely's experiment and variation IDs are 16-digit numbers. FullStory's data-handling and any downstream redaction tooling may flag a bare 16-digit string as a credit card number and strip it. Send IDs as strings combined with a name (the callbacks above use String(...) and pair IDs with names) rather than as raw 16-digit values.
Respect a Custom Namespace
FullStory lets customers rename the global function via window._fs_namespace. Hard-coding FS breaks the integration on those accounts. Both the verification snippet and the custom integration above resolve the namespace dynamically.
Browser API Rate Limits
FullStory throttles its V2 Browser API: trackEvent is limited to roughly 60 calls per user per page per minute, and setProperties to roughly 30 calls per page per minute. A page running many simultaneous experiments that each fire a decision can approach these limits. If you run dozens of concurrent experiments on one page, batch the experiment context into a single setProperties call rather than one per experiment.
Holdback Visitors
Optimizely's documented snippet skips holdback visitors entirely (if (c.isInCampaignHoldback !== true)). The custom integration instead records them with variation_name: 'holdback', which lets you analyze holdback behavior in FullStory. Choose the behavior that matches your analysis needs and be consistent.
Troubleshooting
No Events Appear in FullStory
Wrong API version: The single most common cause. Verify your snippet is V2 and that you are using Method 2's
FS('trackEvent', …)syntax.Integration not enabled: Confirm the integration is toggled on in Settings > Integrations and enabled (Tracked) for the specific experiment.
FullStory blocked: Ad blockers and privacy extensions frequently block
fullstory.comandedge.fullstory.com. Check the network tab for blocked requests;waitUntilwill wait indefinitely if theFSfunction never appears.Visitor not bucketed: The callback fires only on an active bucketing decision. If the visitor does not meet the experiment's audience conditions, no decision fires.
Events Appear but Names Are Numeric
Mask descriptive names is enabled. Disable it in Optimizely Settings > Privacy, then generate a fresh session — existing sessions are not retroactively renamed.
User Properties Not Showing on the User Card
setPropertieswithtype: 'user'applies to the identified user. If the visitor is anonymous and never identified viaFS('setIdentity', …), the properties attach to the anonymous user record for that session but will not merge with a known user profile.Confirm the property key does not exceed FullStory's limits and that the value is a supported type (string, number, or boolean).
Data Discrepancies Between Platforms
Differences between Optimizely visitor counts and FullStory session counts are expected:
Counting unit: Optimizely counts unique cookie-based visitors; FullStory counts sessions and resolves identity through its own model.
Sampling and capture limits: Depending on your FullStory plan, not every session is captured, which lowers FullStory counts relative to Optimizely decisions.
Ad blockers: These may block FullStory or Optimizely independently, skewing counts in either direction.
Preview and QA sessions: Optimizely Preview Mode fires the decision callback, so QA traffic appears in production FullStory unless filtered.
Expect a 5–15% discrepancy between Optimizely visitor counts and FullStory session counts for the same experiment. Investigate further if the gap exceeds 20% or exceeds your plan's sampling rate.