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 Experimentation | A vs B |
|---|---|
| Feature flag | Feature flag (same concept; keys transfer directly) |
| Feature variable | JSON flag value — define a typed object with all variables |
| Experiment (A/B test) | Flag with an A/B test rule |
OptimizelyUserContext | EvalContext (SingleContext) |
user.id | context.key |
user.attributes | Top-level attributes on EvalContext |
client.decide('flag-key', user) | server.forUser(ctx).getFlag('flag-key', default) |
OptimizelyDecision.enabled | Flag<T>.isEnabled() |
OptimizelyDecision.variationKey | Flag<T>.variationKey (same field name) |
OptimizelyDecision.variables | Flag<T>.value — JSON object with all variables |
OptimizelyDecision.reasons | Flag<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? }) |
DecideOption | DecideOption (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.
1import optimizely from '@optimizely/optimizely-sdk';2
3const client = optimizely.createInstance({ sdkKey: 'your-sdk-key' });4await client.onReady();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.source1import optimizely from '@optimizely/optimizely-sdk';2
3const client = optimizely.createInstance({ sdkKey: 'client-key' });4await client.onReady();5const userCtx = client.createUserContext('u_123', { plan: 'pro' });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();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.
1// Each request creates a new context2const userCtx = client.createUserContext('u_123', { plan: 'pro', country: 'US' });3const decision = userCtx.decide('checkout_redesign');4
5// No partial update — re-create with new attributes1// Server — stateless, context per call2const 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 methods6client.identify({ kind: 'user', key: 'u_123', plan: 'pro' });7client.updateAttributes({ country: 'US' }); // patch without full replacement8await client.alias(anonCtx, identifiedCtx); // stitch anonymous identityFlag 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.
1const userCtx = client.createUserContext('u_123', { plan: 'pro' });2
3// Boolean feature flag4const decision = userCtx.decide('show_new_checkout');5if (decision.enabled) {6 // show new checkout7}8
9// Feature with variables (JSON)10const configDecision = userCtx.decide('pricing_config');11const price = configDecision.variables['base_price'] as number;12
13// Individual variable fetch14const price2 = client.getFeatureVariableDouble('pricing_config', 'base_price', 'u_123', attrs);15
16// Decide all flags17const allDecisions = userCtx.decideAll();1const uc = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' });2
3// Boolean flag4const flag = uc.getBoolFlag('show_new_checkout', false);5if (flag.isEnabled()) {6 // show new checkout7}8// flag.variationKey → e.g. 'treatment', 'control'9// flag.source → 'rule' | 'default' | 'holdout' | ...10// flag.reasons → string[]11
12// JSON flag with typed variables13interface 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();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.
1client.track('purchase_completed', 'u_123', { plan: 'pro' }, {2 revenue: 4999, // in cents in Optimizely3 value: 49.99,4 tags: { orderId: 'ord_99' },5});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.
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.
1client.on('configUpdate', ({ publishedAt, reason }) => {2 // Datafile refreshed — re-render flag-dependent UI if needed3});4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {6 // A specific flag value changed7});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.
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 });1import { fetchDatafile } from '@avsbhq/node/server';2const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);3
4// Pass to client provider5<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.
1const userProfileService = {2 lookup: (userId: string) => ({ user_id: userId, experiment_bucket_map: {} }),3 save: (userProfile: object) => { /* persist */ },4};5const client = optimizely.createInstance({ sdkKey, userProfileService });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
1optimizelyClient.close();1await server.close(); // async flush + stop pollingTesting
1import optimizely from '@optimizely/optimizely-sdk';2const client = optimizely.createInstance({3 datafile: { /* inline test datafile */ },4 logLevel: 'error',5});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
Remove Optimizely packages
@optimizely/optimizely-sdk and any related Optimizely packages from your project.Install A vs B packages
@avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.Migrate user context construction
client.createUserContext(id, attributes) with an inline EvalContext object: { kind: 'user', key: id, ...attributes }.Replace decide calls
userCtx.decide(key) with the appropriate typed evaluator: getBoolFlag, getStringFlag, or getJsonFlag<T>.Consolidate feature variables
getJsonFlag<T>. Remove per-type variable accessor calls (getFeatureVariableDouble, etc.).Replace isFeatureEnabled calls
client.isFeatureEnabled(key, userId, attrs) with server.forUser(ctx).getBoolFlag(key, false).isEnabled().Replace track calls
client.track(event, userId, attrs, tags) with server.track(event, { context, value, properties }).Migrate user profile service (if used)
StickyBucketService interface with lookup and save returning StickyAssignment objects.Update tests
createMockClient from @avsbhq/test.Verify and deploy
npm run build and npx tsc --noEmit. Confirm flags and events in the A vs B dashboard before promoting to production.