diff --git a/.changeset/opt-in-native-replay-restart.md b/.changeset/opt-in-native-replay-restart.md new file mode 100644 index 00000000..4b81274d --- /dev/null +++ b/.changeset/opt-in-native-replay-restart.md @@ -0,0 +1,5 @@ +--- +'posthog_flutter': patch +--- + +Fix session replay after `optIn()` continuing the session that was active while opted out, and raise the minimum PostHog iOS SDK to 3.92.3. diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5389a599..a287dc5f 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PostHog/posthog-ios", "state" : { - "revision" : "e751c190562878700599243971a04fae17798480", - "version" : "3.84.0" + "revision" : "2b6b6b4301c14c3b51cb1a7827c954bb17d1b083", + "version" : "3.92.7" } } ], diff --git a/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5389a599..a287dc5f 100644 --- a/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PostHog/posthog-ios", "state" : { - "revision" : "e751c190562878700599243971a04fae17798480", - "version" : "3.84.0" + "revision" : "2b6b6b4301c14c3b51cb1a7827c954bb17d1b083", + "version" : "3.92.7" } } ], diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5389a599..a287dc5f 100644 --- a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PostHog/posthog-ios", "state" : { - "revision" : "e751c190562878700599243971a04fae17798480", - "version" : "3.84.0" + "revision" : "2b6b6b4301c14c3b51cb1a7827c954bb17d1b083", + "version" : "3.92.7" } } ], diff --git a/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5389a599..a287dc5f 100644 --- a/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PostHog/posthog-ios", "state" : { - "revision" : "e751c190562878700599243971a04fae17798480", - "version" : "3.84.0" + "revision" : "2b6b6b4301c14c3b51cb1a7827c954bb17d1b083", + "version" : "3.92.7" } } ], diff --git a/posthog_flutter/darwin/posthog_flutter.podspec b/posthog_flutter/darwin/posthog_flutter.podspec index c39b30b2..f99fd00d 100644 --- a/posthog_flutter/darwin/posthog_flutter.podspec +++ b/posthog_flutter/darwin/posthog_flutter.podspec @@ -21,8 +21,7 @@ Postog flutter plugin s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - # ~> Version 3.80.0 up to, but not including, 4.0.0 - s.dependency 'PostHog', '>= 3.84.0', '< 4.0.0' + s.dependency 'PostHog', '>= 3.92.3', '< 4.0.0' s.ios.deployment_target = '13.0' # PH iOS SDK 3.0.0 requires >= 10.15 diff --git a/posthog_flutter/darwin/posthog_flutter/Package.swift b/posthog_flutter/darwin/posthog_flutter/Package.swift index dd9a201d..c34f1946 100644 --- a/posthog_flutter/darwin/posthog_flutter/Package.swift +++ b/posthog_flutter/darwin/posthog_flutter/Package.swift @@ -14,7 +14,7 @@ let package = Package( ], dependencies: [ .package(name: "FlutterFramework", path: "../FlutterFramework"), - .package(url: "https://github.com/PostHog/posthog-ios", "3.84.0" ..< "4.0.0"), + .package(url: "https://github.com/PostHog/posthog-ios", "3.92.3" ..< "4.0.0"), ], targets: [ .target( diff --git a/posthog_flutter/lib/src/posthog.dart b/posthog_flutter/lib/src/posthog.dart index b0d95263..e16f36d1 100644 --- a/posthog_flutter/lib/src/posthog.dart +++ b/posthog_flutter/lib/src/posthog.dart @@ -28,8 +28,8 @@ class Posthog { PostHogConfig? _config; /// Set when [optOut] stops an in-progress Flutter replay. [optIn] consumes - /// it so opt-in starts a new recording, and a replay the caller already - /// stopped is left stopped. + /// it to re-arm Flutter capture for the recording native opt-in restarts, + /// and a replay the caller already stopped is left stopped. bool _restartSessionReplayOnEnable = false; /// Returns the singleton PostHog client instance. @@ -399,19 +399,17 @@ class Posthog { Future optOut() async { // Uninstall Flutter-specific integrations when opting out _uninstallFlutterIntegrations(); - Future? stopReplay; // Skipped on web: posthog-js drops replay while opted out, and restarting // it on optIn would override a replay disabled in posthog.init. if (!kIsWeb && PostHogInternalEvents.sessionRecordingActive.value) { _restartSessionReplayOnEnable = true; PostHogInternalEvents.sessionRecordingActive.value = false; - // Native stopSessionRecording is a no-op once the SDK is already opted - // out, so it is sent first. Not awaited before disable, so a capture() - // right after an un-awaited optOut() still reaches native after it. - stopReplay = _posthog.stopSessionRecording(); } - await Future.wait([if (stopReplay != null) stopReplay, _posthog.disable()]); + // Native opt-out stops replay itself. A native stopSessionRecording() here + // would record that the app stopped replay, so native opt-in would not + // restart it. + await _posthog.disable(); } /// Opts the current user back in to data collection. @@ -430,9 +428,8 @@ class Posthog { final restartReplay = _restartSessionReplayOnEnable; _restartSessionReplayOnEnable = false; if (restartReplay) { - // On iOS, startSessionRecording is ignored while the SDK is still opted - // out, so the platform opt-in has to finish first. - await startSessionRecording(resumeCurrent: false); + PostHogInternalEvents.requestReplaySessionReset(); + PostHogInternalEvents.sessionRecordingActive.value = true; } } diff --git a/posthog_flutter/test/opt_out_session_replay_test.dart b/posthog_flutter/test/opt_out_session_replay_test.dart index 1138f13e..ca2c2d8a 100644 --- a/posthog_flutter/test/opt_out_session_replay_test.dart +++ b/posthog_flutter/test/opt_out_session_replay_test.dart @@ -73,41 +73,50 @@ void main() { expect(methodsOf(recordedCalls), isNot(contains('sendFullSnapshot'))); }); - test('optOut stops replay before opting out, optIn starts a new recording', - () async { + test('optOut and optIn leave the replay restart to native', () async { PosthogFlutterPlatformInterface.instance = PosthogFlutterIO(); await Posthog().setup(replayConfig()); recordedCalls.clear(); await Posthog().optOut(); - final disabled = methodsOf(recordedCalls); - expect(disabled, contains('stopSessionRecording')); - expect(disabled, contains('disable')); + expect(methodsOf(recordedCalls), contains('disable')); expect( - disabled.indexOf('stopSessionRecording'), - lessThan(disabled.indexOf('disable')), - reason: 'native stop is a no-op once the SDK is already opted out', + methodsOf(recordedCalls), + isNot(contains('stopSessionRecording')), + reason: 'a native stop while opted in keeps replay off after opt-in', ); expect(PostHogInternalEvents.sessionRecordingActive.value, isFalse); recordedCalls.clear(); + final resetsBefore = PostHogInternalEvents.forceReplaySessionReset.value; await Posthog().optIn(); - final enabled = methodsOf(recordedCalls); - expect(enabled, contains('enable')); - expect(enabled, contains('startSessionRecording')); expect( - enabled.indexOf('enable'), - lessThan(enabled.indexOf('startSessionRecording')), - reason: 'startSessionRecording returns immediately while opted out', + PostHogInternalEvents.forceReplaySessionReset.value, + greaterThan(resetsBefore), + ); + expect(methodsOf(recordedCalls), contains('enable')); + expect( + methodsOf(recordedCalls), + isNot(contains('startSessionRecording')), + reason: 'native opt-in already starts a new session', ); - final start = recordedCalls - .firstWhere((call) => call.method == 'startSessionRecording'); - expect(start.arguments, isFalse); expect(PostHogInternalEvents.sessionRecordingActive.value, isTrue); }); + test('optIn leaves Flutter replay off when the app stopped it first', + () async { + PosthogFlutterPlatformInterface.instance = PosthogFlutterIO(); + await Posthog().setup(replayConfig()); + await Posthog().stopSessionRecording(); + + await Posthog().optOut(); + await Posthog().optIn(); + + expect(PostHogInternalEvents.sessionRecordingActive.value, isFalse); + }); + test('capture after an un-awaited optOut reaches native after the opt-out', () async { PosthogFlutterPlatformInterface.instance = PosthogFlutterIO();