Repository navigation
ci: update SDK compliance harness to 1.26.0 - #680
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contributor
posthog-flutter Compliance ReportDate: 2026-10-29T01:54:30.055554+00:00 ✅ All Tests Passed!47/47 tests passed Capture Tests✅ 30/30 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
Co-Authored-By: Claude Opus 5.5 <[email protected]>
marandaneto
approved these changes
Oct 29, 2026
marandaneto
left a comment
Member
There was a problem hiding this comment.
The code looks good; no qualifying findings. Verdict: correct.
turnipdabeets
merged commit Oct 29, 2026
9016430
into
feat/v6-desktop-capture-ai
18 of 27 checks passed
turnipdabeets
added a commit
that referenced
this pull request
Oct 29, 2026
…d 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]>
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
Update: moved to 1.26.0 (
c90b42db), the version the server SDK PRs use (e.g. PostHog/posthog-rs#317). 1.26.0 only changes thecapture_analytics_v1/capture_ai_v1suites, which this adapter doesn't run, so the counts below still hold.Stacked on #679 (
feat/v6-desktop-capture-ai). A harness maintainer asked the mobile capture v1 branches to move to the latest harness (PostHog/posthog-android#881 (comment)). This PR bumps the SDK compliance harness from 0.9.0 to 1.25.0:test-harness-version:0.9.0→1.26.027806196…(main, July) →c90b42dbe2235c89aad3b4dddf74f520a8c6c747(the1.26.0tag). Its inputs are the same as in July. The only change intest-sdk-action.ymlis theupload-artifactpin.sdk_compliance_adapter/docker-compose.ymlimage:0.9.0→1.26.0Why #679
#679 is the only open 6.0 branch that has both desktop capture v1 (#673, through the #678 merge) and
captureAi(#674). #673 has nocaptureAi, and #674 has no desktop capture v1. #679's adapter also already takes theoptionsparameter that #674 added to the platform interface.The adapter doesn't exercise either feature yet, though.
ComplianceAdapterreplacesPosthogFlutterPlatformInterface.instancewith its own_CompliancePlatform, which builds events and posts them to/batchitself. The harness tests the DartPosthog()facade and that adapter-side transport, not the desktop capture v1 client. The adapter declares onlycapture_v0andencoding_gzip, so at 1.25.0 it runs thecaptureandfeature_flagssuites (sdk-typeserver), the same as at 0.9.0. The bump is safe on any branch in this stack. I put it on #679 because a later change that drives the real desktop client through the adapter would go there.Capabilities
No new capabilities are declared. All the capabilities added in 1.24 and 1.25 only gate tests in the
capture_analytics_v1andcapture_ai_v1suites. Those suites needcapture_v1/capture_ai_v1, which this adapter can't honestly declare until it sends events through the SDK's own capture v1 transport.event_uuid_overridecapture/captureAitake no caller uuid.capture_ai_keeps_supplied_uuidis alsosdk_types: [server].bounded_retriesmax_retries_respectedin the v1 suites.disable_geoip/flagsdisable_geoiptests infeature_flagsaren't gated and still run.historical_migrationreported_drops/stateto show the SDK's per-eventdropresults, and the adapter doesn't see them.generated_payloads/capture_aisize tests. The adapter has no/capture_airoute.log_capturegenerated_payloadsincapture_ai_v1.property_layers/initsuper_propertiesandbefore_sendwired to the real SDK.fail_openphx_key handling, which the adapter skips.What changes for this adapter between 0.9.0 and 1.25.0:
capture.non_utc_event_timestamp_is_converted_to_utc(1.0.0) andfeature_flags.retry_behavior.retries_flags_on_502/retries_flags_on_504(0.10.0). The adapter already handles all three./flagstests that assertedperson_properties.distinct_id(0.9.1 relaxed that contract).💚 How did you test it?
I ran the adapter locally with
flutter test test/adapter_server_test.dart(Docker wasn't available) and ran the harness from source at each tag withposthog-test-harness run --sdk-type server:2169494c): 47 tests incapture+feature_flags, 47 passed, 0 failed.68498bcf, baseline): 45 tests, 45 passed, 0 failed.I didn't run the
ghcr.io/posthog/sdk-test-harness:1.25.0image locally. CI on this PR runs it.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset fileNo changeset: this is CI/test-harness only and ships nothing, the same as the 0.9.0 bump (#458).
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Claude Code (Opus) read the 0.9.1–1.25.0 release notes and the 1.25.0 contracts, compared the 6.0 branches' ancestry and adapter code to pick the base, and ran both harness versions against the local adapter.
Rejected: declaring
capture_v1/capture_ai_v1and the 1.24/1.25 capabilities in this PR. The adapter's events never go through the SDK's capture v1 transport, so passing those suites would only test the adapter's own code. Moving the adapter onto the real desktop client is a separate change.🤖 Generated with Claude Code