/Docs

Migrate from Optimizely Feature Experimentation to A vs B

Optimizely Feature Experimentation (formerly Optimizely Full Stack) and A vs B share the same datafile-driven architecture: both download a JSON config, evaluate flags client-side, and send events to a collection endpoint. The migration replaces Optimizely's OptimizelyDecision object with A vs B's Flag<T>, consolidates feature variables into a single JSON-flag pattern, and removes the separate “decide” vs “is-feature-enabled” split. Your experiment keys and event names transfer directly.

Concept mapping

Optimizely Feature ExperimentationA vs B
Feature flagFeature flag (same concept; keys transfer directly)
Feature variableJSON flag value — define a typed object with all variables
Experiment (A/B test)Flag with an A/B test rule
OptimizelyUserContextEvalContext (SingleContext)
user.idcontext.key
user.attributesTop-level attributes on EvalContext
client.decide('flag-key', user)server.forUser(ctx).getFlag('flag-key', default)
OptimizelyDecision.enabledFlag<T>.isEnabled()
OptimizelyDecision.variationKeyFlag<T>.variationKey (same field name)
OptimizelyDecision.variablesFlag<T>.value — JSON object with all variables
OptimizelyDecision.reasonsFlag<T>.reasons (same field name)
client.decideAll(user)client.getAllFlags()
client.isFeatureEnabled(key, user)client.getBoolFlag(key, false).isEnabled()
client.getFeatureVariable(key, variableKey, user)client.getJsonFlag<T>(key, {}).value.variableKey
client.track(eventKey, user, eventTags)server.track(eventKey, { context, value?, properties? })
DecideOptionDecideOption (same enum, same keys)

Client construction

Both SDKs instantiate with an SDK key and download a datafile. Optimizely uses createInstance; A vs B uses the AvsbServer / AvsbClient constructor directly.

Optimizely — Node
ts
1import optimizely from '@optimizely/optimizely-sdk';
2
3const client = optimizely.createInstance({ sdkKey: 'your-sdk-key' });
4await client.onReady();
A vs B — Node
ts
1import { AvsbServer } from '@avsbhq/node';
2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });
4const result = await server.onReady();
5// result.success / result.degraded / result.source
Optimizely — Browser
ts
1import optimizely from '@optimizely/optimizely-sdk';
2
3const client = optimizely.createInstance({ sdkKey: 'client-key' });
4await client.onReady();
5const userCtx = client.createUserContext('u_123', { plan: 'pro' });
A vs B — Browser
ts
1import { AvsbClient } from '@avsbhq/browser';
2
3const client = new AvsbClient({
4 sdkKey: 'sdk_production_abc123',
5 context: { kind: 'user', key: 'u_123', plan: 'pro' },
6});
7await client.onReady();
Info
Optimizely uses createUserContext to bind a user to the client instance for a series of calls. A vs B does the same but at client construction for the browser SDK, or via forUser(ctx) per call on the server SDK.

Identity model

Optimizely's OptimizelyUserContextis created fresh per request with user ID and attributes. A vs B's server client is stateless per call via forUser; the browser client binds a mutable context and exposes explicit mutation methods.

Optimizely
ts
1// Each request creates a new context
2const userCtx = client.createUserContext('u_123', { plan: 'pro', country: 'US' });
3const decision = userCtx.decide('checkout_redesign');
4
5// No partial update — re-create with new attributes
A vs B
ts
1// Server — stateless, context per call
2const ctx = { kind: 'user', key: 'u_123', plan: 'pro', country: 'US' };
3const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);
4
5// Browser — stateful context with mutation methods
6client.identify({ kind: 'user', key: 'u_123', plan: 'pro' });
7client.updateAttributes({ country: 'US' }); // patch without full replacement
8await client.alias(anonCtx, identifiedCtx); // stitch anonymous identity

Flag evaluation

Optimizely's decide returns an OptimizelyDecision with enabled, variationKey, and variables. A vs B's Flag<T> carries identical information under slightly different field names.

Optimizely
ts
1const userCtx = client.createUserContext('u_123', { plan: 'pro' });
2
3// Boolean feature flag
4const decision = userCtx.decide('show_new_checkout');
5if (decision.enabled) {
6 // show new checkout
7}
8
9// Feature with variables (JSON)
10const configDecision = userCtx.decide('pricing_config');
11const price = configDecision.variables['base_price'] as number;
12
13// Individual variable fetch
14const price2 = client.getFeatureVariableDouble('pricing_config', 'base_price', 'u_123', attrs);
15
16// Decide all flags
17const allDecisions = userCtx.decideAll();
A vs B
ts
1const uc = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' });
2
3// Boolean flag
4const flag = uc.getBoolFlag('show_new_checkout', false);
5if (flag.isEnabled()) {
6 // show new checkout
7}
8// flag.variationKey → e.g. 'treatment', 'control'
9// flag.source → 'rule' | 'default' | 'holdout' | ...
10// flag.reasons → string[]
11
12// JSON flag with typed variables
13interface PricingConfig { basePrice: number; currency: string }
14const config = uc.getJsonFlag<PricingConfig>('pricing_config', { basePrice: 99, currency: 'USD' });
15const price = config.value.basePrice;
16
17// All flags (no exposures fired by default)
18const allFlags = client.getAllFlags();
Tip
Optimizely's feature variables are per-variable accessor methods (getFeatureVariableDouble, getFeatureVariableString, etc.). In A vs B, define a single TypeScript interface for all variables of a flag and use getJsonFlag<T>. This gives you better type safety and a single evaluation call.

