Skip to content

landing-variant A/B test not splitting — all visitors get free-plus fallback #39

Description

@kilbot

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:

  1. 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).
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions