> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fullstory

<View title="Developer" />

<View title="Federal docs">
  <Danger>
    **This integration is not available in federal environments**

    To learn more, read <a href="/docs/home/infrastructure/federal">LaunchDarkly in federal environments</a>.
  </Danger>
</View>

<View title="EU docs">
  <Danger>
    **This integration is not available in LaunchDarkly's European Union (EU) instance**

    To learn more, read <a href="/docs/home/infrastructure/eu">LaunchDarkly in the European Union (EU)</a>.
  </Danger>
</View>

This topic explains how to send LaunchDarkly flag evaluations to Fullstory, so that you can segment session replays and analytics by the flag variations your end users received.

Fullstory combines analytics with high-fidelity session replay and intelligent diagnostics to help you fix issues impacting your customers' experience, even if the issues are not reported by customers.

You write this integration as a LaunchDarkly SDK [plugin](/docs/sdk/features/hooks) that calls the Fullstory browser API. There are two approaches, and you can use either one or both:

* **Custom events**, described in [Send flag evaluations as custom events](#send-flag-evaluations-as-custom-events). The plugin sends each flag evaluation to Fullstory as its own event. Because each event carries the flag key, the variation, and the context, you can build Fullstory segments and metrics that compare behavior across variations. We recommend this approach.
* **Page properties**, described in [Send flag evaluations as page properties](#send-flag-evaluations-as-page-properties). The plugin attaches the set of evaluated flags to the current Fullstory page. Because this groups all flags into a single property, it supports less precise analysis than custom events. However, it makes the accumulated set of evaluated flags visible on pages where your application evaluates flags.

To learn more about the use case for this integration, read [LaunchDarkly + Fullstory: Targeted User Observability](https://launchdarkly.com/blog/launchdarkly-and-fullstory-integration/) on the LaunchDarkly blog.

<Note>
  **LaunchDarkly also offers its own session replay**

  If you are evaluating options rather than extending an existing Fullstory implementation, the LaunchDarkly [session replay plugin](/docs/sdk/observability/javascript) records end-user sessions and correlates them with flag evaluations without any custom code. To learn more, read [Session replay](/docs/home/observability/session-replay).
</Note>

## Prerequisites

To build either integration, you need:

* A Fullstory account with the [Fullstory browser snippet installed](https://help.fullstory.com/hc/en-us/articles/360020623514-Installing-the-Fullstory-Script) and using the V2 browser API.
* The [JavaScript SDK](/docs/sdk/client-side/javascript) version 3.7 or later, or the [React Web SDK](/docs/sdk/client-side/react/react-web) version 3.7 or later. These versions introduced plugin support.

## Send flag evaluations as custom events

In this approach, the plugin uses the `afterEvaluation` hook stage to send one Fullstory custom event per flag evaluation. Fullstory indexes the event properties, so you can then segment sessions by flag key and variation.

### Understand the event properties

The example plugin sends an event named `feature_flag.evaluation` with the following properties. This name aligns with the `feature_flag` events that LaunchDarkly [session replay](/docs/home/observability/session-replay) records automatically:

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Description</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>feature\_flag.key</code></td>
      <td>The key of the evaluated flag</td>
    </tr>

    <tr>
      <td><code>feature\_flag.provider\_name</code></td>
      <td>Set to <code>LaunchDarkly</code></td>
    </tr>

    <tr>
      <td><code>feature\_flag.context\_id</code></td>
      <td>The fully-qualified key of the LaunchDarkly context that received the variation</td>
    </tr>

    <tr>
      <td><code>feature\_flag.variation\_index</code></td>
      <td>The zero-based index of the variation the context received</td>
    </tr>

    <tr>
      <td><code>feature\_flag.value</code></td>
      <td>The string representation of the variation value</td>
    </tr>

    <tr>
      <td><code>feature\_flag.evaluation\_reason</code></td>
      <td>The kind of the <a href="/docs/sdk/features/evaluation-reasons">evaluation reason</a>, for example <code>RULE\_MATCH</code> or <code>FALLTHROUGH</code></td>
    </tr>
  </tbody>
</table>

The example nests these properties under a `feature_flag` object rather than passing them as flat, dotted property names. Fullstory property names may only contain letters, numbers, underscores, and hyphens, and must begin with a letter, so a name such as `feature_flag.key` is invalid. Fullstory joins nested property names with dots when it builds its schema, which produces the names in the table above. To learn more, read Fullstory's [Client API requirements](https://help.fullstory.com/hc/en-us/articles/360020623234-Client-API-Requirements).

<Tip>
  **Send flag values as strings**

  Fullstory infers a single type for each property name. Because different flags have different variation types, sending raw variation values would make `feature_flag.value` a boolean for one flag and a string for another, which would cause a schema conflict. The example converts every value to a string so the property has one consistent type.
</Tip>

### Create the plugin

The plugin below sends one event per flag evaluation, and skips the call when a flag returns the same value it returned the last time your application evaluated it for the same context. Your application may call `variation` many times per page, and Fullstory limits custom events to 60 calls per user per page per minute, with a burst limit of 40 calls per second. Without deduplication, a single page render can exhaust that budget.

Modify this code to fit your setup. To use it as written, save it as `fullstoryPlugin.ts`:

<CodeGroup>
  ```ts title="fullstoryPlugin.ts" maxLines=0 lines wrap theme={null}
  import type {
    EvaluationSeriesContext,
    EvaluationSeriesData,
    Hook,
    LDContext,
    LDEvaluationDetail,
    LDPlugin,
    LDPluginEnvironmentMetadata,
    LDPluginMetadata,
  } from 'launchdarkly-js-client-sdk';

  declare global {
    interface Window {
      FS?: (operation: string, payload?: unknown) => unknown;
    }
  }

  // Builds the same fully-qualified context key that LaunchDarkly uses, by
  // prefixing each key with its context kind and escaping '%' and ':'.
  function canonicalKey(context: LDContext | undefined): string | undefined {
    if (!context || typeof context !== 'object') {
      return undefined;
    }

    const encode = (key: string) => key.replace(/%/g, '%25').replace(/:/g, '%3A');
    const multi = context as Record<string, { key?: string }> & { kind?: string };

    if (multi.kind === 'multi') {
      return Object.keys(multi)
        .filter((kind) => kind !== 'kind')
        .sort()
        .map((kind) => `${kind}:${encode(multi[kind]?.key ?? '')}`)
        .join(':');
    }

    const kind = multi.kind ?? 'user';
    const key = (context as { key?: string }).key;
    if (!key) {
      return undefined;
    }

    return kind === 'user' ? key : `${kind}:${encode(key)}`;
  }

  // Fullstory infers one type per property name, so serialize every variation
  // value to a string.
  function serializeValue(value: unknown): string {
    return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
  }

  export function fullstoryPlugin(): LDPlugin {
    // Tracks the last value sent for each flag and context, so that repeated
    // evaluations of an unchanged flag do not send duplicate events.
    const lastSent = new Map<string, string>();

    const hook: Hook = {
      getMetadata() {
        return { name: 'fullstory-feature-flag-hook' };
      },

      afterEvaluation(
        hookContext: EvaluationSeriesContext,
        data: EvaluationSeriesData,
        detail: LDEvaluationDetail,
      ): EvaluationSeriesData {
        // The Fullstory snippet attaches a callable FS function to the window
        // object. If you configured a custom namespace, use that global instead.
        if (typeof window.FS !== 'function') {
          return data;
        }

        const contextId = canonicalKey(hookContext.context);
        const value = serializeValue(detail.value);
        const dedupeKey = `${hookContext.flagKey}:${contextId}`;

        if (lastSent.get(dedupeKey) === value) {
          return data;
        }
        lastSent.set(dedupeKey, value);

        window.FS('trackEvent', {
          name: 'feature_flag.evaluation',
          properties: {
            feature_flag: {
              key: hookContext.flagKey,
              provider_name: 'LaunchDarkly',
              context_id: contextId ?? null,
              variation_index: detail.variationIndex ?? null,
              value,
              evaluation_reason: detail.reason?.kind ?? null,
            },
          },
        });

        return data;
      },
    };

    return {
      getMetadata(): LDPluginMetadata {
        return { name: 'fullstory-feature-flag-plugin' };
      },

      getHooks(_environmentMetadata: LDPluginEnvironmentMetadata): Hook[] {
        return [hook];
      },

      register(): void {
        // No setup required. The SDK calls the hook on each evaluation.
      },
    };
  }
  ```
</CodeGroup>

### Register the plugin

To send the events, pass the plugin to the SDK in the `plugins` configuration option when you initialize the client:

<CodeGroup>
  ```js title="JavaScript" lines wrap theme={null}
  import * as LDClient from 'launchdarkly-js-client-sdk';
  import { fullstoryPlugin } from './fullstoryPlugin';

  const context = {
    kind: 'user',
    key: 'example-context-key',
  };

  const client = LDClient.initialize('example-client-side-id', context, {
    plugins: [fullstoryPlugin()],
  });
  ```

  ```js title="React Web" lines wrap theme={null}
  import { withLDProvider } from 'launchdarkly-react-client-sdk';
  import { fullstoryPlugin } from './fullstoryPlugin';

  const context = {
    kind: 'user',
    key: 'example-context-key',
  };

  const LDProvider = withLDProvider({
    clientSideID: 'example-client-side-id',
    context,
    options: {
      plugins: [fullstoryPlugin()],
    },
  })(App);
  ```
</CodeGroup>

After you deploy this change and your application evaluates a flag, the `feature_flag.evaluation` event appears in Fullstory. To confirm the integration is working, search your Fullstory sessions for that event. To learn more, read Fullstory's [Analytics events](https://developer.fullstory.com/browser/capture-events/analytics-events/).

## Send flag evaluations as page properties

In this approach, the plugin collects the flags your application evaluates and attaches them to the current Fullstory page as a single `ldflags` property. Fullstory page properties apply from the time of the `setProperties` call until the URL host or path changes, so pages where your application evaluates flags show the accumulated set of flags evaluated so far in the session.

Because Fullstory allows a maximum of 50 unique properties per page and 500 unique properties across all pages, the plugin sends the flags as one array property rather than one property per flag.

<Note>
  **Fullstory must finish starting before getSession returns a value**

  `FS('getSession')` returns `null` or `undefined` until Fullstory capture has started. The example plugin skips the call in that case. If your application evaluates flags before Fullstory is ready, use `FS('observe')` or `FS('getSessionAsync')` to wait for capture to begin. To learn more, read Fullstory's [Get session details](https://developer.fullstory.com/browser/get-session-details/) documentation.
</Note>

The plugin below keys its accumulated flags by Fullstory session URL. A new session starts from an empty set, and calls `setProperties` only when a flag value actually changes. Modify this code to fit your setup. To use it as written, save it as `fullstoryPagePropertiesPlugin.ts`:

<CodeGroup>
  ```ts title="fullstoryPagePropertiesPlugin.ts" maxLines=0 lines wrap theme={null}
  import type {
    EvaluationSeriesContext,
    EvaluationSeriesData,
    Hook,
    LDEvaluationDetail,
    LDPlugin,
    LDPluginEnvironmentMetadata,
    LDPluginMetadata,
  } from 'launchdarkly-js-client-sdk';

  declare global {
    interface Window {
      FS?: (operation: string, payload?: unknown) => unknown;
    }
  }

  // Fullstory infers one type per property name, so serialize every variation
  // value to a string.
  function serializeValue(value: unknown): string {
    return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
  }

  export function fullstoryPagePropertiesPlugin(): LDPlugin {
    let currentSession: string | undefined;
    let evaluatedFlags: Record<string, string> = {};

    const hook: Hook = {
      getMetadata() {
        return { name: 'fullstory-page-properties-hook' };
      },

      afterEvaluation(
        hookContext: EvaluationSeriesContext,
        data: EvaluationSeriesData,
        detail: LDEvaluationDetail,
      ): EvaluationSeriesData {
        // The Fullstory snippet attaches a callable FS function to the window
        // object. If you configured a custom namespace, use that global instead.
        if (typeof window.FS !== 'function') {
          return data;
        }

        const sessionUrl = window.FS('getSession', { format: 'url' }) as string | undefined;
        if (!sessionUrl) {
          return data;
        }

        // Start a fresh set of flags whenever Fullstory begins a new session.
        if (sessionUrl !== currentSession) {
          currentSession = sessionUrl;
          evaluatedFlags = {};
        }

        // Skip the Fullstory call when this flag already has this value, so that
        // repeated evaluations do not resend an unchanged property.
        const value = serializeValue(detail.value);
        if (evaluatedFlags[hookContext.flagKey] === value) {
          return data;
        }
        evaluatedFlags[hookContext.flagKey] = value;

        const ldflags = Object.keys(evaluatedFlags)
          .sort()
          .map((flagKey) => `${flagKey}=${evaluatedFlags[flagKey]}`);

        window.FS('setProperties', {
          type: 'page',
          properties: { ldflags },
        });

        return data;
      },
    };

    return {
      getMetadata(): LDPluginMetadata {
        return { name: 'fullstory-page-properties-plugin' };
      },

      getHooks(_environmentMetadata: LDPluginEnvironmentMetadata): Hook[] {
        return [hook];
      },

      register(): void {
        // No setup required. The SDK calls the hook on each evaluation.
      },
    };
  }
  ```
</CodeGroup>

Register this plugin in the `plugins` configuration option, in the same way you [register the custom events plugin](#register-the-plugin). To send both custom events and page properties, pass both plugins:

<CodeGroup>
  ```js title="JavaScript" lines wrap theme={null}
  import * as LDClient from 'launchdarkly-js-client-sdk';
  import { fullstoryPlugin } from './fullstoryPlugin';
  import { fullstoryPagePropertiesPlugin } from './fullstoryPagePropertiesPlugin';

  const client = LDClient.initialize('example-client-side-id', context, {
    plugins: [fullstoryPlugin(), fullstoryPagePropertiesPlugin()],
  });
  ```

  ```js title="React Web" lines wrap theme={null}
  import { withLDProvider } from 'launchdarkly-react-client-sdk';
  import { fullstoryPlugin } from './fullstoryPlugin';
  import { fullstoryPagePropertiesPlugin } from './fullstoryPagePropertiesPlugin';

  const LDProvider = withLDProvider({
    clientSideID: 'example-client-side-id',
    context,
    options: {
      plugins: [fullstoryPlugin(), fullstoryPagePropertiesPlugin()],
    },
  })(App);
  ```
</CodeGroup>
