Skip to content

feat(flags): add defaultValue to isFeatureEnabled - #694

Merged
turnipdabeets merged 3 commits into
chore/v6-posthog-ios-4from
feat/v6-is-feature-enabled-default
Nov 2, 2026
Merged

turnipdabeets merged 3 commits into
chore/v6-posthog-ios-4from
feat/v6-is-feature-enabled-default

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

The is-feature-enabled spec requires a caller-supplied default: return it when the flag has no value (flags not loaded, failed request, no such key). A flag with a value, including false and variants, always wins.

Future<bool> isFeatureEnabled(String key, {bool? sendFeatureFlagEvent, bool? defaultValue})

Calls without defaultValue take the same code path as before. iOS gets the same option in a sibling PR, but that's unreleased, so nothing here depends on it.

Design: resolve the default in Dart, never pass it to a native SDK or posthog-js. This needs no new native code and works with every pinned SDK version.

Platform How "no value" is detected $feature_flag_called
iOS, macOS With a default, calls getFeatureFlagResult; null means no value Same as today. Native isFeatureEnabled is a wrapper around the same call (checked in 3.92.3 and the v4 branch)
Android Same Same send and dedupe logic. One difference: for a missing flag the response is "" instead of the false that bare isFeatureEnabled reports, because that call substitutes its built-in default
Windows, Linux The core client already returns null Unchanged; response is null
Web posthog-js isFeatureEnabled returns undefined Unchanged; response is the real flag value

Why not the native default on Android: PostHog.isFeatureEnabled(key, defaultValue) reports the default as $feature_flag_response, which is what we must avoid.

Why not defaultValue on posthog-js: only recent versions support it, and apps load their own version. Reading undefined works on all of them.

sendFeatureFlagEvent is respected on every path. Native errors and a disabled SDK also return the default on iOS and Android.

Base branch: chore/v6-posthog-ios-4. #649's diff doesn't touch these lines or add anything needed here, and the native read path is the same on the 3.x and 4.x SDKs. This commit also merges cleanly with feat/v6-desktop-capture-v1 and feat/v6-on-feature-flags-listener.

💚 How did you test it?

  • flutter analyze: no issues.
  • flutter test: all pass (1313 passed, 1 skipped).
  • scripts/check-api-dart.sh: snapshot up to date.
  • flutter test --platform chrome with the CI file list: 109 passed, 3 failed. The same 3 app version tests fail on the base commit locally; all new web tests pass.
  • New tests cover missing flag, false beats default true and variant beats default false on the method channel, desktop and web. Also native failure, event forwarding, failed flags request and the desktop event's response value. Temporarily breaking each implementation made the new tests fail.
  • Not run on a device or simulator. The iOS, macOS and Android behavior comes from reading the native SDK source, plus method-channel tests with a mocked native side.

📝 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)

Written with Claude Code. It compared the native and posthog-js flag paths before choosing the Dart-side design, and rejected forwarding the default because of the Android event behavior and posthog-js version differences above.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 30, 2026
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 30, 2026 22:50
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-10-31 02:18:27 UTC
Duration: 96808ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 143ms
Format Validation.Event Has Uuid ✅ 119ms
Format Validation.Event Has Lib Properties ✅ 115ms
Format Validation.Distinct Id Is String ✅ 114ms
Format Validation.Token Is Present ✅ 115ms
Format Validation.Custom Properties Preserved ✅ 120ms
Format Validation.Event Has Timestamp ✅ 117ms
Retry Behavior.Retries On 503 ✅ 5330ms
Retry Behavior.Does Not Retry On 400 ✅ 2118ms
Retry Behavior.Does Not Retry On 401 ✅ 2117ms
Retry Behavior.Respects Retry After Header ✅ 8126ms
Retry Behavior.Implements Backoff ✅ 15443ms
Retry Behavior.Retries On 500 ✅ 5226ms
Retry Behavior.Retries On 502 ✅ 5221ms
Retry Behavior.Retries On 504 ✅ 5224ms
Retry Behavior.Max Retries Respected ✅ 15430ms
Deduplication.Generates Unique Uuids ✅ 125ms
Deduplication.Preserves Uuid On Retry ✅ 5222ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10334ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5230ms
Deduplication.No Duplicate Events In Batch ✅ 124ms
Deduplication.Different Events Have Different Uuids ✅ 116ms
Compression.Sends Gzip When Enabled ✅ 115ms
Batch Format.Uses Proper Batch Structure ✅ 113ms
Batch Format.Flush With No Events Sends Nothing ✅ 108ms
Batch Format.Multiple Events Batched Together ✅ 123ms
Error Handling.Does Not Retry On 403 ✅ 2115ms
Error Handling.Does Not Retry On 413 ✅ 2117ms
Error Handling.Retries On 408 ✅ 5224ms

Feature_Flags Tests

✅ 16/16 tests passed

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

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

@turnipdabeets
turnipdabeets merged commit c2e61cf into chore/v6-posthog-ios-4 Nov 2, 2026
24 of 25 checks passed
@turnipdabeets
turnipdabeets deleted the feat/v6-is-feature-enabled-default branch November 2, 2026 19:10
turnipdabeets added a commit that referenced this pull request Nov 2, 2026
turnipdabeets added a commit that referenced this pull request Nov 2, 2026
turnipdabeets added a commit that referenced this pull request Nov 3, 2026
…695)

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener

Posthog().onFeatureFlags() can be registered before setup() and removed, so the
config field no longer adds anything. Android now drives Dart from the
posthog-android listener like iOS does, which also fires after a manual reload,
so the Dart-side Android reload delivery goes away.

* docs: fold #695 release notes into the 6.0 docs

* test: use the onFeatureFlags listener in desktop tests from the base merge

* fix(example): initialize the binding before registering the flags listener

* docs: show the removed onFeatureFlags constructor argument in the migration guide

* test: use the flags listener in the #694 desktop tests

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
turnipdabeets added a commit that referenced this pull request Nov 3, 2026
* chore!: adopt posthog-ios 4.0

