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

StatsigA 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)
StatsigUserEvalContext (SingleContext)
user.userIDcontext.key
user.customTop-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).valueserver.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:

TypeScript
import Statsig from 'statsig-node';await Statsig.initialize('secret-server-key', {  environment: { tier: 'production' },});// All calls via Statsig.checkGate(user, ...) singleton
TypeScript6 lines

A vs B, Node:

TypeScript
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(...)
TypeScript11 lines

Statsig, Browser (@statsig/js-client):

TypeScript
import { StatsigClient } from '@statsig/js-client';const client = new StatsigClient('client-key', { userID: 'u_123' });await client.initializeAsync();
TypeScript4 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

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:

TypeScript
// 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');
TypeScript7 lines

A vs B:

TypeScript
// 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);
TypeScript16 lines
Info

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:

TypeScript
// 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');
TypeScript10 lines

A vs B, unified evaluator:

TypeScript
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' | null
TypeScript23 lines

Tracking 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:

TypeScript
Statsig.logEvent(user, 'purchase', 49.99, { orderId: 'ord_99', sku: 'pro' });
TypeScript1 line

A vs B:

TypeScript
// 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 });
TypeScript11 lines

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:

TypeScript
const user = {  userID: 'u_123',  custom: { companyID: 'org_42', companyTier: 'enterprise' },};// Target on companyID via custom fields in Statsig rules
TypeScript5 lines

A vs B, explicit 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' on org-level rulesserver.forUser(ctx).getBoolFlag('enterprise_feature', false);
TypeScript9 lines

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:

TypeScript
const unsub = client.on('flagChange', ({ flagKey, previousValue, newValue }) => {  // Re-render anything that depends on flagKey});// Call unsub() to stop listening
TypeScript4 lines

Bootstrap / 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:

TypeScript
const bootstrapValues = Statsig.getClientInitializeResponse(user);// Serialize and embed in HTML for client pickup
TypeScript2 lines

A vs B, server bootstrap:

TypeScript React
// 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>
TypeScript React10 lines

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:

TypeScript
const flag = server.forUser(ctx).getBoolFlag('checkout_redesign', false);if (flag.source === 'holdout') {  // User is in the holdout group: exclude from experiment metrics}
TypeScript4 lines

Cleanup

Statsig:

TypeScript
await Statsig.shutdown();
TypeScript1 line

A vs B:

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

Testing

Statsig, test overrides:

TypeScript
import { DynamicConfig } from 'statsig-node';// Use Statsig's local mode or override APIs in test environments
TypeScript2 lines

A vs B, mock client:

TypeScript
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 expected
TypeScript10 lines

Cutover checklist

1

Remove Statsig packages

Uninstall statsig-node, @statsig/js-client, and statsig-react from your project.

2

Install A vs B packages

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

3

Replace singleton initialization

Replace Statsig.initialize(key, opts) with new AvsbServer({ sdkKey }). Store the instance in a module-level variable or a DI container.

4

Migrate StatsigUser to EvalContext

Move user.userID to context.key, set context.kind = 'user', and flatten user.custom attributes to top-level context properties.

5

Replace gate checks

Replace Statsig.checkGate(user, key) with server.forUser(ctx).getBoolFlag(key, false).value.

6

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.

7

Replace logEvent

Replace Statsig.logEvent(user, name, value, metadata) with server.track(name, { context, value, properties }).

8

Remove layer calls

If you use Statsig layers, identify which flags each layer wraps and address each flag key directly in A vs B.

9

Update tests

Replace Statsig test utilities with createMockClient from @avsbhq/test.

10

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.

Was this helpful?