/Docs

Migrate from Split to A vs B

Split.io's architecture — local evaluation against a downloaded split definition file, impression events for exposure tracking, and an explicit trackcall for metrics — maps closely to A vs B. The biggest conceptual shift is the removal of “traffic types”: Split uses traffic types (user, account, device) as a top-level SDK primitive, while A vs B handles the same concept through context kinds on EvalContext. Your split keys, event types, and attribute names transfer directly.

Concept mapping

Split.ioA vs B
Split (feature flag)Feature flag (keys transfer directly)
TreatmentFlag<T>.variationKey (same concept; string key)
getTreatment(key, splitName, attributes)server.forUser(ctx).getStringFlag(key, 'control').variationKey
getTreatmentWithConfig(key, splitName)server.forUser(ctx).getJsonFlag<T>(splitName, {}).value
getTreatments(key, splitNames, attributes)client.getAllFlags() (all flags; no batched eval)
Traffic type (user, account, device)Context kind (kind: 'user' / 'organization' / 'device')
Matching keycontext.key
Bucketing keycontext.key (or a custom hashAttribute per rule)
AttributesTop-level properties on EvalContext
client.track(trafficType, key, eventType, value, properties)server.track(eventKey, { context, value?, properties? })
Impression listenerclient.on('exposure', handler)
SplitFactorynew AvsbServer({ sdkKey })
factory.client(key)server.forUser({ kind: 'user', key })
client.destroy()server.close()
No holdout conceptFlagDatafileHoldout — available on all plans

Client construction

Split uses a SplitFactory to produce clients, with separate server and browser factories. A vs B uses a single AvsbServer / AvsbClient constructor.

Split.io — Node server
ts
1import { SplitFactory } from '@splitsoftware/splitio';
2
3const factory = SplitFactory({
4 core: {
5 authorizationKey: 'YOUR_SERVER_SIDE_API_KEY',
6 },
7 impressionListener: {
8 logImpression: (impressionData) => {
9 console.log(impressionData.impression);
10 },
11 },
12});
13const client = factory.client();
14await client.ready();
A vs B — Node server
ts
1import { AvsbServer } from '@avsbhq/node';
2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });
4const result = await server.onReady();
5
6// Wire exposure events (replaces impressionListener)
7server.on('exposure', ({ flagKey, variationKey, contextKeys }) => {
8 console.log({ flagKey, variationKey, contextKeys });
9});
Split.io — Browser
ts
1import { SplitFactory } from '@splitsoftware/splitio';
2
3const factory = SplitFactory({
4 core: {
5 authorizationKey: 'YOUR_BROWSER_API_KEY',
6 key: 'u_123',
7 },
8});
9const client = factory.client();
10await client.ready();
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' },
6});
7await client.onReady();

Identity model

Split passes the matching key on every evaluation call. A vs B follows the same per-call pattern on the server via forUser. On the browser client, the context is bound at construction and mutated via explicit methods.

Split.io
ts
1// Key passed per call
2const treatment = client.getTreatment('u_123', 'show_banner', { plan: 'pro' });
3
4// No partial attribute update — pass new attributes on each call
A vs B
ts
1// Server — context per call
2const flag = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' })
3 .getBoolFlag('show_banner', false);
4
5// Browser — bound context, explicit mutations
6client.identify({ kind: 'user', key: 'u_123', plan: 'pro' });
7client.updateAttributes({ plan: 'enterprise' }); // partial patch
8
9// Anonymous → identified stitching at sign-up
10await client.alias(
11 { kind: 'user', key: 'anon_xyz' },
12 { kind: 'user', key: 'u_123' }
13);

Flag evaluation

Split's getTreatment returns a treatment string ('on', 'off', or a named variant). A vs B surfaces the treatment as Flag<T>.variationKey alongside the typed .value. For boolean splits (on/off), use getBoolFlag; for named treatments, use getStringFlag and read .variationKey.

Split.io
ts
1// Boolean split
2const treatment = client.getTreatment('u_123', 'show_banner');
3if (treatment === 'on') { /* ... */ }
4
5// Treatment with config (dynamic config attached to variation)
6const result = client.getTreatmentWithConfig('u_123', 'checkout_redesign');
7// result.treatment, result.config (JSON string)
8const config = JSON.parse(result.config ?? '{}');
9
10// Batch evaluation
11const treatments = client.getTreatments('u_123', ['flag_a', 'flag_b']);
A vs B
ts
1// Boolean flag
2const flag = server.forUser({ kind: 'user', key: 'u_123' })
3 .getBoolFlag('show_banner', false);
4if (flag.isEnabled()) { /* ... */ }
5// flag.variationKey → 'on' | 'off' | null
6
7// Flag with JSON config — the JSON value is directly typed
8interface CheckoutConfig { ctaText: string; showPromo: boolean }
9const checkoutFlag = server.forUser({ kind: 'user', key: 'u_123' })
10 .getJsonFlag<CheckoutConfig>('checkout_redesign', { ctaText: 'Buy', showPromo: false });
11const { ctaText, showPromo } = checkoutFlag.value;
12
13// All flags (no batched eval needed — all evaluate synchronously from in-memory datafile)
14const allFlags = client.getAllFlags();
Info
Split returns 'control' as the treatment when the split definition is not found or evaluation fails. A vs B returns the defaultValue you pass to getFlag and sets source: 'not_found' or source: 'default' on the result. Check flag.exists()to distinguish “flag not found” from “flag found but default served”.

