Skip to content

fix(replay): let native opt-out and opt-in own the replay stop and restart - #687

Merged
turnipdabeets merged 5 commits into
mainfrom
fix/opt-in-native-restarts-replay
Oct 30, 2026
Merged

turnipdabeets merged 5 commits into
mainfrom
fix/opt-in-native-restarts-replay

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

In short: optOut() and optIn() now leave the session replay stop and restart to the native SDKs. Flutter only turns its own capture loop off and back on. Before, Flutter's own stop/start calls made native forget that the app wanted replay, and in two cases replay came back on the session that was active while the user was opted out.

What was wrong:

  • optOut() called native stopSessionRecording() while still opted in. Since posthog-ios #959 and posthog-android #839, native treats that as the app stopping replay, so native optIn() never restarted it.
  • optIn() then restarted replay itself with startSessionRecording(resumeCurrent: false). That start checks the linked flag before starting a new session, on both iOS and Android. So when the linked flag held replay off at opt-in, no new session started, and replay later resumed on the opted-out session once flags loaded.
  • On Android, native opt-in queued a resume on the old session before Flutter's start got through, so even the plain case resumed on the opted-out session.

Change

  • optOut() no longer calls native stopSessionRecording(). Native opt-out stops replay itself.
  • optIn() no longer calls native startSessionRecording(resumeCurrent: false). Native opt-in restarts the replay the app wanted, in a new session. Flutter forces a replay session reset, so the next frame is a full snapshot, and turns its capture loop back on.
  • Raised the minimum PostHog iOS SDK to 3.92.3, the first release with #959's rule. The example apps are pinned to 3.92.7.

Android in the linked-flag case: this relies on native Android starting a new session even when the flag holds replay off, which is PostHog/posthog-android#915. Until that ships and Flutter's Android minimum is raised in a follow-up, Flutter on Android behaves as before in that one case. That's no regression.

Known issues, not changed here

  • Native Android restarts a replay the app stopped (with sessionReplay = true) on the next remote-config re-evaluation. Flutter sends nothing in that state, because its capture loop stays off, but isSessionReplayActive() reports true. This is described in posthog-android#915 and will be a separate fix there.
  • On iOS, a screenshot still being encoded when the session changes is sent under the new session ahead of that session's meta event. The plugin's sendFullSnapshot encodes on a background queue before calling capture. It happens on any session change, and main does it too.

💚 How did you test it?

  • flutter test test/opt_out_session_replay_test.dart passes. The updated test asserts that no native stop/start is sent and that opt-in forces a replay session reset. Removing that reset fails the test. dart format and flutter analyze are clean.
  • Each run used a local mock server that controls remote config and /flags and records every request:
    • iOS: iPhone 17 Pro simulator (iOS 26.4), with posthog-ios 3.92.7 resolved.
    • Android: Pixel 9 emulator, with posthog-android built from posthog-android#915 and published to mavenLocal.
Scenario iOS Android main
Recording → opt-out → opt-in New session, starts with meta + full snapshot New session Android: resumes the opted-out session
Linked flag off → opt-out → opt-in → flag on + reload New session; replay starts on it Same Both: iOS never restarts replay; Android resumes the opted-out session
Stop → opt-out → opt-in Stays off Nothing sent (see known issues) Same
Stop while opted out → opt-in Restarts in a new session Same Same
Manual start with sessionReplay = false → opt-out → opt-in Restarts in a new session Same Same
Launched opted out → opt-in Records Records Same
10 rapid opt-out/opt-in (awaited and un-awaited) Ends recording; one new session; no crash Same —

No events or snapshots were sent while opted out in any run.

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

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-10-30 15:34:04 UTC
Duration: 96379ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 125ms
Format Validation.Event Has Uuid ✅ 113ms
Format Validation.Event Has Lib Properties ✅ 110ms
Format Validation.Distinct Id Is String ✅ 109ms
Format Validation.Token Is Present ✅ 110ms
Format Validation.Custom Properties Preserved ✅ 110ms
Format Validation.Event Has Timestamp ✅ 111ms
Retry Behavior.Retries On 503 ✅ 5323ms
Retry Behavior.Does Not Retry On 400 ✅ 2113ms
Retry Behavior.Does Not Retry On 401 ✅ 2113ms
Retry Behavior.Respects Retry After Header ✅ 8120ms
Retry Behavior.Implements Backoff ✅ 15422ms
Retry Behavior.Retries On 500 ✅ 5216ms
Retry Behavior.Retries On 502 ✅ 5221ms
Retry Behavior.Retries On 504 ✅ 5219ms
Retry Behavior.Max Retries Respected ✅ 15425ms
Deduplication.Generates Unique Uuids ✅ 116ms
Deduplication.Preserves Uuid On Retry ✅ 5217ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10317ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5220ms
Deduplication.No Duplicate Events In Batch ✅ 115ms
Deduplication.Different Events Have Different Uuids ✅ 109ms
Compression.Sends Gzip When Enabled ✅ 109ms
Batch Format.Uses Proper Batch Structure ✅ 107ms
Batch Format.Flush With No Events Sends Nothing ✅ 104ms
Batch Format.Multiple Events Batched Together ✅ 113ms
Error Handling.Does Not Retry On 403 ✅ 2110ms
Error Handling.Does Not Retry On 413 ✅ 2112ms
Error Handling.Retries On 408 ✅ 5211ms

Feature_Flags Tests

✅ 16/16 tests passed

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

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Nothing blocking from me. I traced the new optOut/optIn path against posthog-ios 3.92.3 and posthog-android 3.72.9 and the Dart change lines up with what native does at those floors. The Android linked-flag gap you call out is real and sits on posthog-android#915. The only thing I left inline is the stale version comment in the podspec.

@turnipdabeets
turnipdabeets enabled auto-merge (squash) October 30, 2026 15:32
@turnipdabeets
turnipdabeets merged commit 5b85326 into main Oct 30, 2026
30 checks passed
@turnipdabeets
turnipdabeets deleted the fix/opt-in-native-restarts-replay branch October 30, 2026 15:48
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