Migrate from PostHog Feature Flags to A vs B
PostHog is a product analytics platform that ships feature flags as one of several built-in tools. If you are migrating only your feature flag and A/B test layer — keeping PostHog for analytics — A vs B slots in alongside it. If you are replacing PostHog entirely, A vs B covers feature flags and experiment analysis; you will need a separate product analytics tool for session recording and funnels. PostHog's feature flag API surface is compact, so the migration is small: swap three methods and add an event-context object.
Concept mapping
| PostHog Feature Flags | A vs B |
|---|---|
| Feature flag | Feature flag (keys transfer directly) |
posthog.getFeatureFlag(key) | client.getFlag(key, null).value |
posthog.isFeatureEnabled(key) | client.getBoolFlag(key, false).isEnabled() |
posthog.getFeatureFlagPayload(key) | client.getJsonFlag<T>(key, null).value |
| String / multivariate flag | String flag (getStringFlag) |
posthog.identify(userId, properties) | client.identify({ kind: 'user', key: userId, ...properties }) |
posthog.capture(eventName, properties) | client.track(eventKey, { properties }) |
posthog.reloadFeatureFlags() | client.refresh() (manual datafile refresh) |
| PostHog distinct ID | context.key |
| Person properties | Top-level attributes on EvalContext |
| No holdout concept | FlagDatafileHoldout — available on all plans |
| No multi-context | MultiContext — new capability gained on migration |
posthog.reset() | client.identify({ kind: 'user', key: 'anon_new' }) |
Client construction
PostHog initializes via a global posthog.init call with the API key and host. A vs B uses an explicit client instance with an SDK key.
1import posthog from 'posthog-js';2
3posthog.init('phc_YOUR_KEY', {4 api_host: 'https://app.posthog.com',5});1import { AvsbClient } from '@avsbhq/browser';2
3const client = new AvsbClient({4 sdkKey: 'sdk_production_abc123',5 context: { kind: 'user', key: posthog.get_distinct_id() },6});7await client.onReady();1import { PostHog } from 'posthog-node';2
3const posthog = new PostHog('phc_YOUR_KEY', { host: 'https://app.posthog.com' });1import { AvsbServer } from '@avsbhq/node';2
3const server = new AvsbServer({ sdkKey: process.env.AVSB_SDK_KEY! });4const result = await server.onReady();Identity model
PostHog uses posthog.identify(distinctId, properties) to associate a user and their properties with the PostHog distinct ID. A vs B maps this directly to client.identify with a context object.
1// Identify after login2posthog.identify('user_123', {3 email: 'user@example.com',4 plan: 'pro',5 company: 'Acme',6});7
8// Partial update — re-call identify with all properties1// Full context replacement (equivalent to PostHog identify)2client.identify({3 kind: 'user',4 key: 'user_123',5 email: 'user@example.com',6 plan: 'pro',7 company: 'Acme',8});9
10// Partial attribute patch — no need to repeat unchanged fields11client.updateAttributes({ plan: 'enterprise' });12
13// Stitch anonymous pre-login identity to the identified user14await client.alias(15 { kind: 'user', key: posthog.get_distinct_id() }, // anonymous id16 { kind: 'user', key: 'user_123' } // identified id17);Flag evaluation
PostHog provides three flag evaluation methods. A vs B unifies them under typed evaluators that always return a Flag<T>envelope, so you get evaluation metadata for free without a separate “details” call.
1// Boolean check2const showBanner = posthog.isFeatureEnabled('show_banner');3
4// Multivariate / string variant5const theme = posthog.getFeatureFlag('homepage_theme'); // returns string | boolean | undefined6
7// JSON payload8const config = posthog.getFeatureFlagPayload('pricing_config'); // returns JsonType | null1// Boolean flag2const showBanner = client.getBoolFlag('show_banner', false).value;3// Or use isEnabled() which checks that a rule actually matched:4const enabled = client.getBoolFlag('show_banner', false).isEnabled();5
6// String / multivariate flag7const theme = client.getStringFlag('homepage_theme', 'default').value;8
9// JSON payload10interface PricingConfig { basePrice: number; currency: string }11const config = client.getJsonFlag<PricingConfig>('pricing_config', null).value;12
13// Full envelope (equivalent to fetching metadata)14const flag = client.getBoolFlag('show_banner', false);15flag.variationKey // 'test' | 'control' | null16flag.source // 'rule' | 'default' | 'holdout' | ...17flag.reasons // string[]1// Requires a distinct ID per call (no client-level binding)2const flagValue = await posthog.getFeatureFlag('show_banner', 'user_123');3const isEnabled = await posthog.isFeatureEnabled('show_banner', 'user_123');1const flag = server.forUser({ kind: 'user', key: 'user_123' })2 .getBoolFlag('show_banner', false);Tracking events
PostHog uses posthog.capture for all analytics events. In A vs B, client.track sends conversion events to the experiment analytics pipeline. If you are keeping PostHog for analytics, you can call both — they operate independently.
1posthog.capture('purchase_completed', {2 revenue: 49.99,3 orderId: 'ord_99',4 plan: 'pro',5});1// Sends to A vs B experiment analytics only2client.track('purchase_completed', {3 value: 49.99,4 properties: { orderId: 'ord_99', plan: 'pro' },5});6
7// If keeping PostHog for analytics, also call:8// posthog.capture('purchase_completed', { revenue: 49.99, ... });Multi-context
PostHog targets feature flags on a single user identity. A vs B adds multi-context targeting — a new capability you gain on migration. You can target and bucket on organization, device, or any other entity kind alongside the user context.
1import type { MultiContext } from '@avsbhq/core';2
3const ctx: MultiContext = {4 kind: 'multi',5 user: { kind: 'user', key: 'user_123', plan: 'pro' },6 organization: { kind: 'organization', key: 'org_42', tier: 'enterprise' },7};8server.forUser(ctx).getBoolFlag('enterprise_feature', false);Streaming updates
PostHog reloads feature flags via posthog.reloadFeatureFlags() or on an internal timer. A vs B polls automatically at a configurable interval and emits events when the datafile changes.
1posthog.onFeatureFlags((flags) => {2 // Flags loaded or reloaded3});1client.on('configUpdate', ({ reason }) => {2 // Datafile refreshed — reason: 'poll' | 'stream' | 'manual'3});4
5client.on('flagChange', ({ flagKey, previousValue, newValue }) => {6 // A specific flag value changed7});8
9// Manual refresh (equivalent to reloadFeatureFlags)10await client.refresh();Bootstrap / SSR
PostHog's server-side Node SDK evaluates flags per request without a bootstrap step. A vs B supports a full SSR bootstrap pattern that eliminates client-side loading state entirely.
1// Server component2const flagValue = await posthog.isFeatureEnabled('show_banner', userId);1// Server component (app/layout.tsx)2import { fetchDatafile } from '@avsbhq/node/server';3const datafile = await fetchDatafile(process.env.AVSB_SDK_KEY!);4
5// Client provider — flags evaluate synchronously on first render6<AvsbProvider sdkKey="..." context={ctx} bootstrap={datafile ?? undefined}>7 {children}8</AvsbProvider>Holdouts
PostHog feature flags do not have a holdout concept. A vs B includes holdouts on all plans. Create a holdout in the dashboard and associate flags with it to maintain a clean control group across multiple concurrent experiments.
Cleanup
1await posthog.shutdown();1await server.close(); // flushes buffered events, stops pollingTesting
1// PostHog test mode: use overrideFeatureFlag2posthog.featureFlags.override({ show_banner: true });1import { createMockClient } from '@avsbhq/test';2
3const mock = createMockClient({4 flags: {5 show_banner: true,6 homepage_theme: 'blue',7 },8});Cutover checklist
Decide scope
posthog.capture calls in place and add client.track alongside them for experiment conversions.Remove or reduce PostHog packages
posthog-js and posthog-node. If keeping analytics: leave the packages but remove feature flag calls.Install A vs B packages
@avsbhq/browser and/or @avsbhq/node and @avsbhq/react as needed.Migrate identify calls
posthog.identify(distinctId, properties) with client.identify({ kind: 'user', key: distinctId, ...properties }).Replace flag evaluation calls
posthog.isFeatureEnabled(key) with client.getBoolFlag(key, false).isEnabled(). Replace posthog.getFeatureFlag(key) with client.getStringFlag(key, null).value. Replace posthog.getFeatureFlagPayload(key) with client.getJsonFlag<T>(key, null).value.Add conversion tracking
client.track(eventKey, { value, properties }) at each conversion point you want to measure in A/B test results.Update tests
posthog.featureFlags.override with createMockClient from @avsbhq/test.Verify and deploy
npm run build and npx tsc --noEmit. Confirm flag evaluations and conversion events appear in the A vs B dashboard before promoting to production.