Tracking events

Split's track takes traffic type, key, event type, value, and properties as positional arguments. A vs B uses a named payload object and reads context from forUser (server) or the bound context (browser).

Split.io
ts
1// NOTE: verify before publish — Split.io track signature subject to change in upstream SDK
2client.track('user', 'u_123', 'purchase_completed', 49.99, { orderId: 'ord_99' });
A vs B
ts
1// Server
2server.track('purchase_completed', {
3 context: { kind: 'user', key: 'u_123' },
4 value: 49.99,
5 properties: { orderId: 'ord_99' },
6});
7
8// Browser (context bound)
9client.track('purchase_completed', {
10 value: 49.99,
11 properties: { orderId: 'ord_99' },
12});

Multi-context

Split's traffic types (user, account, device) are a similar concept to A vs B's context kinds, but in Split the traffic type is resolved by passing a different key namespace. In A vs B, multi-context is a first-class data structure — you pass all entity contexts together and rules declare which context kind they hash on.

Split.io — account-level treatment
ts
1// Use the account ID as the key to get account-scoped traffic
2const treatment = client.getTreatment('org_42', 'enterprise_feature', { tier: 'enterprise' });
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' },
6 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },
7};
8// In the dashboard, set hashAttribute to 'organization.key' for org-scoped rules
9server.forUser(ctx).getBoolFlag('enterprise_feature', false);

Streaming updates

Split uses a streaming connection (SSE) for real-time split definition updates. A vs B uses configurable polling with optional SSE for the browser client. The behavioral outcome is the same — the client picks up flag changes without a deployment.

A vs B — update listener
ts
1client.on('configUpdate', ({ publishedAt, reason }) => {
2 // reason: 'poll' | 'stream' | 'manual'
3});
4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {
6 // A specific flag value changed
7});

Bootstrap / SSR

Split does not provide a native SSR bootstrap mechanism for Next.js. A vs B supports a full bootstrap pattern where the datafile is pre-fetched on the server and passed to the client provider, eliminating any loading state.

A vs B — SSR bootstrap
ts
1import { fetchDatafile } from '@avsbhq/node/server';
2const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);
3
4<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>
5 {children}
6</AvsbProvider>

Holdouts

Split does not have a built-in holdout concept. A vs B includes holdouts on all plans. Create a holdout in the dashboard, associate flags with it, and the SDK automatically routes held-out users to the control variation with source: 'holdout'.

Cleanup

Split.io
ts
1client.destroy();
A vs B
ts
1await server.close(); // flushes buffered events, stops polling

Testing

Split.io — localhost mode
ts
1const factory = SplitFactory({
2 core: { authorizationKey: 'localhost' },
3 features: {
4 show_banner: 'on',
5 checkout_redesign: 'treatment_a',
6 },
7});
A vs B — mock client
ts
1import { createMockClient } from '@avsbhq/test';
2
3const mock = createMockClient({
4 flags: {
5 show_banner: true,
6 checkout_redesign: { ctaText: 'Get started', showPromo: true },
7 },
8});

Cutover checklist

1

Remove Split packages

Uninstall @splitsoftware/splitio and related packages.
2

Install A vs B packages

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

Replace SplitFactory with AvsbServer

Replace SplitFactory({ core: { authorizationKey } }) with new AvsbServer({ sdkKey }).
4

Migrate traffic types to context kinds

Replace per-call key / traffic-type pairs with EvalContext objects that use kind to distinguish user, account, device, and other entity types.
5

Replace getTreatment calls

Replace client.getTreatment(key, splitName, attrs) with server.forUser(ctx).getBoolFlag(splitName, false) (boolean splits) or getStringFlag / getJsonFlag as appropriate.
6

Migrate impressionListener

Replace the impressionListener factory option with server.on('exposure', handler).
7

Migrate track calls

Replace client.track(trafficType, key, eventType, value, props) with server.track(eventType, { context, value, properties }).
8

Update localhost / test mode

Replace Split's localhost mode with createMockClient from @avsbhq/test.
9

Verify and deploy

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