Tracking events

Optimizely's track takes the event key, user ID, user attributes, and an event-tags map (which carries the revenue field). A vs B uses a unified payload object.

Optimizely
ts
1client.track('purchase_completed', 'u_123', { plan: 'pro' }, {
2 revenue: 4999, // in cents in Optimizely
3 value: 49.99,
4 tags: { orderId: 'ord_99' },
5});
A vs B
ts
1server.track('purchase_completed', {
2 context: { kind: 'user', key: 'u_123', plan: 'pro' },
3 value: 49.99,
4 properties: { orderId: 'ord_99' },
5});

Multi-context

Optimizely Feature Experimentation added multi-context support in recent versions via the OptimizelyUserContext qualified audiences. A vs B uses the same discriminated kind: 'multi' shape as LaunchDarkly. If you target on organization, device, or other entity types, declare each as a context kind in the A vs B dashboard.

A vs B — multi-context
ts
1import type { MultiContext } from '@avsbhq/core';
2
3const ctx: MultiContext = {
4 kind: 'multi',
5 user: { kind: 'user', key: 'u_123', plan: 'pro' },
6 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },
7};
8server.forUser(ctx).getBoolFlag('enterprise_sso', false);

Streaming updates

Optimizely uses a configurable polling interval to refresh the datafile. A vs B uses the same model with an optional SSE mode for real-time push on the browser.

A vs B — flag change listener
ts
1client.on('configUpdate', ({ publishedAt, reason }) => {
2 // Datafile refreshed — re-render flag-dependent UI if needed
3});
4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {
6 // A specific flag value changed
7});

Bootstrap / SSR

Optimizely provides a datafileManagerfor server-side datafile caching. A vs B's fetchDatafile fetches the datafile on the server and passes it as bootstrap to the client provider.

Optimizely — SSR datafile pre-fetch
ts
1import optimizely from '@optimizely/optimizely-sdk';
2import fetch from 'node-fetch';
3
4const datafileUrl = 'https://cdn.optimizely.com/datafiles/your-key.json';
5const resp = await fetch(datafileUrl);
6const datafile = await resp.json();
7const client = optimizely.createInstance({ datafile });
A vs B — SSR bootstrap
ts
1import { fetchDatafile } from '@avsbhq/node/server';
2const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);
3
4// Pass to client provider
5<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>
6 {children}
7</AvsbProvider>

Sticky bucketing

Optimizely supports user profile service for sticky bucketing. A vs B uses a StickyBucketService with the same lookup/save interface but a richer StickyAssignment payload that includes the rule context.

Optimizely — user profile service
ts
1const userProfileService = {
2 lookup: (userId: string) => ({ user_id: userId, experiment_bucket_map: {} }),
3 save: (userProfile: object) => { /* persist */ },
4};
5const client = optimizely.createInstance({ sdkKey, userProfileService });
A vs B — sticky bucket service
ts
1import { StickyBucketService, StickyAssignment } from '@avsbhq/core';
2
3class MyStore implements StickyBucketService {
4 lookup(userId: string, flagKey: string): StickyAssignment | null { /* ... */ }
5 save(userId: string, flagKey: string, assignment: StickyAssignment): void { /* ... */ }
6}
7const server = new AvsbServer({ sdkKey: '...', stickyBucketService: new MyStore() });

Holdouts

Optimizely Feature Experimentation does not have a built-in holdout concept — global holdouts are typically implemented manually by excluding a segment from all experiments. A vs B provides first-class holdout support on all plans. Configure a holdout in the dashboard and associate flags with it; held-out users receive source: 'holdout'.

Cleanup

Optimizely
ts
1optimizelyClient.close();
A vs B
ts
1await server.close(); // async flush + stop polling

Testing

Optimizely — test instance
ts
1import optimizely from '@optimizely/optimizely-sdk';
2const client = optimizely.createInstance({
3 datafile: { /* inline test datafile */ },
4 logLevel: 'error',
5});
A vs B — mock client
ts
1import { createMockClient } from '@avsbhq/test';
2
3const mock = createMockClient({
4 flags: {
5 show_new_checkout: true,
6 pricing_config: { basePrice: 49, currency: 'USD' },
7 },
8});

Cutover checklist

1

Remove Optimizely packages

Uninstall @optimizely/optimizely-sdk and any related Optimizely packages from your project.
2

Install A vs B packages

Install @avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.
3

Migrate user context construction

Replace client.createUserContext(id, attributes) with an inline EvalContext object: { kind: 'user', key: id, ...attributes }.
4

Replace decide calls

Replace each userCtx.decide(key) with the appropriate typed evaluator: getBoolFlag, getStringFlag, or getJsonFlag<T>.
5

Consolidate feature variables

Group all feature variables for a flag into a single TypeScript interface and use getJsonFlag<T>. Remove per-type variable accessor calls (getFeatureVariableDouble, etc.).
6

Replace isFeatureEnabled calls

Replace client.isFeatureEnabled(key, userId, attrs) with server.forUser(ctx).getBoolFlag(key, false).isEnabled().
7

Replace track calls

Replace client.track(event, userId, attrs, tags) with server.track(event, { context, value, properties }).
8

Migrate user profile service (if used)

Implement the A vs B StickyBucketService interface with lookup and save returning StickyAssignment objects.
9

Update tests

Replace inline test datafiles with createMockClient from @avsbhq/test.
10

Verify and deploy

Run npm run build and npx tsc --noEmit. Confirm flags and events in the A vs B dashboard before promoting to production.