- Require posthog-ios 4.x.
- Adapt to the reloadFeatureFlags(_:) callback that now passes a
  PostHogFeatureFlagsLoaded (PostHog/posthog-ios#913).
- Note the payload decoding changes from PostHog/posthog-ios#903.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(changeset): note posthog-ios 4.0 drops lifecycle version and build

PostHog/posthog-ios#905 removes the legacy version/build properties from
lifecycle events in 4.0, so Flutter iOS and macOS apps stop sending them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(ios): drive onFeatureFlags from the posthog-ios onFeatureFlags listener

posthog-ios 4.0 removes the didReceiveFeatureFlags notification.

* chore!: adopt posthog-android 4.0 (#663)

* chore!: adopt posthog-android 4.0

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags (#664)

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags

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.

* fix: complete the web reloadFeatureFlags with the reloaded flags

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.

* chore: use PostHogFeatureFlagsCallback from posthog-android 4.0

* chore: fix ktlint function-signature

* fix: await posthog-js reloadFeatureFlagsAsync on web

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.

* docs: pass the loaded flags in the setup example

* docs: drop best-effort web note from the person and group property methods

* fix(web): wait for posthog-js flags retries before reporting a reload 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.

* docs: note where onFeatureFlags and errorsLoading differ on Android and web

* docs: describe the older posthog-js reload timeout as the full retry budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs(changeset): fold the posthog-android 4.0 and flags-loaded entries into one changeset

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat: add an onFeatureFlags listener you can unsubscribe

* fix: isolate failing flag listeners, skip web registration without onFeatureFlags, update API snapshot

* fix: deliver flag loads to onFeatureFlags listeners before the config callback

A config callback that threw kept listeners from getting the load, and a
reset() or close() it started was undone when the load was cached after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): stop flag registrations left by close() or the posthog-js loader

A registration whose unsubscribe isn't stored yet (close() during the synchronous replay) or never arrives (the loader snippet's stub) now goes inert once setup() or close() runs again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: drop native flag loads sent before a reset or close reached native

Dart bumps a generation when reset() or close() clears the listener registry and sends it with setup, reset and close. Android and iOS tag each onFeatureFlags notification with the generation they had when it fired, and Dart drops ones from an earlier generation, so the previous user's flags can't repopulate the replay cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: start receiving native flag loads when the first listener is added

With native auto-init, Posthog().onFeatureFlags() can be the app's only
Dart SDK call, and nothing created the platform that installs the channel
handler, so native loads never reached the listener.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: deliver Android reloadFeatureFlags results to onFeatureFlags listeners

posthog-android passes a manual reload only to its completion, so no
onFeatureFlagsCallback arrives and listeners missed the load while new
ones replayed the older snapshot. A reload that finishes after a reset or
close is still dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: stop passing RRStyle to replay screenshot frames (#684)

posthog-android 4.0 drops RRStyle and the wireframe tree fields from the
internal RRWireframe; screenshot frames don't need a style.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(web): clear replayed flags when a queued reset reaches posthog-js

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #670 release notes into the 6.0 docs

* fix: don't throw when SDK calls run before the Flutter binding exists

Posthog() creates PosthogFlutterIO, whose constructor set the channel handler
and aborted without a binding. Retry the install instead, and fail native calls
with a PlatformException so each caller's existing fallback applies.

* fix: stop delivering a flags load that a listener's reset() or close() cleared

The listeners after it and the config callback no longer get the old flags.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener (#695)

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener

Posthog().onFeatureFlags() can be registered before setup() and removed, so the
config field no longer adds anything. Android now drives Dart from the
posthog-android listener like iOS does, which also fires after a manual reload,
so the Dart-side Android reload delivery goes away.

* docs: fold #695 release notes into the 6.0 docs

* test: use the onFeatureFlags listener in desktop tests from the base merge

* fix(example): initialize the binding before registering the flags listener

* docs: show the removed onFeatureFlags constructor argument in the migration guide

* test: use the flags listener in the #694 desktop tests

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
turnipdabeets added a commit that referenced this pull request Nov 5, 2026
)

* chore!: adopt posthog-ios 4.0

- Require posthog-ios 4.x.
- Adapt to the reloadFeatureFlags(_:) callback that now passes a
  PostHogFeatureFlagsLoaded (PostHog/posthog-ios#913).
- Note the payload decoding changes from PostHog/posthog-ios#903.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(changeset): note posthog-ios 4.0 drops lifecycle version and build

PostHog/posthog-ios#905 removes the legacy version/build properties from
lifecycle events in 4.0, so Flutter iOS and macOS apps stop sending them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(ios): drive onFeatureFlags from the posthog-ios onFeatureFlags listener

posthog-ios 4.0 removes the didReceiveFeatureFlags notification.

* chore!: adopt posthog-android 4.0 (#663)

* chore!: adopt posthog-android 4.0

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags (#664)

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags

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.

* fix: complete the web reloadFeatureFlags with the reloaded flags

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.

* chore: use PostHogFeatureFlagsCallback from posthog-android 4.0

* chore: fix ktlint function-signature

* fix: await posthog-js reloadFeatureFlagsAsync on web

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.

* docs: pass the loaded flags in the setup example

* docs: drop best-effort web note from the person and group property methods

* fix(web): wait for posthog-js flags retries before reporting a reload 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.

* docs: note where onFeatureFlags and errorsLoading differ on Android and web

* docs: describe the older posthog-js reload timeout as the full retry budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs(changeset): fold the posthog-android 4.0 and flags-loaded entries into one changeset

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat: add an onFeatureFlags listener you can unsubscribe

* fix: isolate failing flag listeners, skip web registration without onFeatureFlags, update API snapshot

* fix: deliver flag loads to onFeatureFlags listeners before the config callback

A config callback that threw kept listeners from getting the load, and a
reset() or close() it started was undone when the load was cached after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): stop flag registrations left by close() or the posthog-js loader

A registration whose unsubscribe isn't stored yet (close() during the synchronous replay) or never arrives (the loader snippet's stub) now goes inert once setup() or close() runs again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: drop native flag loads sent before a reset or close reached native

Dart bumps a generation when reset() or close() clears the listener registry and sends it with setup, reset and close. Android and iOS tag each onFeatureFlags notification with the generation they had when it fired, and Dart drops ones from an earlier generation, so the previous user's flags can't repopulate the replay cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: start receiving native flag loads when the first listener is added

With native auto-init, Posthog().onFeatureFlags() can be the app's only
Dart SDK call, and nothing created the platform that installs the channel
handler, so native loads never reached the listener.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: deliver Android reloadFeatureFlags results to onFeatureFlags listeners

posthog-android passes a manual reload only to its completion, so no
onFeatureFlagsCallback arrives and listeners missed the load while new
ones replayed the older snapshot. A reload that finishes after a reset or
close is still dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: stop passing RRStyle to replay screenshot frames (#684)

posthog-android 4.0 drops RRStyle and the wireframe tree fields from the
internal RRWireframe; screenshot frames don't need a style.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(web): clear replayed flags when a queued reset reaches posthog-js

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener

Posthog().onFeatureFlags() can be registered before setup() and removed, so the
config field no longer adds anything. Android now drives Dart from the
posthog-android listener like iOS does, which also fires after a manual reload,
so the Dart-side Android reload delivery goes away.

* docs: fold #670 release notes into the 6.0 docs

* docs: fold #695 release notes into the 6.0 docs

* test: use the onFeatureFlags listener in desktop tests from the base merge

* fix(example): initialize the binding before registering the flags listener

* fix: don't throw when SDK calls run before the Flutter binding exists

Posthog() creates PosthogFlutterIO, whose constructor set the channel handler
and aborted without a binding. Retry the install instead, and fail native calls
with a PlatformException so each caller's existing fallback applies.

* docs: show the removed onFeatureFlags constructor argument in the migration guide

* feat!: report the device language as $locale and add $app_language

* fix: address the pre-PR review of $locale and $app_language

* docs: correct the $locale migration notes

* fix: forward setAppLanguage on web

* test: use the flags listener in the #694 desktop tests

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: stop delivering a flags load that a listener's reset() or close() cleared

The listeners after it and the config callback no longer get the old flags.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* fix: keep $app_language on a web capture made right after reset()

posthog-js clears registered properties on reset, and the language was only
registered again after an await, so an un-awaited capture() went out without
it. The web plugin now registers it in the same step as the reset. Native and
desktop keep the language across reset, so Posthog.reset() no longer resends.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say $locale follows a per-app language on iOS and macOS

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: drop the duplicate $locale upgrade checklist item

#701 already lists it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
turnipdabeets added a commit that referenced this pull request Nov 5, 2026
…reAi (#673)

* feat!: send desktop events through capture v1

Windows and Linux post to /i/v1/analytics/events with bearer auth, per-event results and the v1 retry rules.

* fix(desktop): log capture v1 per-event rejections

Counts only: dropped and rate-limited events were deleted from the queue
with no diagnostic, even in debug mode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: describe capture v1 for every platform in the 6.0 changeset

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): close capture v1 batches at 5 MiB of serialized events

A batch closed only at maxBatchSize events, so large events could push a
request past proxy and capture body limits. Each event's serialized size is
now checked before it joins the batch; an event that doesn't fit starts the
next one, and an event over the limit goes alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: retry desktop capture v1 requests that get 404 or 429 (#678)

* fix: retry desktop capture v1 requests that get 404 or 429

A 404 usually means a misconfigured proxy and a 429 an overloaded Envoy; both are worth retrying with the same backoff, Retry-After handling and retry budget. Redirects stay terminal.

* fix: back off across failed desktop flushes and warn once on a capture 404

After a batch fails its retries, later flushes make one attempt each and
pause 6s doubling to 30s until a batch is delivered. Before, every flush,
including the one each capture starts at flushAt, sent 4 requests, so a
missing capture route kept resending the batch.

A 404 prints one warning per client even without debug, naming the URL
and the path a proxy or self-hosted instance must serve.

* fix: leave the host out of the capture 404 warning

A host with proxy credentials in its userinfo would print them to app logs.

* fix(desktop): back off capture v1 retries like posthog-ios and posthog-android

A transient failure or a 2xx whose per-event results ask to retry no longer
resends within the flush (3 tries, 3 s apart). Each one pauses the queue for
1 s doubling to 30 s, with ±25% jitter from an injectable source. A
Retry-After is a floor applied after the jitter, capped at 30 s, instead of
replacing the backoff, so `Retry-After: 1` no longer resends every 3 s.
A delivered or dropped batch resets the backoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): time out capture requests on inactivity, not on total duration

The 10 s whole-call timeout could fail a large batch on a slow uplink on
every attempt. A request now fails only after 10 s without progress:
connecting (HttpClient.connectionTimeout), the socket taking a 64 KiB body
chunk, the response headers, or a response body chunk. This is the closest
dart:io gets to the per-read/per-write idle timeouts of OkHttp and
URLSession: like them, it can't see the bytes still in the kernel send
buffer, so the wait for headers starts once the last chunk is handed over.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): halve flushAt with the batch size on 413 and end the flush

A 413 halved only the batch size and resent at once in the same flush. Like
posthog-ios and posthog-android, it now also halves flushAt, kept within
the batch size, and the smaller batches go out with the next flush (the
next capture over flushAt, or the periodic flush). No backoff is added:
posthog-ios ends the flush without one; posthog-android pauses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): flush once 5 MiB of events are queued

Like posthog-ios and posthog-android, a full capture v1 request's worth of
queued bytes (captureV1MaxBatchBytes) now flushes as flushAt does, so a few
large events go out before the queue starts dropping them. The file queue
keeps each event's serialized size: measured when it is added, and read
from the file sizes when a later process loads the queue.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): cap the response body read at 30 seconds

A body that trickles in more often than the 10 s idle timeout reset it
forever, holding the flush. The body now also fails after 30 s in total.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat(desktop): captureAi on the AI endpoint, native property order and options passthrough (#679)

* chore!: adopt posthog-ios 4.0

- Require posthog-ios 4.x.
- Adapt to the reloadFeatureFlags(_:) callback that now passes a
  PostHogFeatureFlagsLoaded (PostHog/posthog-ios#913).
- Note the payload decoding changes from PostHog/posthog-ios#903.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(changeset): note posthog-ios 4.0 drops lifecycle version and build

PostHog/posthog-ios#905 removes the legacy version/build properties from
lifecycle events in 4.0, so Flutter iOS and macOS apps stop sending them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(ios): drive onFeatureFlags from the posthog-ios onFeatureFlags listener

posthog-ios 4.0 removes the didReceiveFeatureFlags notification.

* chore!: adopt posthog-android 4.0

* feat: add per-event capture options and captureAi

Passes options and captureAi through to posthog-ios and posthog-android 4.0; Windows, Linux and web send known options as their legacy properties.

* feat: add captureAi queue and request settings to PostHogConfig

Add captureAiCompression, captureAiMaxQueueSize, captureAiTimeout and
captureAiMaxEventBytes. Set values are sent to native setup and applied to
the posthog-ios and posthog-android config; unset values are omitted so the
native defaults apply. They have no effect on Windows, Linux and web.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: spell out when captureAi returns null

null means the event wasn't admitted: dropped by beforeSend, oversized on
iOS and macOS, SDK disabled or opted out, or an unsupported platform. On
Windows, Linux and web captureAi is a no-op that sends nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: captureAi returns null for an oversized event on Android too

posthog-android now checks the size before queuing (f46a9711).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: retry desktop capture v1 requests that get 404 or 429

A 404 usually means a misconfigured proxy and a 429 an overloaded Envoy; both are worth retrying with the same backoff, Retry-After handling and retry budget. Redirects stay terminal.

* fix: back off across failed desktop flushes and warn once on a capture 404

After a batch fails its retries, later flushes make one attempt each and
pause 6s doubling to 30s until a batch is delivered. Before, every flush,
including the one each capture starts at flushAt, sent 4 requests, so a
missing capture route kept resending the batch.

A 404 prints one warning per client even without debug, naming the URL
and the path a proxy or self-hosted instance must serve.

* fix: leave the host out of the capture 404 warning

A host with proxy credentials in its userinfo would print them to app logs.

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags (#664)

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags

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.

* fix: complete the web reloadFeatureFlags with the reloaded flags

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.

* chore: use PostHogFeatureFlagsCallback from posthog-android 4.0

* chore: fix ktlint function-signature

* fix: await posthog-js reloadFeatureFlagsAsync on web

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.

* docs: pass the loaded flags in the setup example

* docs: drop best-effort web note from the person and group property methods

* fix(web): wait for posthog-js flags retries before reporting a reload 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.

* docs: note where onFeatureFlags and errorsLoading differ on Android and web

* docs: describe the older posthog-js reload timeout as the full retry budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat(desktop): send event options unchanged in the capture v1 event

On Windows and Linux, the options of capture, screen, captureException,
identify, setPersonProperties, alias and group now go into the event's
capture v1 `options` as they are, unknown keys included, instead of being
mapped to legacy `$` properties. An option still wins over its legacy
property, which is moved into `options` only when the option is unset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(desktop): build event properties in the native 4.0 order

On Windows and Linux, an event's properties are now the ones the SDK
sets, then the registered ones, then the event's own, each winning over
the ones before, and the Dart beforeSend callbacks run after that merge.
They see and can change every property and option the SDK adds, and the
event is queued as they leave it. A caller or registered value changes
that event only.

Person processing is no longer a `$process_person_profile` property: the
event carries `process_person_profile: false` in its options only while
person processing is off and neither an option nor a property sets it,
and omits it otherwise. The event's timestamp is taken before the
callbacks run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): back off capture v1 retries like posthog-ios and posthog-android

A transient failure or a 2xx whose per-event results ask to retry no longer
resends within the flush (3 tries, 3 s apart). Each one pauses the queue for
1 s doubling to 30 s, with ±25% jitter from an injectable source. A
Retry-After is a floor applied after the jitter, capped at 30 s, instead of
replacing the backoff, so `Retry-After: 1` no longer resends every 3 s.
A delivered or dropped batch resets the backoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): time out capture requests on inactivity, not on total duration

The 10 s whole-call timeout could fail a large batch on a slow uplink on
every attempt. A request now fails only after 10 s without progress:
connecting (HttpClient.connectionTimeout), the socket taking a 64 KiB body
chunk, the response headers, or a response body chunk. This is the closest
dart:io gets to the per-read/per-write idle timeouts of OkHttp and
URLSession: like them, it can't see the bytes still in the kernel send
buffer, so the wait for headers starts once the last chunk is handed over.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): halve flushAt with the batch size on 413 and end the flush

A 413 halved only the batch size and resent at once in the same flush. Like
posthog-ios and posthog-android, it now also halves flushAt, kept within
the batch size, and the smaller batches go out with the next flush (the
next capture over flushAt, or the periodic flush). No backoff is added:
posthog-ios ends the flush without one; posthog-android pauses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(desktop): send captureAi events to the capture v1 AI endpoint

On Windows and Linux, captureAi now builds the event like capture, runs
the Dart beforeSend callbacks, and queues it on disk apart from other
events, in posthog_ai_queue/. It is sent to /i/v1/ai/events with the
same wire format, batching, retry rules and backoff as analytics events,
and returns the event's uuid, or null when the event isn't queued:
dropped by beforeSend, opted out, or over the size limit.

- A serialized event over captureAiMaxEventBytes, capped at and by
  default 8 MiB plus 64 KiB, measured before compression, is dropped with
  a log line naming only the event and its size.
- The queue keeps captureAiMaxQueueSize events (100 by default) and at
  most 50 MiB, dropping the oldest first, and flushes as soon as 5 MiB
  is queued.
- Requests use captureAiCompression (gzip by default) and are bounded by
  captureAiTimeout (60 seconds by default).
- Each endpoint keeps its own retry identity, timers, backoff and 413
  batch size, so a failing AI request doesn't hold back other events.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: describe desktop captureAi, options and property order in changesets

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): flush once 5 MiB of events are queued

Like posthog-ios and posthog-android, a full capture v1 request's worth of
queued bytes (captureV1MaxBatchBytes) now flushes as flushAt does, so a few
large events go out before the queue starts dropping them. The file queue
keeps each event's serialized size: measured when it is added, and read
from the file sizes when a later process loads the queue.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): feed flags the person properties beforeSend keeps

$set/$set_once were stored for flag evaluation while the event was
prepared, before the beforeSend callbacks ran, so a redacted value or a
dropped event still reached the next /flags request. They are now taken
from the event as it is queued.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): keep properties beforeSend removes off the queued event

The payload builder merged the common properties ($feature/<key>,
$active_feature_flags, $recording_status, $lib) under every event again,
so a property the callbacks removed came back. Every core path already
builds them into the event's properties, so the payload adds nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* perf(desktop): size the capture batch before reading queued events

The flush peeked up to maxBatchSize events, decoding each one, before
the 5 MiB batch cap picked the few it sends. With 8 MiB AI events queued
that read far more than one request carries, on the UI isolate. The
queue now applies the cap from its tracked per-event sizes before it
reads an event, always keeping the first, so only the batch is decoded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): release delivered payloads after each capture v1 batch

A flush that kept finding queued events held every delivered payload
until it ended. Emit flush per batch so multi-megabyte AI events are
released as soon as they are sent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): turn on person processing only for events beforeSend keeps

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): give beforeSend a deep copy of the event properties

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: limit the capture v1 migration note to native and desktop platforms

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(desktop): note beforeSend cannot change $lib or $lib_version

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): cap the response body read at 30 seconds

