Skip to content

feat(analytics): consume server-bootstrapped landing-variant flag (#39)#40

Open
kilbot wants to merge 1 commit into
mainfrom
feat/bootstrap-flags
Open

feat(analytics): consume server-bootstrapped landing-variant flag (#39)#40
kilbot wants to merge 1 commit into
mainfrom
feat/bootstrap-flags

Conversation

@kilbot

@kilbot kilbot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Consumes the server-resolved landing-variant value so the A/B test renders the assigned variant at first paint instead of racing a (broken) in-browser flag fetch. Companion to wcpos/woocommerce-pos#1189.

  • shared/landing-data.ts — add optional bootstrap_flags?: Record<string, string | boolean> to the WCPOSLanding contract, with an isFlagMap guard, validated/included in getLandingData().
  • shared/analytics.ts — seed posthog.init's bootstrap.featureFlags from data.bootstrap_flags, so getFeatureFlag('landing-variant') returns synchronously. Skipped in preview (the GitHub Pages harness seeds its own assignment cache and must not resolve production flags).
  • Rebuilt assets/js/welcome.js; tests for the contract, the guard, and the bootstrap wiring.

Why the cold path never worked

The in-browser fetch wasn't merely slow — the self-hosted PostHog flags endpoint rejects the public token: POST /capture/ → 200, but POST /flags/?v=2&config=true401 and POST /decide/?v=3403 CSRF. So posthog-js got undefined every time → zero render_source:flag events across all 337 visitors. Raising the 500 ms timeout would not have helped. Bootstrapping the value (resolved server-side in the plugin via PostHog's consistent-hash) makes the decision independent of that endpoint — the resolveFlag cold path now resolves instantly via the bootstrapped value and writes the cache, with no flicker.

Design decisions (preserve through rebases)

  • featureFlags is skipped when preview is true — preview seeds its own assignment cache; resolving production flags there would pollute baselines (consistent with advanced_disable_flags: preview).
  • bootstrap_flags is optional and guarded — older plugins (<1.9.7) don't inject it; getLandingData simply omits it and the existing cold path resolves the variant (now that the plugin also stops identifying early).
  • Rebuilt assets/js/welcome.js is committedassets/ is tracked and check:assets requires it to match a fresh build. Only the bootstrap entry changed; variant chunks rebuilt byte-identical.

Companion PRs / cross-repo

  • fix(analytics): bootstrap landing-variant server-side & stop early identify (#1188) woocommerce-pos#1189 — injects window.wcpos.landing.bootstrap_flags and stops the plugin's early posthog.init/identify() on the welcome page (which was poisoning this bundle's shared localStorage identity). Both must ship for the experiment to go green.
  • Infra (no PR yet): the /flags + /decide endpoints on ph.wcpos.com need fixing so client-side flag reloads, experience-continuity, and future experiment flags work. The bootstrap sidesteps this for landing-variant but does not fix it.

Test plan

  • npm run ci passes (typecheck, 41 tests, lint:*, check:assets) — green locally.
  • With the plugin injecting bootstrap_flags, load the welcome screen: the assigned variant renders with no flash of default, and a landing_variant_rendered event fires with render_source: flag.
  • Confirm $feature_flag_called for landing-variant carries a non-empty $feature_flag_response.
  • Preview harness (GitHub Pages) still renders its chosen variant from the seeded cache (bootstrap flags must NOT override it).
  • Older plugin without bootstrap_flags: page still loads, no errors (cold path resolves or falls back).

Closes #39

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Server-resolved feature flags can now be pre-loaded during analytics initialization (unavailable in preview mode).
    • Landing data configuration now supports optional bootstrap feature flags.
  • Tests

    • Added test coverage for feature flag initialization and landing data validation.

Read window.wcpos.landing.bootstrap_flags (injected by plugin >=1.9.7) into
posthog.init's bootstrap.featureFlags so getFeatureFlag('landing-variant')
resolves synchronously at first paint -- no /flags network fetch, no 500ms
cold-path timeout, no flicker.

The self-hosted PostHog /flags endpoint rejects the public token (401), so the
in-browser flag fetch never resolved (zero render_source:flag events across all
visitors); the server-side bootstrap sidesteps the broken endpoint entirely.

- landing-data.ts: add optional `bootstrap_flags` (Record<string,string|boolean>)
  to the WCPOSLanding contract + `isFlagMap` validation in getLandingData.
- analytics.ts: seed bootstrap.featureFlags from data.bootstrap_flags. Skipped in
  preview, which seeds its own assignment cache and must not resolve prod flags.
- Rebuilt assets/js/welcome.js.
- Tests for the contract, the validation guard, and the bootstrap wiring.

Companion plugin change that injects the data: wcpos/woocommerce-pos#1189.

Closes #39
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

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: 77a519e7-ae96-497d-a896-d7312a7ef301

📥 Commits

Reviewing files that changed from the base of the PR and between 4f176f6 and f6ee219.

📒 Files selected for processing (5)
  • assets/js/welcome.js
  • src/shared/analytics.ts
  • src/shared/landing-data.ts
  • tests/analytics-config.test.mjs
  • tests/landing-data.test.mjs

📝 Walkthrough

Walkthrough

Adds an optional bootstrap_flags field to the WCPOSLanding contract, validated by a new isFlagMap type guard in getLandingData. initAnalytics now injects these server-resolved flags into PostHog's bootstrap.featureFlags during initialization, gated on non-preview mode to protect the preview harness assignment cache.

Changes

Server-resolved feature flags bootstrap

Layer / File(s) Summary
WCPOSLanding contract, isFlagMap guard, and getLandingData extraction
src/shared/landing-data.ts, tests/landing-data.test.mjs
WCPOSLanding gains an optional bootstrap_flags?: Record<string, string | boolean> field. A new isFlagMap type guard rejects arrays and non-string/boolean values. getLandingData conditionally assigns validated.bootstrap_flags when isFlagMap passes. Tests cover the type shape, guard logic, and array rejection.
PostHog featureFlags seeding in initAnalytics
src/shared/analytics.ts, tests/analytics-config.test.mjs
initAnalytics conditionally sets bootstrap.featureFlags = data.bootstrap_flags when !preview && data?.bootstrap_flags. Preview mode skips this to avoid overwriting the harness assignment cache. Tests assert both the seeding presence and the preview gate condition.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

  • wcpos/wp-admin-landing#23: Also modifies initAnalytics() preview-mode behavior in src/shared/analytics.ts, gating PostHog side effects on the preview flag.
  • wcpos/wp-admin-landing#27: Also modifies PostHog initialization options in src/shared/analytics.ts, touching feature-flag-related bootstrap config.
  • wcpos/wp-admin-landing#30: Also adds preview-mode isolation in initAnalytics to prevent production flag resolution from leaking into the preview harness.

Poem

🏴‍☠️ The flags arrive before the page can blink,
Resolved by the server before flags can sink.
No 500ms wait, no A/B test split fate,
bootstrap_flags rides in — never late!
Preview? Stay calm, your cache is just fine. 🎌

🚥 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 describes the main change: implementing server-side consumption of the landing-variant A/B test flag via bootstrap injection into PostHog.
Docstring Coverage ✅ Passed Docstring coverage is 80.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 feat/bootstrap-flags

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6ee21937a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/shared/analytics.ts
// 500 ms cold-path timeout, no flicker. Absent on older plugins
// (the cold path then resolves it). Skipped in preview, which seeds
// its own assignment cache and must not resolve production flags.
...(!preview && data?.bootstrap_flags ? { featureFlags: data.bootstrap_flags } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bootstrap flags outside the new-identity branch

When a browser already has ph_${POSTHOG_KEY}_posthog but no valid wcpos_landing_assignment (for example, anyone who loaded the previous broken /flags cold path, or after the assignment cache expires), this spread is never reached because it is nested under anonId && !hasPersistedIdentity(). prepareVariant() then waits on the still-broken flag endpoint, gets no bootstrapped landing-variant, and renders fallback again, so the server-resolved assignment does not take effect for returning users. Keep the distinctID bootstrapping gated, but pass bootstrap.featureFlags whenever bootstrap_flags is present except in preview.

Useful? React with 👍 / 👎.

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.

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

1 participant