Migrate from Statsig to A vs B
Statsig separates feature gates, dynamic configs, and experiments into three distinct evaluation surfaces. A vs B unifies all three under a single Flag<T> abstraction: boolean flags replace gates, JSON flags replace dynamic configs, and A/B test rules are configured on the same flag entity. Your gate keys, config keys, and event names migrate directly — what changes is the call site shape, the identity model, and the removal of the singleton pattern in favor of explicit client instances.
Concept mapping
| Statsig | A vs B |
|---|---|
Feature gate (checkGate) | Boolean flag (getBoolFlag) |
Dynamic config (getConfig) | JSON flag (getJsonFlag<T>) |
Experiment (getExperiment) | Flag with an A/B test rule (same key, evaluated via getFlag) |
Layer (getLayer) | Flag namespace — use distinct keys per flag; no layer abstraction |
StatsigUser | EvalContext (SingleContext) |
user.userID | context.key |
user.custom | Top-level attributes on EvalContext (no nesting needed) |
Statsig.initialize(key, options) (singleton) | new AvsbServer({ sdkKey }) (explicit instance) |
Statsig.checkGate(user, gateName) | server.forUser(ctx).getBoolFlag(key, false).value |
Statsig.getConfig(user, configName).value | server.forUser(ctx).getJsonFlag(key, {}).value |
Statsig.getExperiment(user, expName).get(param, default) | server.forUser(ctx).getJsonFlag(key, {}).value.param |
Statsig.logEvent(user, eventName, value, metadata) | server.track(eventKey, { context, value?, properties? }) |
Statsig.shutdown() | server.close() |
| Holdout (Statsig Pro) | FlagDatafileHoldout — included in all plans |
Client construction
Statsig uses a module-level singleton. A vs B uses an explicit server instance so you can run multiple SDK keys (e.g. per-project or per-environment) in the same process without conflict.
1import Statsig from 'statsig-node';2
3await Statsig.initialize('secret-server-key', {4 environment: { tier: 'production' },5});6// All calls via Statsig.checkGate(user, ...) singleton1import { AvsbServer } from '@avsbhq/node';2
3const server = new AvsbServer({4 sdkKey: process.env.AVSB_SDK_KEY!,5 defaultDecideOptions: [], // optional global decide options6});7const result = await server.onReady();8if (!result.success && !result.degraded) {9 // Hard failure — consider your fallback strategy10}11// All calls via server.forUser(ctx).getFlag(...)1import { StatsigClient } from '@statsig/js-client';2
3const client = new StatsigClient('client-key', { userID: 'u_123' });4await client.initializeAsync();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
Statsig passes the user object on every evaluation call. A vs B binds the context at client construction and mutates it via explicit identity methods. On the server, the context is passed per-call via forUser.
1// User passed on every call2const user = { userID: 'u_123', custom: { plan: 'pro', country: 'US' } };3Statsig.checkGate(user, 'my_gate');4
5// Update user — call with new user object on next evaluation6const updatedUser = { userID: 'u_123', custom: { plan: 'enterprise' } };7Statsig.checkGate(updatedUser, 'my_gate');1// Browser — context bound at construction, updated via identify/updateAttributes2client.identify({ kind: 'user', key: 'u_123', plan: 'pro', country: 'US' });3
4// Partial attribute patch (no full re-identify needed)5client.updateAttributes({ plan: 'enterprise' });6
7// Anonymous → identified stitching (send once per session at login)8await client.alias(9 { kind: 'user', key: 'anon_device_xyz' },10 { kind: 'user', key: 'u_123' }11);12
13// Server — context passed per call; no mutation needed14const ctx = { kind: 'user', key: 'u_123', plan: 'pro', country: 'US' };15server.forUser(ctx).getBoolFlag('my_gate', false);user.customobject maps to flat attributes on A vs B's EvalContext. Replace { userID: 'u_1', custom: { plan: 'pro' } } with { kind: 'user', key: 'u_1', plan: 'pro' }.Flag evaluation
The three Statsig calls — checkGate, getConfig, and getExperiment — each map to a different A vs B typed evaluator. The return type is always Flag<T>: access .value for the raw result and .isEnabled() as the boolean gate equivalent.
1// Feature gate2const showNewDash = Statsig.checkGate(user, 'new_dashboard');3
4// Dynamic config5const uiConfig = Statsig.getConfig(user, 'ui_settings');6const theme = uiConfig.get('theme', 'default');7
8// Experiment9const exp = Statsig.getExperiment(user, 'checkout_experiment');10const ctaText = exp.get('cta_text', 'Buy now');1const uc = server.forUser(ctx);2
3// Boolean gate equivalent4const showNewDash = uc.getBoolFlag('new_dashboard', false).value;5// Or use isEnabled() which also checks that a rule matched:6const gateOpen = uc.getBoolFlag('new_dashboard', false).isEnabled();7
8// Dynamic config equivalent — the whole JSON value9const uiConfig = uc.getJsonFlag<UiSettings>('ui_settings', { theme: 'default' }).value;10const theme = uiConfig.theme;11
12// Experiment — same as config; variation key tells you the arm13const checkoutFlag = uc.getJsonFlag<CheckoutConfig>('checkout_experiment', { ctaText: 'Buy now' });14const ctaText = checkoutFlag.value.ctaText;15const arm = checkoutFlag.variationKey; // 'control' | 'treatment_a' | nullTracking events
Statsig's logEventtakes a user, event name, optional string value, and optional metadata map. A vs B's track uses a single TrackPayload object where the numeric valuefield replaces Statsig's positional value argument (which Statsig also accepts as a number).
1Statsig.logEvent(user, 'purchase', 49.99, { orderId: 'ord_99', sku: 'pro' });1// Server2server.track('purchase', {3 context: ctx,4 value: 49.99,5 properties: { orderId: 'ord_99', sku: 'pro' },6});7
8// Browser (context bound)9client.track('purchase', {10 value: 49.99,11 properties: { orderId: 'ord_99', sku: 'pro' },12});Multi-context
Statsig supports multi-user context via StatsigUser fields like userID, email, ip, and custom units. A vs B uses a formally typed MultiContextwith named context kinds, which maps cleanly to Statsig's concept of per-unit-type targeting but with an explicit schema. You define context kinds in the dashboard before targeting against them.
1const user = {2 userID: 'u_123',3 custom: { companyID: 'org_42', companyTier: 'enterprise' },4};5// Target on companyID via custom fields in Statsig rules1import 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' on org-level rules9server.forUser(ctx).getBoolFlag('enterprise_feature', false);Streaming updates
Statsig's server SDK polls on a timer; the browser SDK uses a combination of polling and server-sent events depending on configuration. A vs B follows the same hybrid model: configurable poll interval with optional SSE push for the browser.
1const unsub = client.on('flagChange', ({ flagKey, previousValue, newValue }) => {2 // Re-render anything that depends on flagKey3});4// Call unsub() to stop listeningBootstrap / SSR
Statsig provides getClientInitializeResponse for server-side bootstrap. A vs B uses a FlagDatafile fetched on the server and passed as the bootstrap prop to AvsbProvider.
1const bootstrapValues = Statsig.getClientInitializeResponse(user);2// Serialize and embed in HTML for client pickup1// Server component2import { fetchDatafile } from '@avsbhq/node/server';3const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);4
5// Client provider6<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>7 {children}8</AvsbProvider>Holdouts
Statsig holdouts are a Pro/Enterprise feature. In A vs B, holdouts are available on all plans and are configured in the platform under Holdouts. Held-out users receive source: 'holdout' on any flag participating in the holdout — no SDK code changes are needed.
1const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);2if (flag.source === 'holdout') {3 // User is in the holdout group — exclude from experiment metrics4}Cleanup
1await Statsig.shutdown();1await server.close(); // flushes events and stops pollingTesting
1import { DynamicConfig } from 'statsig-node';2// Use Statsig's local mode or override APIs in test environments1import { createMockClient } from '@avsbhq/test';2
3const mock = createMockClient({4 flags: {5 new_dashboard: true,6 ui_settings: { theme: 'blue' },7 checkout_experiment: { ctaText: 'Get started' },8 },9});10// Inject mock wherever AvsbServer or AvsbClient is expectedCutover checklist
Remove Statsig packages
statsig-node, @statsig/js-client, and statsig-react from your project.Install A vs B packages
@avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.Replace singleton initialization
Statsig.initialize(key, opts) with new AvsbServer({ sdkKey }). Store the instance in a module-level variable or a DI container.Migrate StatsigUser to EvalContext
user.userID to context.key, set context.kind = 'user', and flatten user.custom attributes to top-level context properties.Replace gate checks
Statsig.checkGate(user, key) with server.forUser(ctx).getBoolFlag(key, false).value.Replace config and experiment calls
getConfig and getExperiment with getJsonFlag<T>(key, defaultValue). Define a TypeScript interface for each config shape for full type safety.Replace logEvent
Statsig.logEvent(user, name, value, metadata) with server.track(name, { context, value, properties }).Remove layer calls
Update tests
createMockClient from @avsbhq/test.Verify and deploy
npm run build and npx tsc --noEmit. Confirm flag evaluations and events appear in the A vs B dashboard before promoting to production.