/Docs

Migrate from LaunchDarkly to A vs B

LaunchDarkly and A vs B share a very similar mental model: both use a typed context object to identify the evaluation subject, both return a rich evaluation envelope, and both support multi-context targeting. The main practical differences are package names, a cleaner typed-evaluator API that eliminates the need for separate variation andvariationDetail calls, and first-class holdout and bandit support built into the core SDK rather than as enterprise add-ons. Your targeting rules, audience definitions, and event keys transfer directly — only the SDK call sites change.

Concept mapping

LaunchDarklyA vs B
LDContext / LDUserEvalContext (SingleContext or MultiContext)
context.kindcontext.kind (identical field name)
context.keycontext.key (identical field name)
LDMultiKindContextMultiContext with kind: 'multi'
client.variation(key, ctx, default)client.getFlag(key, default).value
client.variationDetail(key, ctx, default)client.getFlag(key, default) — always returns the full Flag<T>
Flag variation valueFlag<T>.value
LDEvaluationDetail.variationIndexFlag<T>.variationKey (string key, not numeric index)
LDEvaluationDetail.reasonFlag<T>.reasons (string array) + .source (enum)
client.identify(ctx)client.identify(ctx) — replaces the full context
client.track(eventName, ctx, data, metricValue)client.track(eventKey, { value?, properties? })
LaunchDarkly metricA vs B metric event key (same concept, different config UI)
Holdout (enterprise)FlagDatafileHoldout — included in all plans
Big segmentsBigSegmentStore interface (bring your own backing store)
client.close()client.close() (async, returns Promise<void>)

Client construction

Both SDKs are initialized with an SDK key and return an async-ready client. The key difference is that A vs B's onReady()resolves (never rejects) with a typed InitResult so you can handle degraded-but-functional starts explicitly.

LaunchDarkly — Node server
ts
1import * as ld from 'launchdarkly-node-server-sdk';
2
3const client = ld.init('sdk-your-server-key');
4await client.waitForInitialization();
5// client is ready
A vs B — Node server
ts
1import { AvsbServer } from '@avsbhq/node';
2
3const server = new AvsbServer({ sdkKey: 'sdk_production_abc123' });
4const result = await server.onReady();
5// result.success === true → fully initialized
6// result.degraded === true → running on defaults; polling continues
LaunchDarkly — Browser
ts
1import * as LDClient from 'launchdarkly-js-client-sdk';
2
3const ctx = { kind: 'user', key: 'u_123', plan: 'pro' };
4const client = LDClient.initialize('client-side-id', ctx);
5await client.waitForInitialization();
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});
7const result = await client.onReady();
Info
LaunchDarkly uses separate server-side SDK keys and client-side IDs. A vs B uses a single per-environment SDK key for both runtimes. Find yours on the Environments page in your feature flag project.

Identity model

LaunchDarkly's browser identify(ctx) replaces the current user and re-fetches flags. A vs B follows the same replace semantics but splits the operation into three explicit methods so the intent is always clear in your code.

LaunchDarkly
ts
1// Replace the whole context (triggers flag re-evaluation)
2await client.identify({ kind: 'user', key: 'u_456', plan: 'enterprise' });
3
4// Partial attribute update — no direct equivalent; must re-identify with full ctx
A vs B
ts
1// Replace the full context (equivalent to LD identify)
2client.identify({ kind: 'user', key: 'u_456', plan: 'enterprise' });
3
4// Patch only specific attributes on an existing context — no full replacement
5client.updateAttributes({ plan: 'enterprise' });
6// Optionally target a specific context kind in a multi-context:
7client.updateAttributes({ tier: 'gold' }, 'organization');
8
9// Link an anonymous pre-login identity to a signed-up user
10await client.alias(
11 { kind: 'user', key: 'anon_abc' }, // previousContext
12 { kind: 'user', key: 'u_456' } // newContext (now identified)
13);
Tip
Use updateAttributes for post-login attribute enrichment (adding a plan or orgId after authentication) rather than calling identify again with the full context. Use alias once per session to stitch the anonymous and authenticated identities together in analytics.

Flag evaluation

LaunchDarkly provides two calls: variation (returns raw value) and variationDetail (returns value + reason). A vs B always returns the full Flag<T>envelope from every call — there is no separate “detail” variant. Typed-evaluator helpers give you compile-time safety without a cast.

