From Split

Split.io's architecture (local evaluation against a downloaded split definition file, impression events for exposure tracking, and an explicit track call 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 (a hyphen in a split name becomes an underscore in the key)
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:

TypeScript
import { SplitFactory } from '@splitsoftware/splitio';const factory = SplitFactory({  core: {    authorizationKey: 'YOUR_SERVER_SIDE_API_KEY',  },  impressionListener: {    logImpression: (impressionData: { impression: unknown }) => {      console.log(impressionData.impression);    },  },});const splitClient = factory.client();await splitClient.ready();
TypeScript14 lines

A vs B, Node server:

TypeScript
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });const result = await server.onReady();// Wire exposure events (replaces impressionListener)server.on('exposure', ({ experimentId, variationId, visitorId, properties }) => {  console.log({ experimentId, variationId, visitorId, flagKey: properties?.flagKey });});
TypeScript9 lines

Split.io, Browser:

TypeScript
import { SplitFactory } from '@splitsoftware/splitio';const factory = SplitFactory({  core: {    authorizationKey: 'YOUR_BROWSER_API_KEY',    key: 'u_123',  },});const splitClient = factory.client();await splitClient.ready();
TypeScript10 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' },});await client.onReady();
TypeScript7 lines

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:

TypeScript
// Key passed per callconst treatment = splitClient.getTreatment('u_123', 'show_banner', { plan: 'pro' });// No partial attribute update: pass new attributes on each call
TypeScript4 lines

A vs B:

TypeScript
// Server: context per callconst flag = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' })  .getBoolFlag('show_banner', false);// Browser: bound context, explicit mutationsclient.identify({ kind: 'user', key: 'u_123', plan: 'pro' });client.updateAttributes({ plan: 'enterprise' }); // partial patch// Anonymous → identified stitching at sign-up. Synchronous: it records the// moment, it does not move past decisions.client.alias(  { kind: 'user', key: 'anon_xyz' },  { kind: 'user', key: 'u_123' });
TypeScript14 lines

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:

TypeScript
// Boolean splitconst treatment = splitClient.getTreatment('u_123', 'show_banner');if (treatment === 'on') { /* ... */ }// Treatment with config (dynamic config attached to variation)const result = splitClient.getTreatmentWithConfig('u_123', 'checkout_redesign');// result.treatment, result.config (JSON string)const config = JSON.parse(result.config ?? '{}');// Batch evaluationconst treatments = splitClient.getTreatments('u_123', ['flag_a', 'flag_b']);
TypeScript11 lines

A vs B:

TypeScript
// Boolean flagconst flag = server.forUser({ kind: 'user', key: 'u_123' })  .getBoolFlag('show_banner', false);if (flag.isEnabled()) { /* ... */ }// flag.variationKey → 'on' | 'off' | null// Flag with JSON config: the JSON value is directly typedinterface CheckoutConfig { ctaText: string; showPromo: boolean }const checkoutFlag = server.forUser({ kind: 'user', key: 'u_123' })  .getJsonFlag<CheckoutConfig>('checkout_redesign', { ctaText: 'Buy', showPromo: false });const { ctaText, showPromo } = checkoutFlag.value;// All flags (no batched eval needed; all evaluate synchronously from in-memory datafile)const allFlags = client.getAllFlags();
TypeScript14 lines
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:

TypeScript
splitClient.track('user', 'u_123', 'purchase_completed', 49.99, { orderId: 'ord_99' });
TypeScript1 line

A vs B:

TypeScript
// Server. `revenue` is money in major units; `value` is the separate// numeric-metric column. Conversion events do not store properties, so// Split's `orderId` above has no A vs B equivalent on this call.server.track('purchase_completed', {  context: { kind: 'user', key: 'u_123' },  revenue: 49.99,});// Browser (context bound)client.track('purchase_completed', { revenue: 49.99 });
TypeScript10 lines

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:

TypeScript
// Use the account ID as the key to get account-scoped trafficconst treatment = splitClient.getTreatment('org_42', 'enterprise_feature', { tier: 'enterprise' });
TypeScript2 lines

A vs B, multi-context:

TypeScript
import type { MultiContext } from '@avsbhq/core';const ctx: MultiContext = {  kind: 'multi',  user:         { kind: 'user',         key: 'u_123' },  organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },};// In the dashboard, set hashAttribute to 'organization.key' for org-scoped rulesserver.forUser(ctx).getBoolFlag('enterprise_feature', false);
TypeScript9 lines

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:

TypeScript
client.on('configUpdate', ({ publishedAt, reason }) => {  // reason: 'poll' | 'stream' | 'manual'});client.on('flagChange', ({ flagKey, previousValue, newValue }) => {  // A specific flag value changed});
TypeScript7 lines

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:

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

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:

TypeScript
splitClient.destroy();
TypeScript1 line

A vs B:

TypeScript
await server.close(); // flushes buffered events, stops polling
TypeScript1 line

Testing

Split.io, localhost mode:

TypeScript
const factory = SplitFactory({  core: { authorizationKey: 'localhost' },  features: {    show_banner: 'on',    checkout_redesign: 'treatment_a',  },});
TypeScript7 lines

A vs B, mock client:

TypeScript
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({  flags: {    show_banner: true,    checkout_redesign: { ctaText: 'Get started', showPromo: true },  },});
TypeScript8 lines

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.

Was this helpful?