Repository navigation
feat: add an onFeatureFlags listener you can unsubscribe #670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
turnipdabeets
wants to merge
9
commits into
chore/v6-adopt-posthog-ios-4
Choose a base branch
from
feat/v6-on-feature-flags-listener
base: chore/v6-adopt-posthog-ios-4
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+889
−24
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4220eb0
feat: add an onFeatureFlags listener you can unsubscribe
turnipdabeets 0677b43
fix: isolate failing flag listeners, skip web registration without on…
turnipdabeets b130bcf
fix: deliver flag loads to onFeatureFlags listeners before the config…
turnipdabeets 7aaa9b8
fix(web): stop flag registrations left by close() or the posthog-js l…
turnipdabeets 1431994
fix: drop native flag loads sent before a reset or close reached native
turnipdabeets b71f8d6
fix: start receiving native flag loads when the first listener is added
turnipdabeets 4d81d9e
fix: deliver Android reloadFeatureFlags results to onFeatureFlags lis…
turnipdabeets 53e1279
Merge chore/v6-adopt-posthog-ios-4 into feat/v6-on-feature-flags-list…
turnipdabeets 1538fa6
fix(web): clear replayed flags when a queued reset reaches posthog-js
turnipdabeets File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "posthog_flutter": minor | ||
| --- | ||
|
|
||
| - Add `Posthog().onFeatureFlags()` to get notified after every feature flags load, returning a `PostHogFeatureFlagsSubscription` you can `unsubscribe()` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import 'dart:async'; | ||
|
|
||
| import 'package:meta/meta.dart'; | ||
|
|
||
| import 'feature_flags_loaded.dart'; | ||
| import 'posthog_flutter_platform_interface.dart'; | ||
| import 'util/logging.dart'; | ||
|
|
||
| /// Delivers each feature flags load to the listeners added with | ||
| /// [Posthog.onFeatureFlags]. | ||
| @internal | ||
| class FeatureFlagsListeners { | ||
| FeatureFlagsListeners._(); | ||
|
|
||
| static final instance = FeatureFlagsListeners._(); | ||
|
|
||
| final _listeners = <Object, OnFeatureFlagsCallback>{}; | ||
| PostHogFeatureFlagsLoaded? _last; | ||
| int _generation = 0; | ||
|
|
||
| /// Bumped by [clear]. The native SDKs tag each load with the generation | ||
| /// they last got, so a load sent before a `reset()` or `close()` reached | ||
| /// them can be told apart. | ||
| int get generation => _generation; | ||
|
|
||
| PostHogFeatureFlagsSubscription add(OnFeatureFlagsCallback listener) { | ||
| final token = Object(); | ||
| _listeners[token] = listener; | ||
| final current = _last; | ||
| if (current != null) { | ||
| // Skipped if a newer load already reached the listener. | ||
| scheduleMicrotask(() { | ||
| if (_listeners.containsKey(token) && identical(_last, current)) { | ||
| _notify(listener, current); | ||
| } | ||
| }); | ||
| } | ||
| return PostHogFeatureFlagsSubscription(() => _listeners.remove(token)); | ||
| } | ||
|
|
||
| void deliver(PostHogFeatureFlagsLoaded loaded) { | ||
| _last = loaded; | ||
| for (final entry in _listeners.entries.toList()) { | ||
| // An earlier listener may have unsubscribed this one. | ||
| if (_listeners.containsKey(entry.key)) _notify(entry.value, loaded); | ||
| } | ||
| } | ||
|
|
||
| // A throwing listener must not stop the others from getting the load. | ||
| void _notify( | ||
| OnFeatureFlagsCallback listener, PostHogFeatureFlagsLoaded loaded) { | ||
| try { | ||
| listener(loaded); | ||
| } catch (e) { | ||
| printIfDebug('onFeatureFlags listener threw: $e'); | ||
| } | ||
| } | ||
|
|
||
| /// Forgets the last load, so new listeners don't get the previous user's | ||
| /// flags after `reset()` or `close()`. | ||
| void clear() { | ||
| _last = null; | ||
| _generation++; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Clear web replay when the queued reset reaches JavaScript
IMPORTANT · REQUIRED
Location:
posthog_flutter/lib/posthog_flutter_web.dart:165, RIGHT.Change relationship: Introduced replay-cache defect exposed by the inherited async
beforeSendsequencing.When a flags response arrives while reset waits behind an asynchronous
beforeSend, a listener added afterawait Posthog().reset()completes receives the previous user's flags. The public getter already returnsnull, so the new replay cache disagrees with the reset SDK state.A supported caller can reach this when recording a logout event:
The inherited 5.58.2 changelog explicitly supports reset after an unawaited capture by preserving capture→reset order. The required conditions are an async
beforeSendstill holding that capture and an old-user flags request completing during the wait:lib/src/posthog.dart:393.CallOrdercorrectly queues JavaScript reset behind the capture (posthog_flutter_web.dart:73–74,406–407).This is a completed-reset replay failure, not a claim that active callbacks during the pending reset are themselves forbidden. It can show stale flag-driven UI after logout until a fresh response replaces the snapshot. No getter corruption, authorization bypass or backend disclosure was demonstrated.
The new API promises replay of current values (
posthog.dart:517–529), and its registry explicitly clears replay to avoid previous-user flags (feature_flags_listeners.dart:59–64). The reset/cache contracts atPostHog/sdk-specs@b1bb2dee5941f418adb270ea06d79f7d28bb1abalikewise require user-scoped cache invalidation:openspec/specs/reset/spec.md,openspec/specs/feature-flag-cache/spec.mdandacceptance/private/feature-flag-cache.feature.Executed regression
The three-case
review_web_before_send_flags_test.dartexercises public Posthog calls with a source-faithful browser stub, without mutating private registry state. It holds capture inbeforeSend, emits the old response only while JS reset remains queued, releases capture, awaits reset, checks the public getter, then attaches a late listener.Executed under the same Flutter3.35.4/Dart3.9.2/Chromium1248 offline environment on each snapshot:
53e1279f9449e040b3a1b94cad6fa043eb559ff4{old-user: true}.d04ec3af0934e2f1afbf0840ab3f73f1735840404d81d9e8575b97efeb0b125ac8cb281902a7c4cbCurrent failure:
Ordinary awaited reset without a held capture passes. A later fresh response correctly replaces the stale replay snapshot, bounding the observed impact.
The stub does not invent an empty-notification omission: actual browser
posthog-core.ts:3847–3993clears persistence/flags then starts reload, andpackages/browser-common/src/feature-flags.ts:1994–2013does not fire an empty flags notification (PostHog/posthog-js@a055ee65e61a876a7b399271b9df026d22dbb144).Consider making replay invalidation part of the ordered JavaScript reset handover, preserving capture→reset ordering, the browser registration and fresh post-reset notifications. Clearing indiscriminately after awaiting reset could discard a valid replacement notification. Add this regression with its ordinary-reset/fresh-response controls. No proposed correction was applied or tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1538fa6 — web
reset()now clears replay when the queued reset reaches posthog-js, with your scenario as a regression test.