A body that trickles in more often than the 10 s idle timeout reset it
forever, holding the flush. The body now also fails after 30 s in total.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): warn when captureAiMaxEventBytes is over the ceiling

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* style: format the merged fetch attempt

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: update SDK compliance harness to 1.26.0 (#680)

* ci: update SDK compliance harness to 1.25.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(compliance): bump the test harness to 1.26.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(desktop): check registered person processing before caching identify

* fix(desktop): keep a legacy $set in properties for beforeSend to remove

* fix(desktop): recompute person processing from the properties beforeSend keeps

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

* fix(desktop): recheck person processing after beforeSend adds person properties

An anonymous event that a callback gave $set kept the SDK's
process_person_profile: false, so PostHog ignored the person update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): apply person state only once an event is queued

An AI event dropped for its size still turned on person processing and
fed its person properties to /flags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: captureAiMaxEventBytes limits the whole event on Android too

posthog-android applies a set value to the whole serialized event, as iOS
and macOS do (captureAiMaxStoredEventBytes on feat/v4-capture-v1).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): log event names and sizes, not properties, in debug mode

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): send the screen name beforeSend leaves

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(desktop): registered and event properties override platform context

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: stop passing RRStyle to replay screenshot frames (#684)

posthog-android 4.0 drops RRStyle and the wireframe tree fields from the
internal RRWireframe; screenshot frames don't need a style.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* chore!: adopt posthog-ios 4.0 and posthog-android 4.0 (#649)

* chore!: adopt posthog-ios 4.0

- Require posthog-ios 4.x.
- Adapt to the reloadFeatureFlags(_:) callback that now passes a
  PostHogFeatureFlagsLoaded (PostHog/posthog-ios#913).
- Note the payload decoding changes from PostHog/posthog-ios#903.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(changeset): note posthog-ios 4.0 drops lifecycle version and build

PostHog/posthog-ios#905 removes the legacy version/build properties from
lifecycle events in 4.0, so Flutter iOS and macOS apps stop sending them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(ios): drive onFeatureFlags from the posthog-ios onFeatureFlags listener

posthog-ios 4.0 removes the didReceiveFeatureFlags notification.

* chore!: adopt posthog-android 4.0 (#663)

* chore!: adopt posthog-android 4.0

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags (#664)

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags

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.

* fix: complete the web reloadFeatureFlags with the reloaded flags

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.

* chore: use PostHogFeatureFlagsCallback from posthog-android 4.0

* chore: fix ktlint function-signature

* fix: await posthog-js reloadFeatureFlagsAsync on web

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.

* docs: pass the loaded flags in the setup example

* docs: drop best-effort web note from the person and group property methods

* fix(web): wait for posthog-js flags retries before reporting a reload 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.

* docs: note where onFeatureFlags and errorsLoading differ on Android and web

* docs: describe the older posthog-js reload timeout as the full retry budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs(changeset): fold the posthog-android 4.0 and flags-loaded entries into one changeset

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: stop passing RRStyle to replay screenshot frames (#684)

posthog-android 4.0 drops RRStyle and the wireframe tree fields from the
internal RRWireframe; screenshot frames don't need a style.

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

* docs: fold capture v1 and native 4.0 release notes into the 6.0 docs

* fix(desktop): skip phx_ keys, keep response bodies out of logs, count duplicate copies (#681)

* fix(desktop): skip phx_ keys, keep response bodies out of logs, count duplicate copies

- Posthog.setup skips a projectToken that is a personal API key (phx_), like the native SDKs, so the key never goes on the wire (INV-32).
- PostHogFetchHttpError.toString leaves out the response body, which can echo request values (FC-5/FQ-18); the flush error log names the lane.
- captureV1Results reads duplicates_dropped and the per-batch log reports the dropped copies (FQ-19).

* fix(desktop): clamp duplicates_dropped to the batch size

* fix(desktop): clamp duplicates_dropped against every copy sent

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): drop unreadable 2xx batches and log lost events with endpoint and reasons

- A 2xx whose body has no readable per-event results fails the batch for
  good: it's dropped, not resent, and reported through the error event,
  matching posthog-ios and posthog-android.
- One aggregate line per batch that lost events, naming the endpoint, the
  count and capture's `details` tags (never uuids or content). A terminal
  status logs how many events it lost; a multi-event 413 still halves.
- A `limited` result counts as delivered, like any unknown result.
- The non-string session/window id warning names the JSON type `boolean`.
- Test fakes answer capture requests with per-event results, as capture
  v1 does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(desktop): answer capture v1 batches with readable results

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): log only reason codes from capture v1 drops

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #681 release notes into the 6.0 docs

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs(migration): keep PostHog keys when a proxy strips Authorization

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: pass the loaded flags to onFeatureFlags in the #694 desktop tests

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* perf(desktop): move queue file work and request compression off the UI isolate (#688)

* perf(desktop): move queue file work and request compression off the UI isolate

On Windows and Linux the Dart client encoded, measured, wrote and read
queued events and gzipped request bodies on the UI isolate. An 8 MB AI
event stalled it for over 200 ms. A long-lived background isolate now
does that work, in order, falling back to the calling isolate if it
cannot start or exits.

- The queue keeps its order and counts on the UI isolate; an event
  takes its place at once and is measured by the worker.
- captureAi waits for the worker's size verdict, and applies person
  properties only if the user is unchanged.
- close() waits for pending writes and deletes, so the next client
  neither misses an event nor sends one again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): wait only for writes preceding a queue peek

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): exclude AI events being measured from queue eviction

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): keep selecting queue events past unreadable files

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): keep person properties set while an AI event is measured

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(ci): TEMP diagnose the hanging test step

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(ci): remove the temporary test step diagnostics

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): recheck flushAt once a queued event is written

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): skip the snapshot write for AI events without person properties

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): keep the directory locked until queue file work is done

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): don't throw from settled while only one queue has file work

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): bound how long close waits for queue file work

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): bound the queue writes given to the worker

