Repository navigation
feat: add an onFeatureFlags listener you can unsubscribe - #878
turnipdabeets merged 8 commits into
Conversation
There was a problem hiding this comment.
PostHog Review · Standard · reviewed b2fbd8b: 2 Should fix.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
b2fbd8b to
b328ebe
Compare
6d389ab to
4469ad4
Compare
46c6dd1 to
777d2c4
Compare
4469ad4 to
3e7794c
Compare
777d2c4 to
e293ef7
Compare
e9eb0db to
46fca5a
Compare
8976a71 to
2dfb42a
Compare
3f4acd8 to
5dcba77
Compare
2dfb42a to
1aa1f7e
Compare
dustinbyrne
left a comment
There was a problem hiding this comment.
After a failed first real load, onFeatureFlags can emit and replay an empty map even when persisted flags exist and the existing completion returns them. This concerns the failed-load payload, not the accepted cache-only readiness policy.
AI-assisted review.
8f4a729 to
57533d7
Compare
dustinbyrne
left a comment
There was a problem hiding this comment.
Closing the SDK from the bootstrap config callback still lets setup deliver and replay observer flags while disabled. A public-entry regression reproduces this; the earlier failed-load payload fix remains correct. The Android listener's delivery-thread contract also needs reconciliation with current sdk-specs wording.
AI-assisted review.
Registers a PostHogFeatureFlagsCallback that runs after every feature flags load, replays the current flags to late subscribers, and returns a PostHogFeatureFlagsSubscription, matching posthog-ios onFeatureFlags(_:).
A load that started before reset() or close() could finish afterwards, reach listeners, and be replayed to late subscribers, including the previous user's flags after reset(). Each load now captures a generation that reset() and close() bump, and listener updates from an older generation are dropped.
A failed first load built the listener snapshot before the flag cache was
hydrated, so listeners and late replays got {} while the completion and
getters returned the persisted flags. Hydrate before either callback runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… closes the SDK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69c79e8 to
9f4d709
Compare
Listeners usually update UI. On the remote config thread, a view update hits CalledFromWrongThreadException and LiveData.setValue throws, which the SDK catches, so the update is lost. This matches the on-feature-flags spec and posthog-ios. The config and reload callbacks keep running in the background. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dustinbyrne
left a comment
There was a problem hiding this comment.
The bootstrap-close fix and Android main-thread delivery address the previous findings. One setup-close path remains: a public integration can close the client during installation, after which startup flags are still delivered and replayed while disabled. The new singleton/factory regression reproduces it. Optional threading-doc clarification is inline.
AI-assisted review.
…the SDK Capture the flags generation right after setup enables the SDK, before any user callout (integration install, session hooks, bootstrap config callback) that could close() it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dustinbyrne
left a comment
There was a problem hiding this comment.
Approved with an optional note on bootstrap observer ordering when a custom integration starts a reload during installation.
AI-assisted review.
An integration that calls reloadFeatureFlags() during install queued its /flags request on the serial remote config executor ahead of setup's bootstrap notification, so onFeatureFlags listeners waited on the network even though bootstrapped flags were ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Note
Stacked on #877, which is stacked on the 4.0 integration PR #871. Merges into
chore/v4-flags-loaded-result.💡 Motivation and Context
Adds
onFeatureFlags(), matching posthog-ios 4.0onFeatureFlags(_:)and posthog-jsonFeatureFlags. Today the only way to hear about flag loads after setup is the singlePostHogConfig.onFeatureFlagscallback, which a screen or library can't add to or remove.PostHogFeatureFlagsLoadedas the config callback (chore!: pass the load result to the feature flags callback #877).PostHog.onFeatureFlags. Listeners stay registered acrossclose()andsetup().For reviewers:
on-feature-flagsspec ("On SDKs with a main/UI thread, delivery happens on that thread") and matches posthog-ios (feat: add public onFeatureFlags listener with flags, variants and error state posthog-ios#897). The core gets an internalPostHogConfig.featureFlagsListenerExecutorthatPostHogAndroidsets to post to the main looper. On the plain JVM, it's unset and listeners run on the remote config thread.config.onFeatureFlagsand thereloadFeatureFlagscallback still run on the remote config thread. iOS splits it the same way: its reload callback stays off main (chore!: mark off-main public callbacks @Sendable for v4 posthog-ios#914). Moving the existing callbacks to main would be its own breaking change, so it's out of scope here.PostHogInterfacegains an abstract method, which breaks third-party implementations of the interface. That's acceptable in 4.0.reset()orclose()but finishes after is dropped for listeners and not replayed, so listeners never get the previous user's flags afterreset(). Each load captures a generation thatreset()andclose()bump.💚 How did you test it?
./gradlew spotlessCheck apiCheck :posthog:test :posthog-server:test :posthog-android:testDebugUnitTestpass.PostHogFeatureFlagsTestcovers delivery on each load untilunsubscribe(), replay to a late listener, listeners survivingclose()andsetup(), and no delivery from a reload while disabled. A new test checks that a load finishing afterclose()reaches no listener and isn't replayed; it fails without the fix.PostHogBootstrapTestcovers replay of bootstrapped flags.PostHogFeatureFlagsTestchecks that listeners and late-listener replay run on the listener executor. It also checks that an update queued there is delivered once even when its replay is queued alongside it, and that unsubscribing drops it.PostHogAndroidTestchecks that Android posts listeners to the main looper. Each fails without its fix.setTexton an attached view loggedCalledFromWrongThreadException, andLiveData.setValuethrew (the SDK caught it, so the update was lost). After it, every delivery ran on main, both worked, and the config and reload callbacks stayed onPostHogRemoteConfigThread.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Claude Code (Claude Opus 5.5) implemented the listener for flags API parity with iOS 4.0 and Flutter 6, after a cross-SDK review of the flags APIs.
🤖 Generated with Claude Code