LaunchDarkly
ts
1// Raw value only
2const showBanner = client.variation('show_banner', ctx, false);
3
4// With reason (separate call)
5const detail = client.variationDetail('show_banner', ctx, false);
6// detail.value, detail.reason.kind, detail.variationIndex
A vs B
ts
1// Every call returns Flag<T> — no separate 'detail' variant needed
2const flag = server.forUser(ctx).getBoolFlag('show_banner', false);
3flag.value // boolean
4flag.isEnabled() // source === 'rule' && value is truthy
5flag.variationKey // string key (e.g. 'on') or null if default served
6flag.source // 'rule' | 'default' | 'holdout' | 'sticky' | 'not_found' | ...
7flag.reasons // string[] — human-readable reasons array
8flag.ruleId // matched rule id or null
9
10// Typed evaluator variants
11const theme = server.forUser(ctx).getStringFlag('homepage_theme', 'default');
12const limit = server.forUser(ctx).getNumberFlag('rate_limit', 100);
13const config = server.forUser(ctx).getJsonFlag<PricingConfig>('pricing_config', {});
14
15// Browser client (context already bound at construction / identify)
16const flag2 = client.getBoolFlag('show_banner', false);
Info
LaunchDarkly's variationIndexis a numeric array position. A vs B's variationKey is the string key you assigned in the flag builder (e.g. 'control', 'treatment_a'). String keys survive flag variation reorders without breaking stored references.

Tracking events

LaunchDarkly's track takes four positional arguments including the context. A vs B binds context at client construction (or via forUser on the server), so the call site is simpler and the metric value is a named field rather than a positional argument.

LaunchDarkly
ts
1client.track('purchase_completed', ctx, { orderId: 'ord_99' }, 49.99);
A vs B
ts
1// Browser — context already bound
2client.track('purchase_completed', { value: 49.99, properties: { orderId: 'ord_99' } });
3
4// Server — pass context in payload
5server.track('purchase_completed', {
6 context: { kind: 'user', key: 'u_123' },
7 value: 49.99,
8 properties: { orderId: 'ord_99' },
9});
10
11// Server with forUser scope
12server.forUser({ kind: 'user', key: 'u_123' }).track('purchase_completed', {
13 value: 49.99,
14 properties: { orderId: 'ord_99' },
15});

Multi-context

Both SDKs support multi-context targeting. The shape is nearly identical — A vs B uses the same kind: 'multi'discriminant as LaunchDarkly. The main difference is that A vs B's hashAttribute on each rule uses a dotted-path syntax to address the bucketing key across contexts.

LaunchDarkly
ts
1const multiCtx: LDMultiKindContext = {
2 kind: 'multi',
3 user: { kind: 'user', key: 'u_123', plan: 'pro' },
4 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },
5};
6client.variation('enterprise_feature', multiCtx, false);
A vs B
ts
1import type { MultiContext } from '@avsbhq/core';
2
3const multiCtx: MultiContext = {
4 kind: 'multi',
5 user: { kind: 'user', key: 'u_123', plan: 'pro' },
6 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },
7};
8// In the dashboard, set hashAttribute to 'organization.key' to bucket by org
9server.forUser(multiCtx).getBoolFlag('enterprise_feature', false);

Streaming updates

LaunchDarkly uses a persistent SSE streaming connection per client. A vs B uses a hybrid: short-poll by default (configurable interval), with an opt-in browser SSE mode for real-time push. Flag-change notifications use the unified event bus in both modes.

LaunchDarkly
ts
1client.on('change', (changes) => {
2 // changes is a map of flagKey → { current, previous }
3});
A vs B
ts
1// Subscribe to individual flag changes
2const unsub = client.on('flagChange', ({ flagKey, previousValue, newValue }) => {
3 console.log(flagKey, previousValue, '→', newValue);
4});
5// Call unsub() to stop listening
6
7// Subscribe to any datafile update (e.g. to force a re-render)
8client.on('configUpdate', ({ publishedAt, reason }) => {
9 // reason: 'poll' | 'stream' | 'manual'
10});

Bootstrap / SSR

Both SDKs support a bootstrap payload to avoid a flash of default content on server-rendered pages. A vs B's bootstrap is the full FlagDatafile fetched on the server and passed to the provider.

LaunchDarkly — Next.js bootstrap
ts
1// Server component
2import { init } from '@launchdarkly/node-server-sdk';
3const ldClient = init(process.env.LD_SDK_KEY!);
4await ldClient.waitForInitialization();
5const bootstrapData = ldClient.allFlagsState(ctx);
6// Pass serialized state to client
A vs B — Next.js bootstrap
ts
1// Server component (app/layout.tsx)
2import { fetchDatafile } from '@avsbhq/node/server';
3
4const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);
5// Pass datafile to the client-side AvsbProvider
6
7// Client provider (app/providers.tsx)
8'use client'
9import { AvsbProvider } from '@avsbhq/react';
10import type { FlagDatafile } from '@avsbhq/react';
11
12export function Providers({ datafile, children }: {
13 datafile: FlagDatafile | null;
14 children: React.ReactNode;
15}) {
16 return (
17 <AvsbProvider
18 sdkKey={process.env.NEXT_PUBLIC_AVSB_SDK_KEY!}
19 context={{ kind: 'user', key: 'anon' }}
20 bootstrap={datafile ?? undefined}
21 >
22 {children}
23 </AvsbProvider>
24 );
25}

