From Statsig
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.
Statsig, Node:
import Statsig from 'statsig-node';await Statsig.initialize('secret-server-key', { environment: { tier: 'production' },});// All calls via Statsig.checkGate(user, ...) singletonA vs B, Node:
import { AvsbServer } from '@avsbhq/node';const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY!,});const result = await server.onReady();if (!result.success) { // Hard failure: no datafile loaded. (The server SDK never sets // `result.degraded`; that field only applies to the browser client.)}// All calls via server.forUser(ctx).getFlag(...)Statsig, Browser (@statsig/js-client):
import { StatsigClient } from '@statsig/js-client';const client = new StatsigClient('client-key', { userID: 'u_123' });await client.initializeAsync();A vs B, Browser:
import { AvsbClient } from '@avsbhq/browser';const client = new AvsbClient({ sdkKey: 'sdk_production_xxxxxxxxxxxxxxxx', context: { kind: 'user', key: 'u_123' },});await 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.
Statsig:
// User passed on every callconst user = { userID: 'u_123', custom: { plan: 'pro', country: 'US' } };Statsig.checkGate(user, 'my_gate');// Update user: call with new user object on next evaluationconst updatedUser = { userID: 'u_123', custom: { plan: 'enterprise' } };Statsig.checkGate(updatedUser, 'my_gate');A vs B:
// Browser: context bound at construction, updated via identify/updateAttributesclient.identify({ kind: 'user', key: 'u_123', plan: 'pro', country: 'US' });// Partial attribute patch (no full re-identify needed)client.updateAttributes({ plan: 'enterprise' });// Anonymous → identified stitching, sent once per session at login.// Synchronous: it records the moment, it does not move past decisions.client.alias( { kind: 'user', key: 'anon_device_xyz' }, { kind: 'user', key: 'u_123' });// Server: context passed per call; no mutation neededconst ctx = { kind: 'user', key: 'u_123', plan: 'pro', country: 'US' };server.forUser(ctx).getBoolFlag('my_gate', false);Statsig's user.custom object 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.
Statsig, gates and configs:
// Feature gateconst showNewDash = Statsig.checkGate(user, 'new_dashboard');// Dynamic configconst uiConfig = Statsig.getConfig(user, 'ui_settings');const theme = uiConfig.get('theme', 'default');// Experimentconst exp = Statsig.getExperiment(user, 'checkout_experiment');const ctaText = exp.get('cta_text', 'Buy now');A vs B, unified evaluator:
interface UiSettings { theme: string;}interface CheckoutConfig { ctaText: string;}const uc = server.forUser(ctx);// Boolean gate equivalentconst showNewDash = uc.getBoolFlag('new_dashboard', false).value;// Or use isEnabled(): true only for a real decision (a rule, holdout,// bandit, or override, not just the default) with a truthy value.const gateOpen = uc.getBoolFlag('new_dashboard', false).isEnabled();// Dynamic config equivalent: the whole JSON valueconst uiConfig = uc.getJsonFlag<UiSettings>('ui_settings', { theme: 'default' }).value;const theme = uiConfig.theme;// Experiment: same as config; variation key tells you the armconst checkoutFlag = uc.getJsonFlag<CheckoutConfig>('checkout_experiment', { ctaText: 'Buy now' });const ctaText = checkoutFlag.value.ctaText;const arm = checkoutFlag.variationKey; // 'control' | 'treatment_a' | nullTracking events
Statsig's logEvent takes a user, event name, optional string value, and optional metadata map. A vs B's track uses a single TrackPayload object where the numeric value field replaces Statsig's positional value argument (which Statsig also accepts as a number).
Statsig:
Statsig.logEvent(user, 'purchase', 49.99, { orderId: 'ord_99', sku: 'pro' });A vs B:
// Server. Statsig's positional value becomes `revenue` for money in major// units, or `value` for a plain numeric metric. Conversion events do not// store properties, so Statsig's metadata (orderId, sku) has no A vs B// equivalent on this call.server.track('purchase', { context: ctx, revenue: 49.99,});// Browser (context bound)client.track('purchase', { revenue: 49.99 });Multi-context
Statsig supports multi-user context via StatsigUser fields like userID, email, ip, and custom units. A vs B uses a formally typed MultiContext with 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.
Statsig, company-level targeting:
const user = { userID: 'u_123', custom: { companyID: 'org_42', companyTier: 'enterprise' },};// Target on companyID via custom fields in Statsig rulesA vs B, explicit multi-context:
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' on org-level rulesserver.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.
A vs B, flag change listener:
const unsub = client.on('flagChange', ({ flagKey, previousValue, newValue }) => { // Re-render anything that depends on flagKey});// 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.
Statsig, server bootstrap:
const bootstrapValues = Statsig.getClientInitializeResponse(user);// Serialize and embed in HTML for client pickupA vs B, server bootstrap:
// Server componentimport { AvsbProvider } from '@avsbhq/react';import { fetchDatafile } from '@avsbhq/browser/server';const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);// Client provider<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}> <YourApp /></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, so no SDK code changes are needed.
A vs B, detecting holdout traffic:
const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);if (flag.source === 'holdout') { // User is in the holdout group: exclude from experiment metrics}Cleanup
Statsig:
await Statsig.shutdown();A vs B:
await server.close(); // flushes events and stops pollingTesting
Statsig, test overrides:
import { DynamicConfig } from 'statsig-node';// Use Statsig's local mode or override APIs in test environmentsA vs B, mock client:
import { createMockClient } from '@avsbhq/test';const mock = createMockClient({ flags: { new_dashboard: true, ui_settings: { theme: 'blue' }, checkout_experiment: { ctaText: 'Get started' }, },});// Inject mock wherever AvsbServer or AvsbClient is expectedCutover checklist
Remove Statsig packages
Uninstall statsig-node, @statsig/js-client, and statsig-react from your project.
Install A vs B packages
Install @avsbhq/node, @avsbhq/browser, and @avsbhq/react as needed.
Replace singleton initialization
Replace Statsig.initialize(key, opts) with new AvsbServer({ sdkKey }). Store the instance in a module-level variable or a DI container.
Migrate StatsigUser to EvalContext
Move user.userID to context.key, set context.kind = 'user', and flatten user.custom attributes to top-level context properties.
Replace gate checks
Replace Statsig.checkGate(user, key) with server.forUser(ctx).getBoolFlag(key, false).value.
Replace config and experiment calls
Replace getConfig and getExperiment with getJsonFlag<T>(key, defaultValue). Define a TypeScript interface for each config shape for full type safety.
Replace logEvent
Replace Statsig.logEvent(user, name, value, metadata) with server.track(name, { context, value, properties }).
Remove layer calls
If you use Statsig layers, identify which flags each layer wraps and address each flag key directly in A vs B.
Update tests
Replace Statsig test utilities with createMockClient from @avsbhq/test.
Verify and deploy
Run npm run build and npx tsc --noEmit. Confirm flag evaluations and events appear in the A vs B dashboard before promoting to production.