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.io | A vs B |
|---|---|
| Split (feature flag) | Feature flag (keys transfer directly) |
| Treatment | Flag<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 key | context.key |
| Bucketing key | context.key (or a custom hashAttribute per rule) |
| Attributes | Top-level properties on EvalContext |
client.track(trafficType, key, eventType, value, properties) | server.track(eventKey, { context, value?, properties? }) |
| Impression listener | client.on('exposure', handler) |
SplitFactory | new AvsbServer({ sdkKey }) |
factory.client(key) | server.forUser({ kind: 'user', key }) |
client.destroy() | server.close() |
| No holdout concept | FlagDatafileHoldout — 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.
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();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});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();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.
1// Key passed per call2const treatment = client.getTreatment('u_123', 'show_banner', { plan: 'pro' });3
4// No partial attribute update — pass new attributes on each call1// Server — context per call2const flag = server.forUser({ kind: 'user', key: 'u_123', plan: 'pro' })3 .getBoolFlag('show_banner', false);4
5// Browser — bound context, explicit mutations6client.identify({ kind: 'user', key: 'u_123', plan: 'pro' });7client.updateAttributes({ plan: 'enterprise' }); // partial patch8
9// Anonymous → identified stitching at sign-up10await 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.
1// Boolean split2const 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 evaluation11const treatments = client.getTreatments('u_123', ['flag_a', 'flag_b']);1// Boolean flag2const flag = server.forUser({ kind: 'user', key: 'u_123' })3 .getBoolFlag('show_banner', false);4if (flag.isEnabled()) { /* ... */ }5// flag.variationKey → 'on' | 'off' | null6
7// Flag with JSON config — the JSON value is directly typed8interface 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();'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).
1// NOTE: verify before publish — Split.io track signature subject to change in upstream SDK2client.track('user', 'u_123', 'purchase_completed', 49.99, { orderId: 'ord_99' });1// Server2server.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.
1// Use the account ID as the key to get account-scoped traffic2const treatment = client.getTreatment('org_42', 'enterprise_feature', { tier: 'enterprise' });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 rules9server.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.
1client.on('configUpdate', ({ publishedAt, reason }) => {2 // reason: 'poll' | 'stream' | 'manual'3});4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {6 // A specific flag value changed7});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.
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
1client.destroy();1await server.close(); // flushes buffered events, stops pollingTesting
1const factory = SplitFactory({2 core: { authorizationKey: 'localhost' },3 features: {4 show_banner: 'on',5 checkout_redesign: 'treatment_a',6 },7});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
Remove Split packages
@splitsoftware/splitio and related packages.Install A vs B packages
@avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.Replace SplitFactory with AvsbServer
SplitFactory({ core: { authorizationKey } }) with new AvsbServer({ sdkKey }).Migrate traffic types to context kinds
EvalContext objects that use kind to distinguish user, account, device, and other entity types.Replace getTreatment calls
client.getTreatment(key, splitName, attrs) with server.forUser(ctx).getBoolFlag(splitName, false) (boolean splits) or getStringFlag / getJsonFlag as appropriate.Migrate impressionListener
impressionListener factory option with server.on('exposure', handler).Migrate track calls
client.track(trafficType, key, eventType, value, props) with server.track(eventType, { context, value, properties }).Update localhost / test mode
localhost mode with createMockClient from @avsbhq/test.Verify and deploy
npm run build and npx tsc --noEmit. Confirm flag evaluations and events in the A vs B dashboard before promoting to production.