From Optimizely

Optimizely Feature Experimentation (formerly Optimizely Full Stack) and A vs B work the same basic way. Both download a datafile (a small JSON file that lists every live experiment, flag, and rule for your project). Both then evaluate flags in your own code and send events to a collection endpoint.

A feature flag is a setting in your code that you can turn on, off, or change without a new deploy. Optimizely and A vs B both use them, and your event names transfer directly. Flag keys do too when they use only lowercase letters, digits and underscores. A key with a hyphen takes its underscore form: flag-key becomes flag_key. Beyond that, only the SDK calls change.

The migration itself has three parts. Optimizely's OptimizelyDecision object becomes A vs B's Flag<T>. Optimizely's per-variable reads become one JSON flag value with every field on it. And Optimizely's separate decide and isFeatureEnabled calls become a single typed call that always returns the same rich result.

Concept mapping

Optimizely Feature ExperimentationA vs B
Feature flagFeature flag (same concept; a hyphen in a key becomes an underscore)
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 (per-call options: skip recording this evaluation, or include reason strings)

Client construction

Both SDKs start with an SDK key and download a datafile. Optimizely uses createInstance. A vs B uses its AvsbServer or AvsbClient constructor directly.

Optimizely, Node:

TypeScript
import optimizely from '@optimizely/optimizely-sdk';const optimizelyClient = optimizely.createInstance({ sdkKey: 'your-sdk-key' });await optimizelyClient.onReady();
TypeScript4 lines

A vs B, Node:

TypeScript
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });const result = await server.onReady();// result.success / result.degraded / result.source
TypeScript5 lines

Optimizely, Browser:

TypeScript
import optimizely from '@optimizely/optimizely-sdk';const optimizelyClient = optimizely.createInstance({ sdkKey: 'client-key' });await optimizelyClient.onReady();const userCtx = optimizelyClient.createUserContext('u_123', { plan: 'pro' });
TypeScript5 lines

A vs B, Browser:

TypeScript
import { AvsbClient } from '@avsbhq/browser';const client = new AvsbClient({  sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx',  context: { kind: 'user', key: 'u_123', plan: 'pro' },});await client.onReady();
TypeScript7 lines
Info

Optimizely uses createUserContext to bind one user to the client for a series of calls. A vs B does this too. On the browser SDK it happens when you build the client. On the server SDK, call forUser(ctx) each time instead.

Identity model

Optimizely builds a fresh user context on every request, with a user id and attributes. A vs B's server client keeps no state between calls: pass a context to forUser each time. The browser client keeps one context and gives you clear methods to update it.

Optimizely:

TypeScript
// Each request creates a new contextconst userCtx = optimizelyClient.createUserContext('u_123', { plan: 'pro', country: 'US' });const decision = userCtx.decide('checkout_redesign');// No partial update: re-create with new attributes
TypeScript5 lines

A vs B:

TypeScript
// Server: stateless, context per callconst ctx = { kind: 'user', key: 'u_123', plan: 'pro', country: 'US' };const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);// Browser: stateful context with mutation methodsclient.identify({ kind: 'user', key: 'u_123', plan: 'pro' });client.updateAttributes({ country: 'US' }); // patch without full replacement// Stitch the anonymous identity to the signed-in one. Synchronous.client.alias({ kind: 'user', key: 'anon_xyz' }, { kind: 'user', key: 'u_123' });
TypeScript10 lines

Flag evaluation

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

Optimizely:

TypeScript
const userCtx = optimizelyClient.createUserContext('u_123', { plan: 'pro' });// Boolean feature flagconst decision = userCtx.decide('show_new_checkout');if (decision.enabled) {  // show new checkout}// Feature with variables (JSON)const configDecision = userCtx.decide('pricing_config');const price = configDecision.variables['base_price'] as number;// Individual variable fetchconst price2 = optimizelyClient.getFeatureVariableDouble(  'pricing_config',  'base_price',  'u_123',  { plan: 'pro' },);// Decide all flagsconst allDecisions = userCtx.decideAll();
TypeScript22 lines

A vs B:

TypeScript
const uc = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' });// Boolean flagconst flag = uc.getBoolFlag('show_new_checkout', false);if (flag.isEnabled()) {  // show new checkout}// flag.variationKey → e.g. 'treatment', 'control'// flag.source       → 'rule' | 'default' | 'holdout' | ...// flag.reasons      → string[]// JSON flag with typed variablesinterface PricingConfig { basePrice: number; currency: string }const config = uc.getJsonFlag<PricingConfig>('pricing_config', { basePrice: 99, currency: 'USD' });const price = config.value.basePrice;// All flags (no exposures fired by default)const allFlags = uc.getAllFlags();
TypeScript18 lines
Tip

Optimizely reads each feature variable with its own method (getFeatureVariableDouble, getFeatureVariableString, and so on). In A vs B, write one TypeScript type for all of a flag's variables, then read them together with getJsonFlag<T>. That is safer, and it is one call instead of several.

Tracking events

Optimizely's track takes the event key, the user id, the user's attributes, and a tags map that carries the revenue field. A vs B uses one payload object instead.

Optimizely:

TypeScript
optimizelyClient.track('purchase_completed', 'u_123', { plan: 'pro' }, {  revenue: 4999,   // in cents in Optimizely  value: 49.99,  tags: { orderId: 'ord_99' },});
TypeScript5 lines

A vs B:

TypeScript
server.track('purchase_completed', {  context: { kind: 'user', key: 'u_123', plan: 'pro' },  revenue: 49.99,  // major units, not cents});
TypeScript4 lines

Multi-context

Optimizely Feature Experimentation calls this a qualified audience (a named, reusable group of visitors that match certain conditions). A vs B uses the same kind: 'multi' shape as LaunchDarkly. If you target on organization, device, or another entity type, add each one as a context kind in the A vs B dashboard.

A vs B, multi-context:

TypeScript
import type { MultiContext } from '@avsbhq/core';const ctx: MultiContext = {  kind: 'multi',  user:         { kind: 'user',         key: 'u_123', plan: 'pro' },  organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },};server.forUser(ctx).getBoolFlag('enterprise_sso', false);
TypeScript8 lines

Streaming updates

Optimizely refreshes the datafile on a timer you can set. A vs B works the same way, with an optional live-push mode (SSE) on the browser.

A vs B, flag change listener:

TypeScript
client.on('configUpdate', ({ publishedAt, reason }) => {  // Datafile refreshed; re-render flag-dependent UI if needed});client.on('flagChange', ({ flagKey, previousValue, newValue }) => {  // A specific flag value changed});
TypeScript7 lines

Bootstrap / SSR

Optimizely's datafileManager caches the datafile on your server. A vs B's fetchDatafile does the same job: fetch it on the server, then pass it as bootstrap to the client provider.

Optimizely, SSR datafile pre-fetch:

TypeScript
import optimizely from '@optimizely/optimizely-sdk';const datafileUrl = 'https://cdn.optimizely.com/datafiles/your-key.json';const resp = await fetch(datafileUrl);const datafile = await resp.json();const optimizelySsrClient = optimizely.createInstance({ datafile });
TypeScript6 lines

A vs B, SSR bootstrap:

TypeScript React
import { AvsbProvider } from '@avsbhq/react';import { fetchDatafile } from '@avsbhq/browser/server';const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);// Pass to client provider<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>  <YourApp /></AvsbProvider>
TypeScript React9 lines

Sticky bucketing

Sticky bucketing is putting a visitor into a group with a consistent hash of their id, so they land in the same group every time. Optimizely does this with a user profile service. A vs B uses a StickyBucketService with the same lookup and save methods, but a richer StickyAssignment payload that includes the rule that made the decision.

Optimizely, user profile service:

TypeScript
const userProfileService = {  lookup: (userId: string) => ({ user_id: userId, experiment_bucket_map: {} }),  save:   (userProfile: object) => { /* persist */ },};const optimizelyStickyClient = optimizely.createInstance({  sdkKey: 'your-sdk-key',  userProfileService,});
TypeScript8 lines

A vs B, sticky bucket service (@avsbhq/node also ships InMemoryStickyBucketService and RedisStickyBucketService):

TypeScript
import type { StickyBucketService, StickyAssignment } from '@avsbhq/core';class MyStore implements StickyBucketService {  // Both methods are synchronous, so back them with a warm local cache.  private cache = new Map<string, StickyAssignment>();  lookup(userId: string, flagKey: string): StickyAssignment | null {    return this.cache.get(`${userId}:${flagKey}`) ?? null;  }  save(userId: string, flagKey: string, assignment: StickyAssignment): void {    this.cache.set(`${userId}:${flagKey}`, assignment);  }}const stickyServer = new AvsbServer({  sdkKey: process.env.AVSB_SDK_KEY!,  stickyBucketService: new MyStore(),});
TypeScript18 lines

Holdouts

A holdout is a slice of visitors kept out of every experiment, so you can measure the combined effect of everything else you shipped. Optimizely Feature Experimentation has no built-in holdout. Teams usually build one by hand, by excluding a segment from every experiment.

A vs B includes holdouts on every plan. Set one up in the dashboard and attach flags to it. Held-out visitors get source: 'holdout'.

Cleanup

Optimizely:

TypeScript
optimizelyClient.close();
TypeScript1 line

A vs B:

TypeScript
await server.close(); // async flush + stop polling
TypeScript1 line

Testing

Optimizely, test instance:

TypeScript
import optimizely from '@optimizely/optimizely-sdk';const optimizelyTestClient = optimizely.createInstance({  datafile: { /* inline test datafile */ },  logLevel: 'error',});
TypeScript5 lines

A vs B, mock client:

TypeScript
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({  flags: {    show_new_checkout: true,    pricing_config: { basePrice: 49, currency: 'USD' },  },});
TypeScript8 lines

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 matching typed call: getBoolFlag, getStringFlag, or getJsonFlag<T>.

5

Consolidate feature variables

Group all of a flag's feature variables into one TypeScript interface and use getJsonFlag<T>. Remove the per-type variable calls (getFeatureVariableDouble, and so on).

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. Check flags and events in the A vs B dashboard before you go live.

Was this helpful?