Integrate FullStory with Optimizely Web Experimentation

Loading...·12 min read

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

campaign_id

string

The campaign (layer) ID in Optimizely

experiment_id

string

The experiment ID within the campaign

experiment_name

string

Human-readable experiment name

variation_id

string

The assigned variation ID

variation_name

string

Human-readable variation name or "holdback"

is_holdback

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 FS.event() (V1)

Current FS('trackEvent') / FS('setProperties') (V2)

Works on a modern FullStory install

No — V1 methods are removed

Yes

Setup

Visual Editor, paste snippet

Paste JSON plugin

Event name

Experiment

Experiment Viewed (customizable)

User-level segmentation

No

Yes (setProperties type user)

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.

  1. In FullStory, go to Settings and copy your recording snippet.

  2. 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.

  3. In Optimizely Settings > Privacy, uncheck Mask descriptive names in project code and third-party integrations and click Save.

  4. Go to Settings > Integrations and click Create Analytics Integration.

  5. Select Using Visual Editor, name it (for example, FullStory IDs), and click Create Custom Analytics Integration.

  6. 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');
      }
    }
  });
})();
  1. Enable the integration under Settings > Integrations by toggling Enable Integration on.

  2. 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

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

  2. Click Create Analytics Integration > Using JSON.

  3. 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"
  }
}
  1. Click Save Integration.

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

  3. Optionally check Enable for all new experiments.

  4. 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

  1. In the FullStory application, open Sessions and use OmniSearch.

  2. Browse a page with an active experiment in a separate tab to generate a session.

  3. Search for the event Experiment Viewed (custom integration) or Experiment (Optimizely's documented integration).

  4. Add a property filter for experiment_name or variation_name and confirm the values match the variation you were assigned.

  5. 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):

  1. Open a session from the verification run and click into the User Card.

  2. Look for a property named [Optimizely] Your Experiment Name.

  3. 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:

  1. In OmniSearch, build a query for users who fired Experiment Viewed where experiment_name equals your experiment AND variation_name equals "Variation A".

  2. Save it as a segment, for example "Checkout Redesign — Variation A".

  3. 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:

  1. Go to Metrics > Funnels and create a funnel (e.g., Product View → Add to Cart → Checkout → Purchase).

  2. Apply your variation segment, or add a funnel-level filter on the variation_name property.

  3. 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.com and edge.fullstory.com. Check the network tab for blocked requests; waitUntil will wait indefinitely if the FS function 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

  • setProperties with type: 'user' applies to the identified user. If the visitor is anonymous and never identified via FS('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.

Related guides