Skip to content

fix: kill-switch watch survives the stale inline callback (addresses Codex on #30)#32

Merged
kilbot merged 3 commits into
mainfrom
fix/killswitch-watch-fresh
Jun 15, 2026
Merged

fix: kill-switch watch survives the stale inline callback (addresses Codex on #30)#32
kilbot merged 3 commits into
mainfrom
fix/killswitch-watch-fresh

Conversation

@kilbot

@kilbot kilbot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Addresses the P2 Codex flagged on #30 (which I'd already merged — apologies; not self-merging this one).

Bug: watchKillSwitch reused resolveFlag's unsubscribe-after-first-fire guard. On a cached repeat visit posthog-js fires onFeatureFlags inline with the stale persisted flags, so the watcher read the same value recordCachedExposure already saw and unsubscribed before the fresh network reload — meaning a kill-switch flipped since the last visit never cleared wcpos_landing_assignment. The exact scenario #30 set out to fix, it didn't.

Fix: drop the guard. Stay subscribed and clear the cache on any observed ON value (the fresh network reload, and the post-identify reload), then stop; bounded by an 8s timeout so the listener never lingers. The experiment-flag exposure read stays synchronous + pre-identify (unchanged). Regression pinned in tests/exposure-ordering.test.mjs (asserts no first-fire unsubscribe, bounded by timeout).

Please review before merge.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved kill-switch feature flag handling for cached renders, ensuring the listener stays active as needed, only clears cache when the ON state is observed, and uses a fixed timeout to avoid premature stopping.
  • Tests

    • Strengthened coverage to confirm the kill-switch watch does not unsubscribe after the first callback and that the subscription lifecycle is bounded by an 8-second timeout.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kilbot, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 45 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 3abe648a-35c8-4de4-8a81-02375368fc96

📥 Commits

Reviewing files that changed from the base of the PR and between 27fdbf1 and 846995c.

📒 Files selected for processing (1)
  • src/bootstrap/variant-loader.ts
📝 Walkthrough

Walkthrough

watchKillSwitch in variant-loader.ts is reworked to keep the PostHog onFeatureFlags subscription alive until the kill-switch flag is observed as ON, at which point it clears the cache and unsubscribes. A hard 8-second timeout is added as a fallback. The previous fire-once-then-unsubscribe behavior is removed. Tests add assertions confirming the new lifetime semantics.

Changes

Kill-switch subscription lifecycle

Layer / File(s) Summary
Persistent subscription with timeout
src/bootstrap/variant-loader.ts, tests/exposure-ordering.test.mjs
Removes the fired/first-callback unsubscribe guard; the onFeatureFlags callback now stays active, clears the cached assignment and calls stop() only when the kill-switch is ON, with a fixed 8s setTimeout as a hard upper bound. Tests assert the absence of the old first-fire unsub pattern and the presence of setTimeout(.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • wcpos/wp-admin-landing#27: Directly modifies the same PostHog kill-switch subscription logic in src/bootstrap/variant-loader.ts, making it a clear predecessor to this change in subscription lifecycle behavior.

Poem

🔪 The kill-switch once bailed on the very first call,
Now it lingers and watches, refusing to fall.
Eight seconds it waits — tick, tick, tick, tick —
For the flag to flip ON, then it stops real quick.
Persistent and patient, no premature end,
A watcher that watches until the very last bend! ⏱️

🚥 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 directly addresses the core fix: the kill-switch watch now survives the stale inline callback, which is the primary behavioral change described in the PR objectives.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/killswitch-watch-fresh

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

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
tests/exposure-ordering.test.mjs (1)

55-55: ⚡ Quick win

Could we assert the explicit 8-second timeout contract here?

Right now this only proves setTimeout exists; a wrong duration would still pass. Would you tighten this to assert 8000 as part of the watcher lifecycle guarantee?

As per coding guidelines, "Respect documented author intent and check for companion PRs"—the PR intent explicitly defines a fixed 8-second bound.

🤖 Prompt for 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.

In `@tests/exposure-ordering.test.mjs` at line 55, The assertion on the
assert.match call for the watch variable currently only verifies that setTimeout
exists but does not validate the specific 8-second timeout duration. Update the
regex pattern in the assert.match call to explicitly check for the 8000
millisecond timeout value within the setTimeout invocation, ensuring the test
verifies the documented 8-second timeout contract as part of the watcher
lifecycle guarantee.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@tests/exposure-ordering.test.mjs`:
- Line 55: The assertion on the assert.match call for the watch variable
currently only verifies that setTimeout exists but does not validate the
specific 8-second timeout duration. Update the regex pattern in the assert.match
call to explicitly check for the 8000 millisecond timeout value within the
setTimeout invocation, ensuring the test verifies the documented 8-second
timeout contract as part of the watcher lifecycle guarantee.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b118081-db26-45a1-9e5a-d0e890208753

📥 Commits

Reviewing files that changed from the base of the PR and between 253abc4 and 7824566.

📒 Files selected for processing (3)
  • assets/js/welcome.js
  • src/bootstrap/variant-loader.ts
  • tests/exposure-ordering.test.mjs

@kilbot

kilbot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Fix triage

# Source File Class Category Decision Outcome
1 Merge state assets/js/welcome.js Mechanical Blocker Fix main moved (#31 merged); the built welcome.js conflicts because both #31 (i18n) and this PR (watchKillSwitch) rebuilt it. Source files don't overlap (i18n.ts vs variant-loader.ts) — rebasing onto main and rebuilding produces a welcome.js with both changes.
2 CodeRabbit exposure-ordering.test.mjs:55 Mechanical Nitpick Fix Assertion only checks setTimeout exists, not the documented 8s bound — tighten to assert 8000.

Working through both now.

kilbot added 2 commits June 15, 2026 13:17
)

watchKillSwitch copied resolveFlag's unsubscribe-after-first-fire guard, but on
a cached repeat visit posthog-js fires onFeatureFlags inline with the STALE
persisted flags — so it read the same value recordCachedExposure already saw and
unsubscribed before the fresh network reload, meaning a kill-switch flipped since
the last visit never cleared the assignment cache (the exact case it was meant to
handle). Removed the guard: stay subscribed, clear the cache on any observed ON
value (fresh reload / post-identify reload), bounded by an 8s timeout so the
listener never lingers. Pins the regression in tests/exposure-ordering.test.mjs.
@kilbot kilbot force-pushed the fix/killswitch-watch-fresh branch from 7824566 to 27fdbf1 Compare June 15, 2026 11:17
@kilbot

kilbot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

✅ Both items resolved:

# Source Decision Outcome
1 Merge conflict (welcome.js) Fixed Rebased onto current main (#31); rebuilt welcome.js so it carries both the i18n loadPath (#31) and the watchKillSwitch fix (this PR). Source files never overlapped.
2 CodeRabbit — exposure-ordering.test.mjs:55 Fixed 27fdbf1 — assertion now checks setTimeout(…, 8000) explicitly, pinning the documented 8s bound rather than just the presence of setTimeout.

@coderabbitai the 8s-timeout nitpick is addressed in 27fdbf1. CI green, no open threads. Ready for review/merge — not self-merging.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

@kilbot Thanks for the clean resolution on both items — the explicit 8000 pin in the timeout assertion is a nice improvement in test clarity, and good to hear the welcome.js rebuild carried both changes without source overlap. Triggering a fresh review now.

🐇 ✅

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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 `@src/bootstrap/variant-loader.ts`:
- Around line 106-113: The docblock at lines 95-96 describing the watcher
lifecycle is outdated and contradicts the new implementation shown at lines
106-113. Update the earlier docblock to reflect the persistent-subscription
design intent: explain that the watcher intentionally stays subscribed without
the synchronous-fire/unsubscribe-after-first guard, that it clears on observed
ON values from fresh network reloads, and that it is bounded by a timeout to
prevent the listener from lingering indefinitely. This aligns the documentation
with the actual persistent-subscription pattern implemented in the code.
🪄 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: a1b058c0-fcb1-480c-9f0b-b61c13e7ec14

📥 Commits

Reviewing files that changed from the base of the PR and between 7824566 and 27fdbf1.

📒 Files selected for processing (3)
  • assets/js/welcome.js
  • src/bootstrap/variant-loader.ts
  • tests/exposure-ordering.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/exposure-ordering.test.mjs

Comment thread src/bootstrap/variant-loader.ts
…ign (CodeRabbit #32)

The docblock at lines 95-96 still described the old synchronous-fire guard
that unsubscribes after the first reload. The implementation deliberately
dropped that guard so the watch survives the stale inline callback and picks
up the fresh network reload. Update the wording to match.
@kilbot kilbot merged commit 2cfafc5 into main Jun 15, 2026
8 checks passed
@kilbot kilbot deleted the fix/killswitch-watch-fresh branch June 15, 2026 11:40
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