Captures outpacing the worker kept every event in memory until it was written. Writes past a small window now wait in the queue, where eviction can cancel them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(desktop): finish queue file work before temp directories are deleted

Windows can't delete a directory with a file still open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): complete capture once the event is written, and survive failed queue jobs

An awaited capture is on disk again, so awaiting it before exit() keeps the event. A failed write job keeps the event in memory instead of wedging the queue, a flush waits for events still being written, and a failed job during close no longer stops the next setup. Records the unawaited-capture timing as a breaking change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): complete identify, alias, group and setPersonProperties once written

Awaiting them before exit() keeps their events, like capture. Without this, identify lost its event for good, since the new id is already saved. Also tests the awaits and the failed close properly, and records the change in the changeset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs(desktop): state what awaiting a call keeps, and trim comments

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): guard deferred AI person properties with a reset generation

Identifying an anonymous user keeps the person, so comparing distinct ids
discarded their properties, and let pre-reset ones back when the same id
was identified again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): log when queue work falls back to the UI isolate

The frame drops the worker removes would otherwise return with no sign of why.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* refactor(desktop): keep the gzip choice with the encoded body

The bytes and the Content-Encoding header could otherwise disagree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): release the shared worker's logger when its client closes

The logger retains the client, and with it any events kept in memory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(desktop): avoid a protected member in the logger release test

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #688 release notes into the 6.0 docs

* fix(desktop): keep events after an unmeasured AI event person-processed

An AI event with $set turned on person processing only once the worker had
measured it, so an unawaited captureAi followed by capture sent the next
event personless.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): limit AI events waiting to be written by estimated bytes (#693)

* fix(desktop): limit AI events waiting to be written by estimated bytes

The count limit alone let unawaited captures of large events hold over a
GiB in memory. These events would mostly be evicted by the 50 MiB queue
limit once measured, so they are now evicted before that, and captureAi
reports them as not queued.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): count containers in the waiting-bytes estimate

A long list of empty strings otherwise counts as nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* perf(desktop): keep a running total of waiting bytes

