feat: synchronous feature flag reads via Dart-side fetch#328
feat: synchronous feature flag reads via Dart-side fetch#328brunovsiqueira wants to merge 2 commits intoPostHog:mainfrom
Conversation
Fetch feature flags directly from PostHog's /flags/?v=2 API on the Dart side, caching results in memory for synchronous reads. This eliminates the need for FutureBuilder/setState patterns when reading flags in build() methods. New sync API on the Posthog singleton: - isFeatureEnabledSync(key) - getFeatureFlagSync(key) - getFeatureFlagResultSync(key) - getAllFeatureFlagResults() - isFeatureFlagsLoaded New onFeatureFlagsLoaded callback receives the full flags map. The existing async API (isFeatureEnabled, getFeatureFlag, etc.) remains unchanged and still calls the native iOS/Android SDK. Both native and Dart caches are populated independently — if the Dart HTTP call fails, async methods still work from the native cache. Key behaviors (following Android SDK patterns): - Request coalescing: concurrent reloads are queued, not duplicated - Error merging: partial failures preserve cached values for failed flags - Quota limiting: skips update when feature_flags is quota limited
dustinbyrne
left a comment
There was a problem hiding this comment.
Hey @brunovsiqueira, thanks for the pull request! 👏
Just jotting down some initial thoughts here. From my perspective, I'd like to understand if it's necessary to introduce an additional feature flags client in Dart, or if we can cleanly get cached flag values into Dart from the native side such that we can serve them synchronously from a thin cache without requiring an additional client/request.
Open to hearing thoughts here.
| } | ||
|
|
||
| Future<void> _doLoadFeatureFlags(_LoadParams params) async { | ||
| final url = '$_host/flags/?v=2&config=true'; |
There was a problem hiding this comment.
We can drop &config=true here, this appends remote configuration to the response, and we're only considering the default flags response.
| // Fire legacy callback immediately (backward compat) | ||
| _onFeatureFlagsCallback?.call(); | ||
| // Also reload Dart-side flags; onFeatureFlagsLoaded fires when done | ||
| _reloadDartFlags(); |
There was a problem hiding this comment.
I wonder if it's better to read in native cached flag values here and avoid the duplicate call to /flags
There was a problem hiding this comment.
I'm thinking of implementing an opt-in-based approach, where we would add a new flag to the config, something like enableSyncFeatureFlags, that would switch between Native wrappers (current approach), to Dart-only Feature flags (new approach).
So we keep it without breaking changes, and also avoid duplicate calls in the case the user opted out of sync FFs.
Wdyt?
| try { | ||
| final distinctId = await getDistinctId(); | ||
| if (distinctId.isNotEmpty) { | ||
| await featureFlags.loadFeatureFlags(distinctId); |
There was a problem hiding this comment.
We're missing the anonymous ID and additional evaluation properties here. This would cause some flags to evaluate differently in dart than in native.
|
Hey @dustinbyrne 👋🏽 This approach would work as well, but it still keeps the SDK as a wrapper around the native SDKs for Android/iOS, which means Desktop (Windows/Linux/macOS) would remain unsupported. Since the calls to fetch the flags are simple HTTP calls, and given the upside that doing it in pure Dart would also enable Feature Flags to be used in other platforms like Desktop (given that Flutter is multi-platform), I lean towards the proposed approach. I think the upsides here overcome the downsides. Thoughts? |
desktop support is coming thru this PR #313 suggestion: |
|
Appreciate the dialogue here, and I'd like to add an additional thought:
We have to consider that each call to |
Problem
The PostHog Flutter SDK wraps native iOS/Android SDKs via method channels, making all feature flag reads async (
Future<bool>,Future<Object?>). This forces Flutter developers intoFutureBuilder/setStatepatterns just to read a flag value:There are other workarounds, like creating your own cache on top of the SDK when querying flags. However, these are all workarounds.
Most other PostHog SDKs (web, node, python, Android, iOS) provide synchronous flag reads. Flutter is the outlier. This is tracked across multiple issues: #241 (sync API), #231 (callback with data), #51 (pure Dart SDK).
Solution
Fetch feature flags directly from PostHog's
/flags/?v=2&config=trueAPI on the Dart side usingpackage:http, cache results in memory, and expose synchronous reads:New API
Architecture: Additive (not replacement)
The Dart cache runs alongside the native SDK — it does not replace it:
isFeatureEnabled, etc.)isFeatureEnabledSync, etc.)/flags/?v=2false/nullWhy additive instead of replacing native reads?
The tradeoff is one extra HTTP call on startup (both native and Dart fetch flags independently). Open to feedback on whether we should instead replace the async methods to read from the Dart cache (single source of truth, fewer HTTP calls).
Key Behaviors (following Android SDK patterns)
reloadFeatureFlags()calls are queued, not duplicatederrorsWhileComputingFlags=true, only non-failed flags are merged into cache (preserving cached values for failed flags)feature_flagsis quota limitedTest plan
dart analyzeclean (only pre-existing warning)build()identify()triggers flag reload via native callback