Summary
The landing-variant A/B test is not splitting traffic — every real visitor renders the free-plus fallback. The PostHog flag is configured correctly and resolves a clean ~50/50 split server-side; the failure is in client-side variant resolution (src/bootstrap/variant-loader.ts). This bundle owns the experiment; the server-side enabler lives in the plugin.
Companion issue (plugin-side init/bootstrap fix): wcpos/woocommerce-pos#1188
Evidence (from production PostHog / ClickHouse on ph.wcpos.com)
landing_variant_rendered, all-time:
| render_source |
variant |
count |
who |
fallback |
free-plus |
337 |
every real visitor |
cache |
indie |
2 |
manual test hits only |
$feature_flag_called for landing-variant: 164 calls / 138 users, $feature_flag_response empty every time (client gets undefined).
- Zero
render_source: "flag" events exist — the cold path never resolves a network value.
- Server-side
get_all_feature_flags() returns a clean ~50/50 indie/free-plus split, so the flag itself works.
- Flag config:
active: true, rollout 100%, variants indie 50 / free-plus 50, ensure_experience_continuity: True.
Root cause
prepareVariant / resolveFlag in src/bootstrap/variant-loader.ts decide the variant client-side after load:
- Cold path waits up to 500 ms (
resolveFlag(ph, timeoutMs = 500)) for onFeatureFlags. With ensure_experience_continuity: True the flag requires a server round-trip that doesn't beat 500 ms against the self-hosted instance → flagValue: undefined → fallback to free-plus (render_source: fallback).
- Compounding it, the plugin (
woocommerce-pos/includes/Admin/Menu.php) calls posthog.identify(distinctId) at init before this bundle loads — which violates this bundle's deliberate flag-before-identify ordering (spec §5.1) and poisons anon-bucket exposure for an experience-continuity flag.
So first-time visitors wait ~500 ms and still get the wrong variant; only cached repeat visitors render a real assignment.
Fix (this repo)
The decision should not block on a browser-side network call. Preferred direction is consume a server-injected bootstrap rather than racing the flag fetch:
- When the plugin bootstraps
landing-variant into posthog.init (see companion issue), getFeatureFlag(FLAG_KEY) returns synchronously and the cold path resolves with render_source: flag instantly — no 500 ms wait, no flicker. Verify variant-loader.ts handles a bootstrapped value cleanly through the flag path (and that exposure accounting still fires once, in the right bucket).
- Re-evaluate the 500 ms cold-path timeout as a fallback only path. Raising it alone is not the fix (it trades a wrong variant for a slow one) — the goal is to not depend on the in-browser fetch for the decision at all.
- Confirm flag-before-identify ordering holds once the plugin stops identifying early.
Also (no deploy)
Turning off ensure_experience_continuity on the flag in PostHog removes the forced server round-trip and may let the current cold path succeed — useful to confirm the diagnosis independent of any bundle change.
Acceptance
- New
landing_variant_rendered events: ~50/50 split, render_source: flag.
$feature_flag_called for landing-variant has a non-empty response.
- First-time visitors render the assigned variant with no perceptible wait / no flash of default.
Summary
The
landing-variantA/B test is not splitting traffic — every real visitor renders thefree-plusfallback. The PostHog flag is configured correctly and resolves a clean ~50/50 split server-side; the failure is in client-side variant resolution (src/bootstrap/variant-loader.ts). This bundle owns the experiment; the server-side enabler lives in the plugin.Companion issue (plugin-side init/bootstrap fix): wcpos/woocommerce-pos#1188
Evidence (from production PostHog / ClickHouse on ph.wcpos.com)
landing_variant_rendered, all-time:fallbackcache$feature_flag_calledforlanding-variant: 164 calls / 138 users,$feature_flag_responseempty every time (client getsundefined).render_source: "flag"events exist — the cold path never resolves a network value.get_all_feature_flags()returns a clean ~50/50indie/free-plussplit, so the flag itself works.active: true, rollout 100%, variantsindie50 /free-plus50,ensure_experience_continuity: True.Root cause
prepareVariant/resolveFlaginsrc/bootstrap/variant-loader.tsdecide the variant client-side after load:resolveFlag(ph, timeoutMs = 500)) foronFeatureFlags. Withensure_experience_continuity: Truethe flag requires a server round-trip that doesn't beat 500 ms against the self-hosted instance →flagValue: undefined→ fallback tofree-plus(render_source: fallback).woocommerce-pos/includes/Admin/Menu.php) callsposthog.identify(distinctId)at init before this bundle loads — which violates this bundle's deliberate flag-before-identify ordering (spec §5.1) and poisons anon-bucket exposure for an experience-continuity flag.So first-time visitors wait ~500 ms and still get the wrong variant; only cached repeat visitors render a real assignment.
Fix (this repo)
The decision should not block on a browser-side network call. Preferred direction is consume a server-injected bootstrap rather than racing the flag fetch:
landing-variantintoposthog.init(see companion issue),getFeatureFlag(FLAG_KEY)returns synchronously and the cold path resolves withrender_source: flaginstantly — no 500 ms wait, no flicker. Verifyvariant-loader.tshandles a bootstrapped value cleanly through theflagpath (and that exposure accounting still fires once, in the right bucket).Also (no deploy)
Turning off
ensure_experience_continuityon the flag in PostHog removes the forced server round-trip and may let the current cold path succeed — useful to confirm the diagnosis independent of any bundle change.Acceptance
landing_variant_renderedevents: ~50/50 split,render_source: flag.$feature_flag_calledforlanding-varianthas a non-empty response.