Rescanning every waiting event on each capture is quadratic on the UI
isolate in a burst of small events.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #693 release notes into the 6.0 docs

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* test(desktop): cover calls made while the queue worker holds its reply (#700)

GatedWorker can now hold reads and request bodies too. The new tests make a
call while the worker has not answered, then release it: captures, evictions,
reset and close during a flush's read or encode, and AI events measured after
close or dropped for their size during a flush.

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat!: make beforeSend callbacks synchronous (#686)

* feat!: make beforeSend callbacks synchronous

Event and log beforeSend callbacks no longer return FutureOr, matching the
native SDKs and the before-send-hook spec. The event is handed to the SDK in
the same call, so calls made right after an un-awaited capture can no longer
reach the SDK first, on every platform including Windows and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep the call queue on iOS, macOS and Android

With #682 and beforeSend set, an event takes two native calls. Without the
queue, an optOut(), close() or setup() made right after capture() reaches
native between them and the event is lost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(android): restore the screenshot scale expectation lost in the main merge

* fix(web): hand feature events to posthog-js before yielding

Without CallOrder the await on the flag lookup let a reset() made right after
an un-awaited captureFeatureView or captureFeatureInteraction overtake it, so
the event went out as the new anonymous user with the previous user's flag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #686 release notes into the 6.0 docs

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat: add an onFeatureFlags listener you can unsubscribe (#670)

* chore!: adopt posthog-ios 4.0

- Require posthog-ios 4.x.
- Adapt to the reloadFeatureFlags(_:) callback that now passes a
  PostHogFeatureFlagsLoaded (PostHog/posthog-ios#913).
- Note the payload decoding changes from PostHog/posthog-ios#903.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(changeset): note posthog-ios 4.0 drops lifecycle version and build

PostHog/posthog-ios#905 removes the legacy version/build properties from
lifecycle events in 4.0, so Flutter iOS and macOS apps stop sending them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(ios): drive onFeatureFlags from the posthog-ios onFeatureFlags listener

posthog-ios 4.0 removes the didReceiveFeatureFlags notification.

* chore!: adopt posthog-android 4.0 (#663)

* chore!: adopt posthog-android 4.0

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags (#664)

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags

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.

* fix: complete the web reloadFeatureFlags with the reloaded flags

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.

* chore: use PostHogFeatureFlagsCallback from posthog-android 4.0

* chore: fix ktlint function-signature

* fix: await posthog-js reloadFeatureFlagsAsync on web

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.

* docs: pass the loaded flags in the setup example

* docs: drop best-effort web note from the person and group property methods

* fix(web): wait for posthog-js flags retries before reporting a reload 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.

* docs: note where onFeatureFlags and errorsLoading differ on Android and web

* docs: describe the older posthog-js reload timeout as the full retry budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs(changeset): fold the posthog-android 4.0 and flags-loaded entries into one changeset

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat: add an onFeatureFlags listener you can unsubscribe

* fix: isolate failing flag listeners, skip web registration without onFeatureFlags, update API snapshot

* fix: deliver flag loads to onFeatureFlags listeners before the config callback

A config callback that threw kept listeners from getting the load, and a
reset() or close() it started was undone when the load was cached after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): stop flag registrations left by close() or the posthog-js loader

A registration whose unsubscribe isn't stored yet (close() during the synchronous replay) or never arrives (the loader snippet's stub) now goes inert once setup() or close() runs again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: drop native flag loads sent before a reset or close reached native

Dart bumps a generation when reset() or close() clears the listener registry and sends it with setup, reset and close. Android and iOS tag each onFeatureFlags notification with the generation they had when it fired, and Dart drops ones from an earlier generation, so the previous user's flags can't repopulate the replay cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: start receiving native flag loads when the first listener is added

With native auto-init, Posthog().onFeatureFlags() can be the app's only
Dart SDK call, and nothing created the platform that installs the channel
handler, so native loads never reached the listener.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: deliver Android reloadFeatureFlags results to onFeatureFlags listeners

posthog-android passes a manual reload only to its completion, so no
onFeatureFlagsCallback arrives and listeners missed the load while new
ones replayed the older snapshot. A reload that finishes after a reset or
close is still dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: stop passing RRStyle to replay screenshot frames (#684)

posthog-android 4.0 drops RRStyle and the wireframe tree fields from the
internal RRWireframe; screenshot frames don't need a style.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(web): clear replayed flags when a queued reset reaches posthog-js

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #670 release notes into the 6.0 docs

* fix: don't throw when SDK calls run before the Flutter binding exists

Posthog() creates PosthogFlutterIO, whose constructor set the channel handler
and aborted without a binding. Retry the install instead, and fail native calls
with a PlatformException so each caller's existing fallback applies.

* fix: stop delivering a flags load that a listener's reset() or close() cleared

The listeners after it and the config callback no longer get the old flags.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener (#695)

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener

Posthog().onFeatureFlags() can be registered before setup() and removed, so the
config field no longer adds anything. Android now drives Dart from the
posthog-android listener like iOS does, which also fires after a manual reload,
so the Dart-side Android reload delivery goes away.

* docs: fold #695 release notes into the 6.0 docs

* test: use the onFeatureFlags listener in desktop tests from the base merge

* fix(example): initialize the binding before registering the flags listener

* docs: show the removed onFeatureFlags constructor argument in the migration guide

* test: use the flags listener in the #694 desktop tests

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

* ci: run the capture v1 compliance suites against the real desktop client (#690)

* ci: run the capture v1 compliance suites against the real desktop client

The adapter posted events to /batch with its own sender, so the harness never
exercised the SDK's capture v1 code. It now captures through DesktopPostHog and
records requests with a local forwarding proxy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: read the adapter's pending events from the SDK queues

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: keep status and redirects as the SDK would see them in the adapter proxy

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: bound how long the adapter proxy waits for the harness

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: fail the check when the compliance suites fail

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: let the adapter proxy wait as long as the AI client

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: run the compliance suites through the SDK's event and flag code

The adapter called captureStateless and sent its own /flags request, so
the suites skipped how the SDK builds events and evaluates flags. Events
now go through prepareEvent and capturePrepared, and flags through
reloadFeatureFlagsAsync and getFeatureFlagResult.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: keep flag state per user and count captures as they happen

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci: skip the anonymous flag reload when the adapter switches users

reset() reloads flags for its rotated anonymous id before the adapter sets the requested user. That sent an extra request, and its flags were served if the user's own reload then failed.

* ci: pass non-HTTP redirects through the compliance proxy

Uri.origin throws for other schemes and hostless URLs, so the proxy answered 502 and the SDK retried a redirect it treats as terminal.

* docs: list the recording proxy test in the adapter test command

* test: expect a 2xx without readable results to be dropped in the adapter

Capture now drops such a batch for good, like posthog-ios and posthog-android. The redirect test answers with per-event results, and the truncated-body test expects a drop with no retry.

* test(compliance): declare property_layers, fail_open, generated_payloads and log_capture

The adapter now runs the configured beforeSend callbacks with the SDK's
applyBeforeSend, registers super_properties, routes /init through the
same project-token check Posthog.setup uses (extracted to
isUsableProjectToken), pads /capture_ai properties for the event-size
tests and records debugPrint lines for /state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: drive the compliance adapter through the real desktop plugin

The adapter hand-copied the plugin's capture steps, so the suites could
pass on code apps don't run. PosthogFlutterDesktop now takes an optional
test-only client factory, and the adapter captures through the plugin's
capture() and captureAi(), with the harness's distinct id and timestamp
set on the injected client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(compliance): admit the trimmed project token, as public setup does

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat!: run beforeSend on the native-built event on iOS, macOS and Android (#682)

* feat!: run beforeSend on the native-built event on iOS, macOS and Android

Dart beforeSend callbacks now get the event the native SDK built, with
its system and registered properties, uuid, distinct id and timestamp,
and the native SDK sends what they return without adding properties.
Without callbacks, events still take one native call.

Also adds PosthogFlutterPlugin.addBeforeSend in Swift and Kotlin for
events the native SDK captures itself.

* fix: import only the PostHogInternal SPI from posthog-ios

* fix: drop built events whose beforeSend callbacks never finish

* fix: clear pending built events on setup and close

* fix: keep primitive arrays when sending built events over the channel

* feat(desktop): give beforeSend the full event and add nothing after it

- On Windows and Linux, `PostHogEvent` now carries the event's uuid,
  distinct ID and timestamp, as on iOS, macOS and Android. A callback can
  change the distinct ID (a blank one keeps the current) and the
  timestamp; the event keeps its uuid.
- `$exception_steps` are attached as the event is built, before the
  callbacks, as posthog-ios and posthog-android do, so a callback sees
  and can remove them. Steps recorded while a callback runs, or an event
  renamed to `$exception`, no longer get them.
- The prepared core event carries its uuid and distinct ID, which the
  queue keeps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* style: merge the repeated guards in buildEvent for swiftformat 0.64

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): keep another distinct id's person properties out of the current user's flag cache

When a beforeSend callback sends the event for another distinct id, its
$set and $set_once no longer update the current user's flag person
properties or person-properties hash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: optOut, close and setup wait for an event being sent

Native refuses sendBuiltEvent once one of them has reached it, so the event
would be lost if they could run between buildEvent and sendBuiltEvent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(beforeSend): run the callbacks set when the event was captured

setup() replaces the callbacks synchronously, so an event still waiting on
its native build ran the next setup's chain, or none, and skipped its filter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(beforeSend): keep capture semantics for events named $screen

With callbacks set, capture('$screen') reached the native builders' screen()
path, which drops an event without a $screen_name and, on iOS, ignores person
properties. Build it under another name and restore the name in Dart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): decide person processing before beforeSend

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #682 release notes into the 6.0 docs

* fix(desktop): keep another distinct id's person properties from enabling person processing

When a beforeSend callback sends an event with person properties for another
distinct id, the current user's persisted person processing state stays off,
so their later events keep process_person_profile: false.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): decide later events' person processing from what the app captured

Matches iOS and Android. A callback that adds $set to an anonymous event no
longer turns person processing on for later events, and one that strips $set
no longer leaves it off after a person-processed event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say person processing is decided before beforeSend on native

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: give beforeSend callbacks typed nested values on iOS, macOS and Android

Nested values from the method channel are untyped, so a callback casting
$exception_list to List<Map<String, dynamic>> threw and the event was dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(darwin): expire built events on a monotonic clock

A wall clock jump during a beforeSend callback could evict and lose the
event. Android already uses a monotonic clock.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say what the one-minute built-event expiry covers now that callbacks are sync

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep events and user properties through beforeSend (#698)

* feat!: make beforeSend callbacks synchronous

Event and log beforeSend callbacks no longer return FutureOr, matching the
native SDKs and the before-send-hook spec. The event is handed to the SDK in
the same call, so calls made right after an un-awaited capture can no longer
reach the SDK first, on every platform including Windows and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep the call queue on iOS, macOS and Android

With #682 and beforeSend set, an event takes two native calls. Without the
queue, an optOut(), close() or setup() made right after capture() reaches
native between them and the event is lost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(android): restore the screenshot scale expectation lost in the main merge

* fix(web): hand feature events to posthog-js before yielding

Without CallOrder the await on the flag lookup let a reset() made right after
an un-awaited captureFeatureView or captureFeatureInteraction overtake it, so
the event went out as the new anonymous user with the previous user's flag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold #686 release notes into the 6.0 docs

* fix: run a renaming beforeSend once and keep its user properties

A hook that renamed a screen or exception ran again through capture() and lost
its userProperties on web, iOS, Android and macOS. Also covers calls made from
inside a hook and bursts of events, and documents that a hook blocks the UI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep events when beforeSend resets on desktop or sets user properties on web

On Windows and Linux, a callback calling reset() or identify() dropped the
event. It now goes out as the user it was prepared for, as on iOS, Android and
macOS. On web, userProperties set on a screen or exception that kept its name
were lost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): keep an event on its user when beforeSend changes the user

posthog-js builds the event after the callbacks, so one calling reset() or
identify() moved the event to the new user. It now stays on the earlier user,
as on the other platforms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): keep the prepared user's person and session state when beforeSend changes the user

A hook calling reset() or identify() left the event on its original user,
but person processing was recomputed from the replacement's state, and a
$screen hook calling reset() put the old screen in the new session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): don't restore the distinct id across a beforeSend consent change

Under cookieless_mode on_reject, restoring the id reattached the opted-out
user to a cookieless event, or sent the shared cookieless id after opting in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): drop user properties when beforeSend resets the user

posthog-js stores an event's $set as the current user's person properties
for flags, so after a reset() in the callback they reached the next user.
identify() of an anonymous user links the two, so they stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: note what a web beforeSend calling reset() keeps

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: add exception steps before beforeSend on Windows and Linux

beforeSend can now read, change or remove $exception_steps, as on iOS and Android.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(compliance): pass uuid and prepared-event identity through the adapter

The adapter's captureStateless/captureAiStateless overrides now forward
the uuid parameter, and its prepareEvent override copies uuid, distinctId
and requestsPersonProcessing, so it compiles against #690's base.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix: send events with a lone surrogate on Windows and Linux (#703)

* fix: drop an event with a lone surrogate in its name or ids on Windows and Linux

Capture v1 rejects the whole request for a lone UTF-16 surrogate in a
top-level field, so one bad event lost its batch. Drop that event at
enqueue, and replace lone surrogates in properties and options with U+FFFD.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* fix: repair lone surrogates only in queued events, and keep dropped events from changing state

register() and the flag property stores keep their values as given, so a
registered id with a lone surrogate drops later events instead of being
repaired, and unregister() still matches its key. A dropped event no longer
updates person properties, and identify() ignores an id that isn't
well-formed instead of saving it.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* perf: repair lone surrogates in the background worker when the body is encoded

The scan cost as much as jsonEncode on the UI isolate; in the worker it
also covers property values sent as their toString().

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: ignore alias() and group() calls with a lone surrogate

Repairing them would merge distinct aliases or groups into one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: note in the changeset that identify, alias and group ignore a lone surrogate

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: repair lone surrogates in event names and ids on Windows and Linux

Send the event with U+FFFD instead of dropping it, as iOS, macOS and
Android already do through the platform channel. identify(), alias()
and group() store the repaired id instead of ignoring the call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: repair lone surrogates in the /flags request on Windows and Linux

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: cover the /flags request in the lone surrogate changeset entry

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep ids as given and repair lone surrogates only on the wire

identify(), alias() and group() stored a repaired id, while bootstrap,
storage and resetGroupPropertiesForFlags() kept what the app passed, so
the same id compared unequal: identify() after an identified bootstrap
was treated as a user switch, and a group type's flag properties could
not be reset. Ids now stay as the app passed them everywhere in the SDK,
and the capture and /flags bodies are repaired when they are encoded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* perf: copy a request body only when it has a lone surrogate

Encode first and repair only when the JSON has a \udXXX escape, so normal
batches skip the extra copy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs: link the capture v1 upgrade guide and cover capture v1 on every platform (#701)

* docs: link the capture v1 SDK upgrade guide

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: cover capture v1 on Windows and Linux and align the 6.0 guide with the backend SDK guides

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* docs: lone surrogates on Windows and Linux (#703)

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* docs: identify ignores a distinct ID with a lone surrogate (#703)

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* docs: complete the platform interface changes and the upgrade checklist

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: document the final 6.0 state in the guide and one grouped changeset

Folds in the MIGRATION.md and changeset text of #682, #698, #703, #670,
#695 and #699 so landing this PR last is "take this side".

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: describe lone surrogates as repaired on Windows and Linux

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: cover the /flags request in the lone surrogate changeset entry

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: note the /flags request in the lone surrogate guide line

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say the SDK keeps lone-surrogate ids as passed and repairs what it sends

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: drop the confirmed capture v1 default TODOs from the guide

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say $locale follows a per-app language picked in macOS Settings too

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(android): set PostHogEvent.options after copy

posthog-android 4.0 keeps the 3.x PostHogEvent constructor and copy so
posthog-server stays compatible, and makes options a property instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(desktop): resend a capture v1 retry alone to keep its request identity (#704)

When events wait to be retried, the next request of their lane sends
exactly those events, so it keeps PostHog-Request-Id and created_at and
increments PostHog-Attempt. Newer events go out after it as a new
request. If one of the events to retry is gone from the queue, the lane
falls back to normal batching and starts a new request at attempt 1.

Matches posthog-android 4.0 and the backend capture v1 SDKs.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat!: report the device language as $locale and add $app_language (#699)

* chore!: adopt posthog-ios 4.0

- Require posthog-ios 4.x.
- Adapt to the reloadFeatureFlags(_:) callback that now passes a
  PostHogFeatureFlagsLoaded (PostHog/posthog-ios#913).
- Note the payload decoding changes from PostHog/posthog-ios#903.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs(changeset): note posthog-ios 4.0 drops lifecycle version and build

PostHog/posthog-ios#905 removes the legacy version/build properties from
lifecycle events in 4.0, so Flutter iOS and macOS apps stop sending them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(ios): drive onFeatureFlags from the posthog-ios onFeatureFlags listener

posthog-ios 4.0 removes the didReceiveFeatureFlags notification.

* chore!: adopt posthog-android 4.0 (#663)

* chore!: adopt posthog-android 4.0

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags (#664)

* chore!: pass the loaded flags to onFeatureFlags and reloadFeatureFlags

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.

* fix: complete the web reloadFeatureFlags with the reloaded flags

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.

* chore: use PostHogFeatureFlagsCallback from posthog-android 4.0

* chore: fix ktlint function-signature

* fix: await posthog-js reloadFeatureFlagsAsync on web

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.

* docs: pass the loaded flags in the setup example

* docs: drop best-effort web note from the person and group property methods

* fix(web): wait for posthog-js flags retries before reporting a reload 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.

* docs: note where onFeatureFlags and errorsLoading differ on Android and web

* docs: describe the older posthog-js reload timeout as the full retry budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs(changeset): fold the posthog-android 4.0 and flags-loaded entries into one changeset

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat: add an onFeatureFlags listener you can unsubscribe

* fix: isolate failing flag listeners, skip web registration without onFeatureFlags, update API snapshot

* fix: deliver flag loads to onFeatureFlags listeners before the config callback

A config callback that threw kept listeners from getting the load, and a
reset() or close() it started was undone when the load was cached after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(web): stop flag registrations left by close() or the posthog-js loader

A registration whose unsubscribe isn't stored yet (close() during the synchronous replay) or never arrives (the loader snippet's stub) now goes inert once setup() or close() runs again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: drop native flag loads sent before a reset or close reached native

Dart bumps a generation when reset() or close() clears the listener registry and sends it with setup, reset and close. Android and iOS tag each onFeatureFlags notification with the generation they had when it fired, and Dart drops ones from an earlier generation, so the previous user's flags can't repopulate the replay cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: start receiving native flag loads when the first listener is added

With native auto-init, Posthog().onFeatureFlags() can be the app's only
Dart SDK call, and nothing created the platform that installs the channel
handler, so native loads never reached the listener.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: deliver Android reloadFeatureFlags results to onFeatureFlags listeners

posthog-android passes a manual reload only to its completion, so no
onFeatureFlagsCallback arrives and listeners missed the load while new
ones replayed the older snapshot. A reload that finishes after a reset or
close is still dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: stop passing RRStyle to replay screenshot frames (#684)

posthog-android 4.0 drops RRStyle and the wireframe tree fields from the
internal RRWireframe; screenshot frames don't need a style.

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix(web): clear replayed flags when a queued reset reaches posthog-js

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat!: remove PostHogConfig.onFeatureFlags in favor of the listener

Posthog().onFeatureFlags() can be registered before setup() and removed, so the
config field no longer adds anything. Android now drives Dart from the
posthog-android listener like iOS does, which also fires after a manual reload,
so the Dart-side Android reload delivery goes away.

* docs: fold #670 release notes into the 6.0 docs

* docs: fold #695 release notes into the 6.0 docs

* test: use the onFeatureFlags listener in desktop tests from the base merge

* fix(example): initialize the binding before registering the flags listener

* fix: don't throw when SDK calls run before the Flutter binding exists

Posthog() creates PosthogFlutterIO, whose constructor set the channel handler
and aborted without a binding. Retry the install instead, and fail native calls
with a PlatformException so each caller's existing fallback applies.

* docs: show the removed onFeatureFlags constructor argument in the migration guide

* feat!: report the device language as $locale and add $app_language

* fix: address the pre-PR review of $locale and $app_language

* docs: correct the $locale migration notes

* fix: forward setAppLanguage on web

* test: use the flags listener in the #694 desktop tests

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: stop delivering a flags load that a listener's reset() or close() cleared

The listeners after it and the config callback no longer get the old flags.

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

* fix: keep $app_language on a web capture made right after reset()

posthog-js clears registered properties on reset, and the language was only
registered again after an await, so an un-awaited capture() went out without
it. The web plugin now registers it in the same step as the reset. Native and
desktop keep the language across reset, so Posthog.reset() no longer resends.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say $locale follows a per-app language on iOS and macOS

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: drop the duplicate $locale upgrade checklist item

#701 already lists it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
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