Sticky bucketing

LaunchDarkly's sticky bucketing stores a simple variation index. A vs B's StickyBucketService stores a richer StickyAssignment that includes the rule that produced the assignment and the time it was made — enabling age-based reassignment policies.

LaunchDarkly
ts
1// Provide a StickyBucketingService implementation at init
2const client = ld.init(sdkKey, {
3 stickyBucketService: new RedisStickyBucketService(redisClient),
4});
A vs B
ts
1import { StickyBucketService, StickyAssignment } from '@avsbhq/core';
2
3class RedisStickyService implements StickyBucketService {
4 lookup(userId: string, flagKey: string): StickyAssignment | null {
5 // return { variationId, ruleId, ruleType, assignedAt } or null
6 }
7 save(userId: string, flagKey: string, assignment: StickyAssignment): void {
8 // persist { variationId, ruleId, ruleType, assignedAt }
9 }
10}
11
12const server = new AvsbServer({
13 sdkKey: process.env.AVSB_SDK_KEY!,
14 stickyBucketService: new RedisStickyService(),
15});

Holdouts

LaunchDarkly offers holdouts as an enterprise feature. In A vs B, holdouts are included in all plans and are configured in the dashboard under Holdouts. Users in the holdout cohort receive source: 'holdout' and the configured default variation for each flag — no SDK code change is needed. Exposure events fire with ruleType: 'holdout' so you can exclude held-out cohorts from A/B analysis.

A vs B — reading holdout source
ts
1const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);
2if (flag.source === 'holdout') {
3 // This user is in the holdout group — they see the control across all flags
4 // in this holdout. Do not count them in experiment metrics.
5}

Cleanup

LaunchDarkly
ts
1await client.close();
A vs B
ts
1// Flush any buffered events and stop polling/streaming
2await server.close(); // server SDK
3await client.close(); // browser SDK — also calls flush() internally

Testing

LaunchDarkly provides test fixture helpers per SDK. A vs B ships a dedicated @avsbhq/test package with a createMockClient that implements the full client interface with controllable flag values.

LaunchDarkly — test stub
ts
1import { TestData } from 'launchdarkly-node-server-sdk';
2
3const td = TestData.dataSource();
4td.update(td.flag('show_banner').booleanFlag().variationForAll(true));
5const client = ld.init('sdk-key', { dataSource: td });
A vs B — test stub
ts
1import { createMockClient } from '@avsbhq/test';
2
3const mockClient = createMockClient({
4 flags: {
5 show_banner: true,
6 homepage_theme: 'blue',
7 rate_limit: 100,
8 },
9});
10// mockClient conforms to the full AvsbClient interface
11// Inject it wherever your code expects an AvsbClient

Cutover checklist

1

Uninstall LaunchDarkly packages

Remove launchdarkly-node-server-sdk, launchdarkly-js-client-sdk, and launchdarkly-react-client-sdk from your package.json.
2

Install A vs B packages

Run npm install @avsbhq/node for server code, npm install @avsbhq/browser for browser code, and npm install @avsbhq/react for React apps.
3

Swap SDK key

Replace your LaunchDarkly SDK key / client-side ID environment variables with your A vs B SDK key from the Environments page.
4

Migrate context construction

Replace LDContext / LDUser objects with A vs B EvalContext. Field names (kind, key, attributes) are identical — the type import changes.
5

Replace variation calls

Replace each client.variation(key, ctx, default) with client.getBoolFlag / getStringFlag / getNumberFlag / getJsonFlag as appropriate. Drop the ctx argument on the browser client (it uses the bound context). Use forUser(ctx) on the server.
6

Replace variationDetail calls

Every variationDetail call becomes a plain getFlag call — the full Flag<T> envelope is always returned.
7

Migrate track calls

Replace client.track(event, ctx, data, metricValue) with client.track(event, { value, properties }). The ctx argument is no longer passed on the browser client.
8

Migrate identify calls

client.identify(ctx) works the same way. Add client.alias(anonCtx, identifiedCtx) at login time to stitch anonymous and authenticated identities.
9

Update sticky bucketing (if used)

Replace your StickyBucketingServiceimplementation with one that implements A vs B's StickyBucketService interface — lookup and save now work with StickyAssignment objects instead of plain variation index strings.
10

Update tests

Replace LaunchDarkly test fixtures with createMockClient from @avsbhq/test.
11

Verify and deploy

Run npm run build and npx tsc --noEmit. Deploy to a staging environment and confirm flag evaluations and metric events are reaching the A vs B dashboard before cutting over production.