Repository navigation
fix(replay): stop session replay when the user opts out - #606
Conversation
|
Hey @o-mid Thank you for the contribution here. I ran a repro on Pixel 8 against a local mock server and the fix holds up. A couple of things in the description tat don't match what actually happened though, if you mind validating as well. After disable() the widget did keep screenshotting on Android, but every one dropped natively with "PostHog is in OptOut state", so nothing reached On iOS, optOut() uninstalls the replay integration completely, so the capturer skips screenshots. However, there's still a wasted getSessionReplayState call per interval Mind updating the PR description when you validate the above and also sign your commits before this can be merged? |
|
Updated the description. On Android the timer did keep capturing after |
60531d1 to
054afc9
Compare
| } | ||
|
|
||
| _config = config; // Store the config | ||
| _restartSessionReplayOnEnable = false; |
There was a problem hiding this comment.
If the user opted out in an earlier launch, setup() still sets sessionRecordingActive to true a few lines down, so after a relaunch the replay timer runs again until disable() is called. That's fine as a follow-up rather than something for this PR. The RN fix for the same issue (PostHog/posthog-js#5124) checks saved consent at startup. Would you like to take that on here too?
There was a problem hiding this comment.
I will leave the persisted opt-out for a follow-up. setup() still turns the Flutter timer on whenever sessionReplay is set, and checking saved consent means waiting on native setup the way the React Native change does.
| if (PostHogInternalEvents.sessionRecordingActive.value) { | ||
| _restartSessionReplayOnEnable = true; | ||
| // Native stopSessionRecording is a no-op once the SDK is already opted | ||
| // out, so this has to run before the platform disable. | ||
| await stopSessionRecording(); | ||
| } | ||
|
|
||
| return _posthog.disable(); | ||
| await _posthog.disable(); |
There was a problem hiding this comment.
Awaiting the stop delays sending disable by a platform round trip, so an un-awaited Posthog().disable(); Posthog().capture(...) now reaches native as [stopSessionRecording, capture, disable] (on main it's [disable, capture]) — the event goes out after the user opted out. Confirmed with a mock channel that replies one event-loop hop later. I think we need to send both without awaiting in between; invokeMethod dispatches synchronously, so the order holds (your tests still pass with this):
| if (PostHogInternalEvents.sessionRecordingActive.value) { | |
| _restartSessionReplayOnEnable = true; | |
| // Native stopSessionRecording is a no-op once the SDK is already opted | |
| // out, so this has to run before the platform disable. | |
| await stopSessionRecording(); | |
| } | |
| return _posthog.disable(); | |
| await _posthog.disable(); | |
| Future<void>? stopReplay; | |
| if (PostHogInternalEvents.sessionRecordingActive.value) { | |
| _restartSessionReplayOnEnable = true; | |
| PostHogInternalEvents.sessionRecordingActive.value = false; | |
| // Sent back to back, not awaited in between, so a capture() right after | |
| // an un-awaited disable() still reaches native after the opt-out. | |
| stopReplay = _posthog.stopSessionRecording(); | |
| } | |
| await Future.wait([if (stopReplay != null) stopReplay, _posthog.disable()]); |
A test with an un-awaited disable() followed by capture() would lock it in.
| if (restartReplay) { | ||
| // startSessionRecording is ignored while the SDK is still opted out, so | ||
| // the platform opt-in has to finish first. | ||
| await startSessionRecording(resumeCurrent: false); |
There was a problem hiding this comment.
On web this lands in posthog-js startSessionRecording(), which sets disable_session_recording: false, so an app that turned replay off in its posthog.init but shares sessionReplay = true in Dart starts recording after enable(). posthog-js already drops replay while opted out ($snapshot goes through capture(), which bails), and PostHogWidget doesn't record on web, so I think we should skip this on web. Gating the disable() side is enough, since the restart flag then never gets set:
if (!kIsWeb && PostHogInternalEvents.sessionRecordingActive.value) {(kIsWeb is already in scope via foundation.dart.)
| // startSessionRecording is ignored while the SDK is still opted out, so | ||
| // the platform opt-in has to finish first. |
There was a problem hiding this comment.
[nit] This holds on iOS, but Android's startSessionReplay doesn't check opt-out, so naming the platform keeps the next reader from treating the ordering as a cross-platform rule:
| // startSessionRecording is ignored while the SDK is still opted out, so | |
| // the platform opt-in has to finish first. | |
| // On iOS, startSessionRecording is ignored while the SDK is still opted | |
| // out, so the platform opt-in has to finish first. |
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the |
disable() left the Flutter screenshot loop running after opt-out. Stop it before the platform opt-out, and start a new recording on opt-in only if a recording was actually stopped.
Callers do not need the native stop and start constraints. Those notes now sit next to the calls.
Send the native stop and disable back to back instead of awaiting the stop first, so a capture() after an un-awaited optOut() still reaches native after the opt-out. Skip the stop and restart on web, where posthog-js already drops replay while opted out and a restart would override a replay disabled in posthog.init. Name iOS as the platform that ignores startSessionRecording while opted out. Co-authored-by: Omid Mirzaei <omidomirzaei@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
a4b121c to
d5726dd
Compare
|
Thanks @o-mid! I rebased this onto main and pushed a follow-up commit to address the review. @PostHog/team-client-libraries mind giving this a quick look before merging? |
Motivation
Posthog().disable()forwards opt-out and uninstalls Flutter error autocapture, but it leftsessionRecordingActivetrue, soPostHogWidgetkept its capture timer running.On Android that timer still took screenshots. Native
capturereturns immediately while opted out (PostHog is in OptOut state.), so those frames never reached/s/. On iOS,optOut()uninstalls the replay integration,getSessionReplayStatecomes back inactive, and the capturer skips the screenshot. The timer still calledgetSessionReplayStateon every interval.stopSessionRecordinghas to run before the platform opt-out: posthog-ios 3.84 and newer ignore that call once the SDK is already opted out.enable()starts a new recording only if thisdisable()was what stopped one.This is the Flutter side of #427. It does not close the Android gap in PostHog/posthog-android#242 —
optOut()there still does not uninstall other native integrations. This change only stops the Flutter replay loop through the existing channel.How I tested
posthog_flutter/test/opt_out_session_replay_test.dartfailed on main (sessionRecordingActivestayed true, and the channel only receiveddisable), then passed after the change.flutter testinposthog_flutter(708 tests).dart analyzeon the touched files, andscripts/check-api-dart.sh(public API snapshot unchanged).Checklist
pnpm changesetto generate a changeset file