Fullstory

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 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. 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. 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 on the LaunchDarkly blog.

LaunchDarkly also offers its own session replay

If you are evaluating options rather than extending an existing Fullstory implementation, the LaunchDarkly session replay plugin records end-user sessions and correlates them with flag evaluations without any custom code. To learn more, read Session replay.

Prerequisites

To build either integration, you need:

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 records automatically:

PropertyDescription
feature_flag.keyThe key of the evaluated flag
feature_flag.provider_nameSet to LaunchDarkly
feature_flag.context_idThe fully-qualified key of the LaunchDarkly context that received the variation
feature_flag.variation_indexThe zero-based index of the variation the context received
feature_flag.valueThe string representation of the variation value
feature_flag.evaluation_reasonThe kind of the evaluation reason, for example RULE_MATCH or FALLTHROUGH

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.

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.

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:

fullstoryPlugin.ts
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.
},
};
}

Register the plugin

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

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()],
});

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.

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.

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

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:

fullstoryPagePropertiesPlugin.ts
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.
},
};
}

Register this plugin in the plugins configuration option, in the same way you register the custom events plugin. To send both custom events and page properties, pass both plugins:

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()],
});