Skip to content

fix(analytics): bootstrap landing-variant server-side & stop early identify (#1188)#1189

Open
kilbot wants to merge 3 commits into
mainfrom
fix/landing-variant-bootstrap
Open

fix(analytics): bootstrap landing-variant server-side & stop early identify (#1188)#1189
kilbot wants to merge 3 commits into
mainfrom
fix/landing-variant-bootstrap

Conversation

@kilbot

@kilbot kilbot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the landing-variant A/B test on the wp-admin welcome page, which was stuck on the free-plus fallback for every visitor (issue #1188).

  • New Services\Feature_Flags — resolves landing-variant server-side, locally, using PostHog's standard consistent-hash algorithm (the same one the server SDKs' get_all_feature_flags() uses). No network call, no secret.
  • Landing_Profile::get_functional_data() now injects bootstrap_flags into the always-present, consent-independent functional data, keyed by the per-site anon_id.
  • Menu welcome page — removed the early posthog.init() + identify() inline script and the redundant DOM CTA tracker. The wp-admin-landing bundle now solely owns PostHog init and the flag-before-identify ordering.

Root cause (two compounding bugs)

  1. The self-hosted PostHog flags endpoint is broken for the public token. Verified against ph.wcpos.com: POST /capture/200 (token valid), but POST /flags/?v=2&config=true401 "API key invalid" and POST /decide/?v=3403 CSRF. So posthog-js never resolves the flag over the network — hence zero render_source:flag events across all 337 visitors (a 500 ms timeout would have let some through; zero means systematic endpoint failure). This is an infra bug that still needs a separate fix — see "Follow-ups". The team's get_all_feature_flags() "works" only because it's the Python SDK doing local evaluation with a personal API key, never touching /flags.
  2. The plugin identified too early. The welcome page called posthog.init + identify(user_uuid) in an inline script that runs before welcome.js. Both PostHog instances share the ph_<token>_posthog localStorage key, so this flipped the bundle's hasPersistedIdentity() gate and poisoned the anon-bucket exposure (spec §5.1 flag-before-identify).

The issue's suggested fix (bootstrap inside the plugin's posthog.init, server-side /decide) is not viable: that init is consent-gated so it never runs for the anonymous majority, and /decide is the broken endpoint. This PR instead delivers the variant through the always-present landing data and removes the plugin's PostHog involvement on that page entirely.

Design decisions (preserve through rebases)

  • Local consistent-hash instead of a /decide network call — deliberate, because the self-hosted /flags//decide endpoints reject the public token (see root cause). Local hashing replicates PostHog's exact algorithm (verified against a reference implementation — all 6 test vectors match), so assignment agrees with get_all_feature_flags() and is independent of the broken endpoint.
  • Removed the welcome-page posthog.init/identify and landing_tracking_inline_script() — not dead-code cleanup. The bundle owns PostHog; the plugin's early init poisoned shared localStorage and the DOM CTA tracker double-fired upgrade_cta_clicked against the bundle's own React tracking. Menu::get_posthog_inline_script() is kept — still used by the Settings and wc-Analytics pages.
  • bootstrap_flags is consent-independent — it's anonymous (random per-site UUID, no PII) and the experiment runs pre-consent by design, so it ships in the functional tier, not the consented tier.
  • @since 1.9.7 on the new filter assumes the next patch release off 1.9.6 — adjust if the release is numbered differently.

Companion PRs / cross-repo

Test plan

  • CI runs the PHPUnit suite (see "Local validation note"). New tests: Test_Feature_Flags, Test_Menu_Landing_Bootstrap, and bootstrap_flags cases in Test_Landing_Profile.
  • Load the WCPOS welcome screen → view source: window.wcpos.landing contains bootstrap_flags: { "landing-variant": "indie" | "free-plus" }, and there is no posthog.init / posthog.identify inline script.
  • Confirm the assigned variant is stable across reloads for the same site and ~50/50 across sites.
  • Settings page and wc-Analytics page still emit their PostHog init snippet (regression — get_posthog_inline_script() unchanged).
  • After wp-admin-landing#39 ships: new landing_variant_rendered events show render_source: flag with a ~50/50 split and a non-empty $feature_flag_response.

Local validation note

  • composer run lint (PHPCS), composer run phpstan (level 5), and php -l all pass on every changed file.
  • /review (correctness) and /critic (architecture) gates: both "ship it", no blocking findings.
  • PHPUnit could not be run locally: wp-env could not start because Docker cannot reach the image registry in this environment (docker pull of even alpine stalls). Per project policy this is not worked around with host PHPUnit — CI is the sanctioned runner. The new tests are written to the repo's conventions and the hash logic was independently verified against a reference implementation.

Follow-ups (out of scope here)

  • Infra: fix the ph.wcpos.com /flags + /decide endpoints so the public token authenticates — required for client-side flag reloads, experience-continuity, and any future experiment flag.
  • Drift tripwire: the 50/50 two-arm config is hardcoded (it must be, given /flags is down). Consider a detectable server-side exposure property so a future PostHog-side config change can't silently skew the split.

Refs #1188

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added deterministic landing-page variant feature flags and included the selected variant in the landing page bootstrap data for consistent A/B testing.
  • Bug Fixes

    • Updated landing page script injection to prevent duplicate analytics initialization and remove redundant CTA click tracking from the landing inline flow.
  • Tests

    • Expanded unit tests to verify landing bootstrap wiring, correct variant assignment, absence of analytics init, and no duplicate CTA tracking.
    • Added full test coverage for feature-flag resolution and overrides.

…ntify

The landing-variant A/B test was stuck on the free-plus fallback for every
visitor. Two root causes:

1. The self-hosted PostHog /flags endpoint rejects the public project token
   (401) while /capture works, so posthog-js never resolves the flag over the
   network (zero render_source:flag events). The infra needs a separate fix.
2. The plugin's early posthog.init + identify() on the welcome page wrote the
   bundle's shared `ph_<token>_posthog` localStorage identity first, flipping
   the bundle's hasPersistedIdentity() gate and breaking the deliberate
   flag-before-identify exposure ordering.

Fix (plugin side):
- Add Services\Feature_Flags: resolve landing-variant via PostHog's standard
  consistent-hash locally (no network, no secret), so assignment matches the
  server SDK's get_all_feature_flags() and works regardless of the broken
  /flags endpoint.
- Landing_Profile: inject `bootstrap_flags` into the always-present functional
  data (anon, consent-independent) so the bundle can seed posthog.init
  bootstrap.featureFlags at first paint.
- Menu: stop initialising PostHog / identifying on the welcome page and remove
  the redundant DOM CTA tracker (it double-fired upgrade_cta_clicked against the
  bundle's own React tracking). The bundle now solely owns PostHog init +
  identify ordering.

Companion bundle change required: wcpos/wp-admin-landing#39 (read bootstrap_flags
into bootstrap.featureFlags). This PR is necessary but the experiment only goes
green once both ship.

Refs #1188
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: wcpos/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6a5c908-7161-4aec-bc03-95b486e11398

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1e642 and fb8a325.

📒 Files selected for processing (2)
  • tests/includes/Admin/Test_Menu_Landing_Bootstrap.php
  • tests/includes/Services/Test_Feature_Flags.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/includes/Services/Test_Feature_Flags.php
  • tests/includes/Admin/Test_Menu_Landing_Bootstrap.php

📝 Walkthrough

Walkthrough

A new Feature_Flags service performs deterministic server-side landing-variant assignment using a PostHog-compatible consistent hash. Landing_Profile::get_functional_data() now injects the resolved bootstrap_flags into the landing payload. Menu.php removes its PostHog init, identify, and CTA tracker injections, leaving those entirely to the landing bundle.

Changes

Landing Variant Bootstrap & PostHog Init Cleanup

Layer / File(s) Summary
Feature_Flags service: constants, variant resolution, consistent hash
includes/Services/Feature_Flags.php
Introduces Feature_Flags with LANDING_FLAG_KEY, LONG_SCALE, and LANDING_VARIANTS constants. Implements get_landing_bootstrap_flags(), get_landing_variant(), match_variant(), and hash() for deterministic PostHog-compatible variant assignment, with a woocommerce_pos_landing_bootstrap_flags WordPress filter escape hatch.
Landing_Profile: inject bootstrap_flags into functional data
includes/Services/Landing_Profile.php
get_functional_data() precomputes $anon_id and extends the returned payload with a bootstrap_flags entry from Feature_Flags::get_landing_bootstrap_flags().
Menu.php: remove PostHog init, identify, and CTA tracker injection
includes/Admin/Menu.php
Strips PostHog bootstrap inline script and landing_tracking_inline_script() injection from enqueue_landing_scripts_and_styles(). Deletes the landing_tracking_inline_script() method entirely. Only landing_inline_script() (functional data) is now emitted.
Tests: Feature_Flags, Landing_Profile bootstrap flags, Menu enqueue assertions
tests/includes/Services/Test_Feature_Flags.php, tests/includes/Services/Test_Landing_Profile.php, tests/includes/Admin/Test_Menu_Landing_Bootstrap.php
Adds full Test_Feature_Flags suite (determinism, PostHog reference vectors, variant bounds, 50/50 split distribution, null/empty guard, filter override, bootstrap structure). Extends Test_Landing_Profile with two methods verifying bootstrap_flags.landing-variant presence and anon_id consistency. Adds Test_Menu_Landing_Bootstrap with fixture setup/teardown and three assertion tests confirming functional data injection, absence of posthog.init/posthog.identify, and absence of duplicate upgrade_cta_clicked.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • wcpos/woocommerce-pos#1146: Introduced anon_id into Landing_Profile::get_functional_data() and bumped schema_version; this PR builds directly on that foundation by using the same anon_id to compute bootstrap_flags via Feature_Flags.
  • wcpos/woocommerce-pos#1166: Both PRs modify includes/Admin/Menu.php::enqueue_landing_scripts_and_styles() to eliminate duplicate "upgrade CTA" analytics signals on admin renders.

Poem

A hash of your ID, a float in disguise,
landing-variant assigned before PostHog flies.
No double-init, no identity strife—
The bundle takes charge of its PostHog life.
🎲 indie or free-plus, the server decides,
while PHP quietly cleans up its slides.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: bootstrapping landing-variant server-side and stopping early PostHog identify calls to fix the A/B test stuck on free-plus fallback.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/landing-variant-bootstrap

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

E2E UI Test Results

24 tests   24 ✅  1m 7s ⏱️
 9 suites   0 💤
 1 files     0 ❌

Results for commit fb8a325.

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/includes/Admin/Test_Menu_Landing_Bootstrap.php`:
- Around line 26-27: The setUp() method modifies the general settings
(tracking_consent = allowed), but tearDown() doesn't restore the original
values, causing test isolation issues for subsequent test suites. Store the
original state of the general settings before modification in setUp(), then
restore those original values in tearDown() to ensure proper cleanup and
maintain test isolation across the test suite.

In `@tests/includes/Services/Test_Feature_Flags.php`:
- Around line 129-138: The filter cleanup using remove_filter for
woocommerce_pos_landing_bootstrap_flags in the
test_bootstrap_flags_filter_can_override_variant method is not exception-safe.
If get_landing_bootstrap_flags throws an exception, the remove_filter call will
not execute, causing the filter to leak into subsequent tests. Wrap the call to
get_landing_bootstrap_flags and the assertions in a try-finally block, moving
the remove_filter call into the finally block to ensure the filter is always
cleaned up regardless of whether an exception occurs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: wcpos/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 49f79754-e3a5-490f-ace2-3830b80990dc

📥 Commits

Reviewing files that changed from the base of the PR and between 6af654b and 9e1e642.

📒 Files selected for processing (6)
  • includes/Admin/Menu.php
  • includes/Services/Feature_Flags.php
  • includes/Services/Landing_Profile.php
  • tests/includes/Admin/Test_Menu_Landing_Bootstrap.php
  • tests/includes/Services/Test_Feature_Flags.php
  • tests/includes/Services/Test_Landing_Profile.php

Comment thread tests/includes/Admin/Test_Menu_Landing_Bootstrap.php
Comment thread tests/includes/Services/Test_Feature_Flags.php
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

PHPUnit Test Results

     7 files  ± 0     749 suites  +14   15m 6s ⏱️ +3s
 1 490 tests +13   1 483 ✅ +13   7 💤 ±0  0 ❌ ±0 
10 430 runs  +91  10 381 ✅ +91  49 💤 ±0  0 ❌ ±0 

Results for commit fb8a325. ± Comparison against base commit 6af654b.

♻️ This comment has been updated with latest results.

@wcpos-bot

wcpos-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Fix round triage before changes:

# Source File Class Category Decision Outcome
1 CodeRabbit Test_Menu_Landing_Bootstrap.php Mechanical Test isolation Fix Store original general settings before enabling tracking consent and restore them in tearDown().
2 CodeRabbit Test_Feature_Flags.php Mechanical Test isolation Fix Move woocommerce_pos_landing_bootstrap_flags cleanup into finally so the filter cannot leak after an exception.

CI scope: current PR checks are passing; no failing CI item to fix in this round.

@wcpos-bot

wcpos-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review-fix triage after push:

Thread File Issue Decision Commit
CodeRabbit Test_Menu_Landing_Bootstrap.php setUp() persisted tracking_consent = allowed without restoring the original general settings. Fixed and thread resolved. 36e7306
CodeRabbit Test_Feature_Flags.php Filter cleanup could leak woocommerce_pos_landing_bootstrap_flags if the call under test errored before remove_filter(). Fixed and thread resolved. fb8a325

Excluded threads: none.

Thread Reason for skipping
None All unresolved actionable review threads from the fresh GraphQL inventory were fixed and resolved.

Validation:

  • composer run lint-report passed.
  • composer run phpstan passed.
  • pnpm run test:unit:js passed.
  • pnpm run test:unit:php could not run locally because wp-env was not initialized, and pnpm exec wp-env start failed with spawn docker ENOENT (Docker is not installed in this environment). Per repo policy, I did not fall back to host PHPUnit.

Post-push unresolved inline review threads: 0.
CI scope: GitHub checks restarted for fb8a3252 and are currently in progress; no failing check has been observed in this fix round.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant