Skip to content

chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags - #664

Open
turnipdabeets wants to merge 9 commits into
chore/v6-adopt-posthog-android-4from
chore/v6-flags-loaded-result
Open

turnipdabeets wants to merge 9 commits into
chore/v6-adopt-posthog-android-4from
chore/v6-flags-loaded-result

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

This assumed we’ve pulled in Android 4.0

PostHogConfig.onFeatureFlags was a void Function() and reloadFeatureFlags() returned Future<void>, so Flutter apps couldn't tell what loaded or whether loading failed. posthog-ios 4.0 passes this to its listener and reload callback (PostHog/posthog-ios#897, #913), posthog-android 4.0 gets the same (PostHog/posthog-android#877), and posthog-js already passes it to onFeatureFlags.

Closes #231

Both now carry a new PostHogFeatureFlagsLoaded:

Field Meaning
flags keys of the enabled flags
variants enabled flag → true or the variant string; disabled flags left out
errorsLoading true when the latest load failed; the values are then the last known ones

Per platform:

Platform onFeatureFlags reloadFeatureFlags()
iOS/macOS from posthog-ios onFeatureFlags from posthog-ios reloadFeatureFlags(_:)
Android from posthog-android PostHogFeatureFlagsCallback from posthog-android reloadFeatureFlags
Web from posthog-js onFeatureFlags posthog-js 1.438.2+: awaits featureFlags.reloadFeatureFlagsAsync(), which settles after this reload's own request (not one already in flight); errorsLoading unless it loaded. Older posthog-js: the next onFeatureFlags call, with a timeout of twice feature_flag_request_timeout_ms
Windows/Linux from the Dart core awaits the Dart core's reload

fromMap stays internal (channel_serialization.dart), in line with #646.

Breaking: onFeatureFlags = () { … } becomes (loaded) { … } or (_) { … }. Callers of reloadFeatureFlags() are unaffected; custom platform interface implementations must return the new type.

Depends on #663 (posthog-android 4.0). The Android code is written against the PostHogFeatureFlagsLoaded posthog-android 4.0 adds, which is still in review, so this stays a draft until that API is final.

💚 How did you test it?

  • flutter analyze, flutter test (1258 pass), dart format, scripts/check-api-dart.sh (snapshot updated).
  • New tests: the method channel passes native flags to the callback and drops disabled ones; malformed native args give empty flags; on desktop, reloadFeatureFlags() returns the loaded flags, and after a failed request returns the last known flags with errorsLoading: true, as does onFeatureFlags. The desktop test fails when errorsLoading is hardcoded to false.
  • flutter build ios --simulator against a local posthog-ios 4.0 checkout (temporary .package(path:), reverted), and flutter build web.
  • Web (Chrome): reloadFeatureFlags awaits reloadFeatureFlagsAsync() when present and reports errorsLoading when posthog-js skips the request; the first test fails without that path.
  • Not yet built: Android, until posthog-android 4.0 exists locally.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code (Claude Opus 5.5) made this as part of the feature flag parity work across the iOS, Android and Flutter majors. The field names mirror posthog-ios's PostHogFeatureFlagsLoaded. For web reloadFeatureFlags(), waiting only for the next posthog-js onFeatureFlags call could return a response from a request already in flight, and never fires when posthog-js skips the request, so newer posthog-js uses reloadFeatureFlagsAsync() and older versions keep that path behind a timeout.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 23, 2026

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 93f0034: 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

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.

Comment thread posthog_flutter/lib/src/posthog_flutter_web_handler.dart Outdated
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-10-27 18:31:52 UTC
Duration: 96496ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 130ms
Format Validation.Event Has Uuid ✅ 116ms
Format Validation.Event Has Lib Properties ✅ 113ms
Format Validation.Distinct Id Is String ✅ 112ms
Format Validation.Token Is Present ✅ 112ms
Format Validation.Custom Properties Preserved ✅ 114ms
Format Validation.Event Has Timestamp ✅ 114ms
Retry Behavior.Retries On 503 ✅ 5327ms
Retry Behavior.Does Not Retry On 400 ✅ 2113ms
Retry Behavior.Does Not Retry On 401 ✅ 2114ms
Retry Behavior.Respects Retry After Header ✅ 8122ms
Retry Behavior.Implements Backoff ✅ 15425ms
Retry Behavior.Retries On 500 ✅ 5222ms
Retry Behavior.Retries On 502 ✅ 5221ms
Retry Behavior.Retries On 504 ✅ 5220ms
Retry Behavior.Max Retries Respected ✅ 15439ms
Deduplication.Generates Unique Uuids ✅ 117ms
Deduplication.Preserves Uuid On Retry ✅ 5219ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10329ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5223ms
Deduplication.No Duplicate Events In Batch ✅ 117ms
Deduplication.Different Events Have Different Uuids ✅ 111ms
Compression.Sends Gzip When Enabled ✅ 111ms
Batch Format.Uses Proper Batch Structure ✅ 109ms
Batch Format.Flush With No Events Sends Nothing ✅ 107ms
Batch Format.Multiple Events Batched Together ✅ 121ms
Error Handling.Does Not Retry On 403 ✅ 2112ms
Error Handling.Does Not Retry On 413 ✅ 2114ms
Error Handling.Retries On 408 ✅ 5219ms

Feature_Flags Tests

✅ 16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 9ms
Request Payload.Flags Request Uses V2 Query Param ✅ 6ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 6ms
Request Payload.Flags Request Omits Authorization Header ✅ 5ms
Request Payload.Token In Flags Body Matches Init ✅ 6ms
Request Payload.Groups Round Trip ✅ 6ms
Request Payload.Groups Default To Empty Object ✅ 5ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It ✅ 6ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 5ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 6ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 6ms
Request Lifecycle.No Flags Request On Init Alone ✅ 2ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 109ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 11ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 6ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 110ms

@turnipdabeets
turnipdabeets marked this pull request as ready for review October 23, 2026 17:11
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 23, 2026 17:11

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 80ac673: 1 Should fix.

Comment thread posthog_flutter/lib/src/posthog_flutter_web_handler.dart Outdated
onFeatureFlags and reloadFeatureFlags now carry a PostHogFeatureFlagsLoaded with the enabled flags, their values and whether loading failed, matching posthog-ios 4.0 and posthog-android 4.0. On web, reloadFeatureFlags completes right away with the flags loaded so far, because posthog-js doesn't report when a reload finishes.
Wait for the next posthog-js onFeatureFlags call instead of returning the flags already loaded, so a failed reload reports errorsLoading. Complete right away when flags are disabled, and after twice the flags request timeout when posthog-js skips the request without calling back.
The next onFeatureFlags callback can carry a response from a request already in flight, so reloadFeatureFlags() could complete with flags from before the reload. posthog-js 1.438.2+ settles reloadFeatureFlagsAsync() after this reload's own request; use it when present and keep the callback path for older posthog-js.
@turnipdabeets
turnipdabeets force-pushed the chore/v6-flags-loaded-result branch from de3a190 to 0a7ab2a Compare October 26, 2026 18:38

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The native/desktop result plumbing and major migration look consistent, but the legacy web fallback can report a successful retry as failed with default settings. Consider accounting for the full retry lifecycle before completing the fallback. Two optional documentation qualifications are inline. Native 4.0 dependency/build prerequisites remain separate.

AI-assisted review.

try {
return await reloaded.future.timeout(
Duration(milliseconds: 2 * (requestTimeoutMs ?? 3000)),
onTimeout: () => currentFlags(errorsLoading: true),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1. [P1] Let supported legacy retries finish before reporting reload failure

IMPORTANT · REQUIRED · Introduced result-reporting defect
Location: posthog_flutter/lib/src/posthog_flutter_web_handler.dart:273–274, right side.

The legacy deadline can return cached flags with errorsLoading: true before posthog-js finishes a successful retry—even with its default 3000 ms timeout and one retry. It should await that supported reload cycle and return the successful snapshot.

A normal slow-network sequence is:

  1. The first request reaches its 3000 ms timeout.
  2. JS waits its built-in 500 ms retry backoff.
  3. The retry succeeds after 2800 ms, within its own timeout.
  4. Flutter has already returned at approximately 6000 ms with cached flags and failure; JS subsequently publishes fresh flags and success.

The supported caller is simply:

await Posthog().setup(PostHogConfig('<project_token>'));
final loaded = await Posthog().reloadFeatureFlags();

No overlapping reload, identity change or custom timeout is necessary. Applications branching on loaded.errorsLoading can incorrectly enter failure handling and receive stale values. This affects the retained older-JS branch; the modern async branch avoids this deadline.

Official JS 1.438.1 defaults to one retry and keeps retries/backoff inside the logical reload. The canonical completion contract explicitly forbids reporting failure when that reload succeeds.

Executed evidence: Loaded the actual published npm JS 1.438.1 distribution and exercised public Flutter setup/reload. Only fetch was intercepted: attempt one respected the real AbortSignal timeout; attempt two supplied a successful Response after 2800 ms. Both timeout/retry configuration keys were omitted. Actual JS retry and callback machinery remained intact.

Head: cached flags, errorsLoading=true, elapsedMs=6003
Later: 2 JS fetch attempts; fresh flags; successful notification
Assertion expected errorsLoading=false: FAIL
Ordinary successful-request control: PASS, fresh flags, ~6 ms

The equivalent base run observes the same retry and eventual fresh flags, but returns void/null immediately. Its controls pass; this is not a passing base error-result assertion, because that API did not exist.

Exact executed commands:

bash /Users/db/dev/review-evidence/flutter-664/run-validation.sh \
  head test --no-pub --reporter expanded --platform chrome \
  test/real_default_js_reload_test.dart
# exit 1: retry assertion fails; ordinary control passes

bash /Users/db/dev/review-evidence/flutter-664/run-validation.sh \
  base test --no-pub --reporter expanded --platform chrome \
  --dart-define=REVIEW_BASE=true test/real_default_js_reload_test.dart
# exit 0: both base controls pass

The complete fixture, readable template and logs remain under the evidence directory. The published tarball SHA1 was verified as d06c1d7727bd872ed34bce04af794d205023d138. This was deterministic browser execution, not a live backend test.

Consider deriving the fallback from the full supported retry budget, including configured retries, backoff and scheduling, and adding this regression beside the no-notification test. Requiring newer JS instead would need an explicit support-policy decision. No fix was applied or claimed tested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the repro! Fixed in a406020: the older-JS deadline now covers every attempt posthog-js may make, (feature_flag_request_max_retries + 2) × timeout + retries × 500 ms, and there's a regression test next to the no-notification one that fails the same way your run did without the fix.

/// It receives the enabled flags and whether loading failed. It runs after
/// the flags load at startup, after [Posthog.reloadFeatureFlags], and after
/// calls that reload flags such as `identify()` or `group()`. When a load
/// fails, [PostHogFeatureFlagsLoaded.errorsLoading] is `true` and the flags

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2. [P2] Qualify the callback lifecycle documentation for Android

MINOR · OPTIONAL · New documentation overstates unchanged runtime behavior
Location: posthog_flutter/lib/src/posthog_config.dart:268–270, right side.

The new text promises that config.onFeatureFlags runs after manual Posthog.reloadFeatureFlags(), but Android delivers only the supplied per-call completion on that path.

config.onFeatureFlags = (loaded) {
  // Rebuild feature-dependent UI.
};
// Later:
final loaded = await Posthog().reloadFeatureFlags();

The returned result works, but this configuration listener is not separately notified on Android. The Flutter plugin supplies its reload completion at PosthogFlutterPlugin.kt:1933–1937; Android’s reload implementation passes it through, and remote-config callback delivery does not additionally invoke the configuration callback. Setup/identity/group flows explicitly supply that callback instead.

Consider qualifying the Android documentation and directing manual-refresh callers to the returned result, rather than changing native callback policy here.

Verification: Deterministic source call-graph comparison. The manual-reload structure exists on base; only the unconditional documentation guarantee is new. Native runtime execution was not performed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, confirmed that manual reload on Android only passes the per-call callback. Qualified in 0e8a97b and pointed callers at the returned result.

/// Whether the latest attempt to load feature flags failed.
///
/// When it did, [flags] and [variants] hold the last known values.
final bool errorsLoading;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3. [P2] Document that browser replay lacks historical load-error context

MINOR · OPTIONAL · New documentation overstates an existing upstream representation
Location: posthog_flutter/lib/src/feature_flags_loaded.dart:30–33, right side.

On web, errorsLoading: false on an immediate callback replay does not necessarily mean the latest load succeeded.

The host initializes JS before Flutter setup. If its flags request fails before Flutter registers config.onFeatureFlags, JS can replay cached values without error context. Both current browser replay and legacy posthog-featureflags.ts:1677–1685 call the listener with only flags and variants. The legacy failed-response path still marks flags loaded. Flutter then defaults absent context to false in channel_serialization.dart:187.

An ordinary startup callback can therefore receive cached flags and errorsLoading: false after a failed JS load, contrary to the unqualified “whether the latest attempt … failed” wording. Live failed-load notifications still carry their error context.

Consider documenting the browser replay limitation. Preserving historical error state would require separate upstream/wrapper work and need not expand this PR.

Verification: Deterministic source proof; no dedicated replay regression was executed. This is a documentation qualification, not an introduced upstream runtime defect or a claim based on the newer listener scenario absent from the authoritative default branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, documented the web replay limitation in 0e8a97b. Keeping historical error context would need upstream work, so it stays out of this PR.

@dustinbyrne
dustinbyrne requested a review from a team October 27, 2026 17:00
… failure

posthog-js retries a timed-out /flags request once by default, 500 ms later, inside the same reload. The older-JS deadline of twice the request timeout reported cached flags with errorsLoading set before a successful retry landed.

This branch has not been deployed

No deployments
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.

2 participants