From c99ea612ea7347030903e92af965b0482b56e30b Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Wed, 21 Jan 2026 02:24:19 +0200 Subject: [PATCH 1/6] feat: add manual start and stop recording --- CHANGELOG.md | 5 ++ .../posthog/flutter/PosthogFlutterPlugin.kt | 20 +++++ example/lib/main.dart | 90 +++++++++++++++++++ ios/Classes/PosthogFlutterPlugin.swift | 33 +++++++ lib/posthog_flutter_web.dart | 19 ++++ lib/src/posthog.dart | 18 ++++ lib/src/posthog_flutter_io.dart | 43 +++++++++ .../posthog_flutter_platform_interface.dart | 22 +++++ lib/src/posthog_flutter_web_handler.dart | 28 +++++- lib/src/replay/native_communicator.dart | 7 +- 10 files changed, 281 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74628df7..3d67c273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Next +- feat: add manual session recording control APIs ([#256](https://github.com/PostHog/posthog-flutter/pull/256)) + - `startSessionRecording({bool resumeCurrent = true})` - Start session recording, optionally starting a new session + - `stopSessionRecording()` - Stop the current session recording + - `isSessionReplayActive()` - Check if session replay is currently active + # 5.11.1 - fix: RichText, SelectableText, TextField labels and hints not being masked in session replay ([#251](https://github.com/PostHog/posthog-flutter/pull/251)) diff --git a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index c1e839ce..4790d970 100644 --- a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -173,6 +173,12 @@ class PosthogFlutterPlugin : "isSessionReplayActive" -> { result.success(isSessionReplayActive()) } + "startSessionRecording" -> { + startSessionRecording(call, result) + } + "stopSessionRecording" -> { + stopSessionRecording(result) + } "getSessionId" -> { getSessionId(result) } @@ -190,6 +196,20 @@ class PosthogFlutterPlugin : private fun isSessionReplayActive(): Boolean = PostHog.isSessionReplayActive() + private fun startSessionRecording( + call: MethodCall, + result: Result, + ) { + val resumeCurrent = call.argument("resumeCurrent") ?: true + PostHog.startSessionReplay(resumeCurrent) + result.success(null) + } + + private fun stopSessionRecording(result: Result) { + PostHog.stopSessionReplay() + result.success(null) + } + private fun handleMetaEvent( call: MethodCall, result: Result, diff --git a/example/lib/main.dart b/example/lib/main.dart index d868f8d4..0732670d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -254,6 +254,96 @@ class InitialScreenState extends State { child: Text("distinctId"), )), const Divider(), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + "Session Recording", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + onPressed: () async { + await _posthogFlutterPlugin.startSessionRecording(); + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Session recording started (resume current)'), + duration: Duration(seconds: 2), + ), + ); + } + }, + child: const Text("Start Recording"), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + ), + onPressed: () async { + await _posthogFlutterPlugin.startSessionRecording( + resumeCurrent: false); + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Session recording started (new session)'), + duration: Duration(seconds: 2), + ), + ); + } + }, + child: const Text("Start New Session"), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + onPressed: () async { + await _posthogFlutterPlugin.stopSessionRecording(); + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Session recording stopped'), + duration: Duration(seconds: 2), + ), + ); + } + }, + child: const Text("Stop Recording"), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + ), + onPressed: () async { + final isActive = + await _posthogFlutterPlugin.isSessionReplayActive(); + if (mounted && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Session replay active: $isActive'), + duration: const Duration(seconds: 2), + ), + ); + } + }, + child: const Text("Check Active"), + ), + ], + ), + const Divider(), const Padding( padding: EdgeInsets.all(8.0), child: Text( diff --git a/ios/Classes/PosthogFlutterPlugin.swift b/ios/Classes/PosthogFlutterPlugin.swift index ef750c19..05611c8e 100644 --- a/ios/Classes/PosthogFlutterPlugin.swift +++ b/ios/Classes/PosthogFlutterPlugin.swift @@ -223,6 +223,10 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin { sendFullSnapshot(call, result: result) case "isSessionReplayActive": isSessionReplayActive(result: result) + case "startSessionRecording": + startSessionRecording(call, result: result) + case "stopSessionRecording": + stopSessionRecording(result: result) case "getSessionId": getSessionId(result: result) case "openUrl": @@ -455,6 +459,35 @@ extension PosthogFlutterPlugin { #endif } + private func startSessionRecording( + _ call: FlutterMethodCall, + result: @escaping FlutterResult + ) { + #if os(iOS) + let resumeCurrent: Bool + if let args = call.arguments as? [String: Any], + let resume = args["resumeCurrent"] as? Bool + { + resumeCurrent = resume + } else { + resumeCurrent = true + } + PostHogSDK.shared.startSessionRecording(resumeCurrent: resumeCurrent) + result(nil) + #else + result(nil) + #endif + } + + private func stopSessionRecording(result: @escaping FlutterResult) { + #if os(iOS) + PostHogSDK.shared.stopSessionRecording() + result(nil) + #else + result(nil) + #endif + } + private func openUrl( _ call: FlutterMethodCall, result: @escaping FlutterResult diff --git a/lib/posthog_flutter_web.dart b/lib/posthog_flutter_web.dart index cf9a6f9e..1c51674e 100644 --- a/lib/posthog_flutter_web.dart +++ b/lib/posthog_flutter_web.dart @@ -225,4 +225,23 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface { }) async { // Not implemented on web } + + @override + Future startSessionRecording({bool resumeCurrent = true}) async { + return handleWebMethodCall(MethodCall('startSessionRecording', { + 'resumeCurrent': resumeCurrent, + })); + } + + @override + Future stopSessionRecording() async { + return handleWebMethodCall(const MethodCall('stopSessionRecording')); + } + + @override + Future isSessionReplayActive() async { + final result = + await handleWebMethodCall(const MethodCall('isSessionReplayActive')); + return result as bool? ?? false; + } } diff --git a/lib/src/posthog.dart b/lib/src/posthog.dart index ffa6a450..9bc74cf7 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -189,5 +189,23 @@ class Posthog { Future getSessionId() => _posthog.getSessionId(); + /// Starts session recording. + /// + /// This method will have no effect if PostHog is not enabled, or if session + /// replay is disabled in your project settings. + /// + /// [resumeCurrent] - If true (default), resumes recording of the current session. + /// If false, starts a new session and begins recording. + Future startSessionRecording({bool resumeCurrent = true}) => + _posthog.startSessionRecording(resumeCurrent: resumeCurrent); + + /// Stops the current session recording if one is in progress. + /// + /// This method will have no effect if PostHog is not enabled. + Future stopSessionRecording() => _posthog.stopSessionRecording(); + + /// Returns whether session replay is currently active. + Future isSessionReplayActive() => _posthog.isSessionReplayActive(); + Posthog._internal(); } diff --git a/lib/src/posthog_flutter_io.dart b/lib/src/posthog_flutter_io.dart index eba78977..e5ec94de 100644 --- a/lib/src/posthog_flutter_io.dart +++ b/lib/src/posthog_flutter_io.dart @@ -519,4 +519,47 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { printIfDebug('Exception on openUrl: $exception'); } } + + @override + Future startSessionRecording({bool resumeCurrent = true}) async { + if (!isSupportedPlatform()) { + return; + } + + try { + await _methodChannel.invokeMethod('startSessionRecording', { + 'resumeCurrent': resumeCurrent, + }); + } on PlatformException catch (exception) { + printIfDebug('Exception on startSessionRecording: $exception'); + } + } + + @override + Future stopSessionRecording() async { + if (!isSupportedPlatform()) { + return; + } + + try { + await _methodChannel.invokeMethod('stopSessionRecording'); + } on PlatformException catch (exception) { + printIfDebug('Exception on stopSessionRecording: $exception'); + } + } + + @override + Future isSessionReplayActive() async { + if (!isSupportedPlatform()) { + return false; + } + + try { + final result = await _methodChannel.invokeMethod('isSessionReplayActive'); + return result as bool? ?? false; + } on PlatformException catch (exception) { + printIfDebug('Exception on isSessionReplayActive: $exception'); + return false; + } + } } diff --git a/lib/src/posthog_flutter_platform_interface.dart b/lib/src/posthog_flutter_platform_interface.dart index faa09554..e63dcf2d 100644 --- a/lib/src/posthog_flutter_platform_interface.dart +++ b/lib/src/posthog_flutter_platform_interface.dart @@ -150,5 +150,27 @@ abstract class PosthogFlutterPlatformInterface extends PlatformInterface { throw UnimplementedError('getSessionId() not implemented'); } + /// Starts session recording. + /// + /// [resumeCurrent] - If true, resumes recording of the current session. + /// If false, starts a new session and begins recording. + /// Defaults to true. + Future startSessionRecording({bool resumeCurrent = true}) { + throw UnimplementedError( + 'startSessionRecording() has not been implemented.'); + } + + /// Stops the current session recording if one is in progress. + Future stopSessionRecording() { + throw UnimplementedError( + 'stopSessionRecording() has not been implemented.'); + } + + /// Returns whether session replay is currently active. + Future isSessionReplayActive() { + throw UnimplementedError( + 'isSessionReplayActive() has not been implemented.'); + } + // TODO: missing capture with more parameters } diff --git a/lib/src/posthog_flutter_web_handler.dart b/lib/src/posthog_flutter_web_handler.dart index ec2f91c9..4e869b89 100644 --- a/lib/src/posthog_flutter_web_handler.dart +++ b/lib/src/posthog_flutter_web_handler.dart @@ -34,6 +34,19 @@ extension PostHogExtension on PostHog { // ignore: non_constant_identifier_names external JSAny? get_session_id(); external void onFeatureFlags(JSFunction callback); + external void startSessionRecording(); + external void stopSessionRecording(); + external bool sessionRecordingStarted(); + external SessionManager? get sessionManager; +} + +// SessionManager JS interop +@JS() +@staticInterop +class SessionManager {} + +extension SessionManagerExtension on SessionManager { + external void resetSessionId(); } // Accessing PostHog from the window object @@ -204,9 +217,18 @@ Future handleWebMethodCall(MethodCall call) async { // Flutter Web uses the JS SDK for Session replay break; case 'isSessionReplayActive': - // not supported on Web - // Flutter Web uses the JS SDK for Session replay - return false; + return posthog?.sessionRecordingStarted() ?? false; + case 'startSessionRecording': + final resumeCurrent = args['resumeCurrent'] as bool? ?? true; + if (!resumeCurrent) { + // Reset session ID to start a new session + posthog?.sessionManager?.resetSessionId(); + } + posthog?.startSessionRecording(); + break; + case 'stopSessionRecording': + posthog?.stopSessionRecording(); + break; case 'openUrl': // not supported on Web break; diff --git a/lib/src/replay/native_communicator.dart b/lib/src/replay/native_communicator.dart index 4c0d22df..9cb70c61 100644 --- a/lib/src/replay/native_communicator.dart +++ b/lib/src/replay/native_communicator.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart'; import 'package:posthog_flutter/src/util/logging.dart'; @@ -34,10 +35,14 @@ class NativeCommunicator { } Future isSessionReplayActive() async { + if (kIsWeb) { + // Flutter doesn't capture screenshots on web, JS SDK handles session replay + return false; + } try { return await _channel.invokeMethod('isSessionReplayActive'); } catch (e) { - printIfDebug('Error sending full snapshot to native: $e'); + printIfDebug('Error checking session replay status: $e'); return false; } } From 00ada2bdf48d8f42f586c3e8da41565f7332b905 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Thu, 29 Jan 2026 20:53:51 +0200 Subject: [PATCH 2/6] fix: simplify method channel call --- .../posthog/flutter/PosthogFlutterPlugin.kt | 2 +- ios/Classes/.swiftformat | 109 ++++++++++++++++++ ios/Classes/PosthogFlutterPlugin.swift | 12 +- lib/src/posthog_flutter_io.dart | 4 +- lib/src/posthog_flutter_web_handler.dart | 2 +- 5 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 ios/Classes/.swiftformat diff --git a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index 09ce126c..dd028d52 100644 --- a/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -200,7 +200,7 @@ class PosthogFlutterPlugin : call: MethodCall, result: Result, ) { - val resumeCurrent = call.argument("resumeCurrent") ?: true + val resumeCurrent = call.arguments as? Boolean ?: true PostHog.startSessionReplay(resumeCurrent) result.success(null) } diff --git a/ios/Classes/.swiftformat b/ios/Classes/.swiftformat new file mode 100644 index 00000000..c15a1c08 --- /dev/null +++ b/ios/Classes/.swiftformat @@ -0,0 +1,109 @@ +# Disabled rules +--disable docComments +--disable noForceUnwrapInTests +--disable hoistTry +--disable redundantAsync +--disable redundantThrows +--disable redundantProperty +--disable redundantReturn +--disable redundantClosure +--disable redundantType +--disable wrapPropertyBodies +--disable blankLinesBetweenScopes + +--acronyms ID,URL,UUID +--allman false +--anonymousforeach convert +--assetliterals visual-width +--asynccapturing +--beforemarks +--binarygrouping none +--callsiteparen default +--categorymark "MARK: %c" +--classthreshold 0 +--closingparen balanced +--closurevoid remove +--commas always +--complexattrs preserve +--computedvarattrs preserve +--condassignment after-property +--conflictmarkers reject +--dateformat system +--decimalgrouping ignore +--doccomments before-declarations +--elseposition same-line +--emptybraces no-space +--enumnamespaces always +--enumthreshold 0 +--exponentcase lowercase +--exponentgrouping disabled +--extensionacl on-extension +--extensionlength 0 +--extensionmark "MARK: - %t + %c" +--fractiongrouping disabled +--fragment false +--funcattributes preserve +--generictypes +--groupedextension "MARK: %c" +--guardelse auto +--header ignore +--hexgrouping 4,8 +--hexliteralcase uppercase +--ifdef indent +--importgrouping alpha +--indent 4 +--indentcase false +--indentstrings false +--initcodernil false +--lifecycle +--lineaftermarks true +--linebreaks lf +--markcategories true +--markextensions always +--marktypes always +--maxwidth none +--modifierorder +--nevertrailing +--nilinit remove +--noncomplexattrs +--nospaceoperators +--nowrapoperators +--octalgrouping none +--onelineforeach ignore +--operatorfunc spaced +--organizationmode visibility +--organizetypes actor,class,enum,struct +--patternlet hoist +--ranges spaced +--redundanttype infer-locals-only +--self remove +--selfrequired +--semicolons never +--shortoptionals except-properties +--smarttabs enabled +--someany true +--storedvarattrs preserve +--stripunusedargs always +--structthreshold 0 +--tabwidth unspecified +--throwcapturing +--timezone system +--trailingclosures +--trimwhitespace always +--typeattributes preserve +--typeblanklines remove +--typedelimiter space-after +--typemark "MARK: - %t" +--voidtype void +--wraparguments preserve +--wrapcollections preserve +--wrapconditions preserve +--wrapeffects preserve +--wrapenumcases always +--wrapparameters default +--wrapreturntype preserve +--wrapternary default +--wraptypealiases preserve +--xcodeindentation disabled +--yodaswap always +--hexgrouping ignore \ No newline at end of file diff --git a/ios/Classes/PosthogFlutterPlugin.swift b/ios/Classes/PosthogFlutterPlugin.swift index 6e14c89c..8bfffddb 100644 --- a/ios/Classes/PosthogFlutterPlugin.swift +++ b/ios/Classes/PosthogFlutterPlugin.swift @@ -464,14 +464,7 @@ extension PosthogFlutterPlugin { result: @escaping FlutterResult ) { #if os(iOS) - let resumeCurrent: Bool - if let args = call.arguments as? [String: Any], - let resume = args["resumeCurrent"] as? Bool - { - resumeCurrent = resume - } else { - resumeCurrent = true - } + let resumeCurrent = call.arguments as? Bool ?? true PostHogSDK.shared.startSessionRecording(resumeCurrent: resumeCurrent) result(nil) #else @@ -678,8 +671,7 @@ extension PosthogFlutterPlugin { } } - private func reloadFeatureFlags(_ result: @escaping FlutterResult - ) { + private func reloadFeatureFlags(_ result: @escaping FlutterResult) { PostHogSDK.shared.reloadFeatureFlags() result(nil) } diff --git a/lib/src/posthog_flutter_io.dart b/lib/src/posthog_flutter_io.dart index c22c9ce3..495f9aa8 100644 --- a/lib/src/posthog_flutter_io.dart +++ b/lib/src/posthog_flutter_io.dart @@ -666,9 +666,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { } try { - await _methodChannel.invokeMethod('startSessionRecording', { - 'resumeCurrent': resumeCurrent, - }); + await _methodChannel.invokeMethod('startSessionRecording', resumeCurrent); } on PlatformException catch (exception) { printIfDebug('Exception on startSessionRecording: $exception'); } diff --git a/lib/src/posthog_flutter_web_handler.dart b/lib/src/posthog_flutter_web_handler.dart index 1bdcffe4..5655a12d 100644 --- a/lib/src/posthog_flutter_web_handler.dart +++ b/lib/src/posthog_flutter_web_handler.dart @@ -496,7 +496,7 @@ Future handleWebMethodCall(MethodCall call) async { case 'isSessionReplayActive': return posthog?.sessionRecordingStarted() ?? false; case 'startSessionRecording': - final resumeCurrent = args['resumeCurrent'] as bool? ?? true; + final resumeCurrent = args as bool? ?? true; if (!resumeCurrent) { // Reset session ID to start a new session posthog?.sessionManager?.resetSessionId(); From c385b6205a2d92c49fb72e17a3cf4cb878b127a9 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 10 Feb 2026 14:39:53 +0200 Subject: [PATCH 3/6] fix: dynamically enable or disable PostHogWidget --- lib/src/posthog.dart | 21 ++++++++++++--- lib/src/posthog_widget.dart | 40 ++++++++++++++++++++++++++--- lib/src/replay/change_detector.dart | 6 ----- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/lib/src/posthog.dart b/lib/src/posthog.dart index aabde069..897e6dbd 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -1,4 +1,4 @@ -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'package:posthog_flutter/src/error_tracking/posthog_error_tracking_autocapture_integration.dart'; import 'package:posthog_flutter/src/error_tracking/posthog_exception.dart'; @@ -14,6 +14,9 @@ class Posthog { PostHogConfig? _config; + @internal + final sessionRecordingActive = ValueNotifier(false); + factory Posthog() { return _instance; } @@ -42,6 +45,10 @@ class Posthog { Future setup(PostHogConfig config) { _config = config; // Store the config + if (config.sessionReplay) { + sessionRecordingActive.value = true; + } + _installFlutterIntegrations(config); return _posthog.setup(config); @@ -200,6 +207,7 @@ class Posthog { Future close() { _config = null; _currentScreen = null; + sessionRecordingActive.value = false; PosthogObserver.clearCurrentContext(); // Uninstall Flutter integrations @@ -217,13 +225,18 @@ class Posthog { /// /// [resumeCurrent] - If true (default), resumes recording of the current session. /// If false, starts a new session and begins recording. - Future startSessionRecording({bool resumeCurrent = true}) => - _posthog.startSessionRecording(resumeCurrent: resumeCurrent); + Future startSessionRecording({bool resumeCurrent = true}) async { + await _posthog.startSessionRecording(resumeCurrent: resumeCurrent); + sessionRecordingActive.value = true; + } /// Stops the current session recording if one is in progress. /// /// This method will have no effect if PostHog is not enabled. - Future stopSessionRecording() => _posthog.stopSessionRecording(); + Future stopSessionRecording() async { + await _posthog.stopSessionRecording(); + sessionRecordingActive.value = false; + } /// Returns whether session replay is currently active. Future isSessionReplayActive() => _posthog.isSessionReplayActive(); diff --git a/lib/src/posthog_widget.dart b/lib/src/posthog_widget.dart index 80bf3aa7..b88af39b 100644 --- a/lib/src/posthog_widget.dart +++ b/lib/src/posthog_widget.dart @@ -31,19 +31,51 @@ class PostHogWidgetState extends State { super.initState(); final config = Posthog().config; - if (config == null || !config.sessionReplay) { + if (config == null) { return; } - _throttleDuration = config.sessionReplayConfig.throttleDelay; + if (config.sessionReplay) { + _initComponents(config); + _changeDetector?.start(); + } + + // start listening for session recording toggles + Posthog().sessionRecordingActive.addListener(_onSessionRecordingChanged); + } + void _initComponents(PostHogConfig config) { + _throttleDuration = config.sessionReplayConfig.throttleDelay; _screenshotCapturer = ScreenshotCapturer(config); _nativeCommunicator = NativeCommunicator(); - _changeDetector = ChangeDetector(_onChangeDetected); + } + + void _onSessionRecordingChanged() { + if (Posthog().sessionRecordingActive.value) { + _startRecording(); + } else { + _stopRecording(); + } + } + + void _startRecording() { + final config = Posthog().config; + if (config == null) { + return; + } + + if (_changeDetector == null) { + _initComponents(config); + } + _changeDetector?.start(); } + void _stopRecording() { + _changeDetector?.stop(); + } + // This works as onRootViewsChangedListeners void _onChangeDetected() { if (_isThrottling) { @@ -97,6 +129,8 @@ class PostHogWidgetState extends State { @override void dispose() { + Posthog().sessionRecordingActive.removeListener(_onSessionRecordingChanged); + _throttleTimer?.cancel(); _throttleTimer = null; _changeDetector?.stop(); diff --git a/lib/src/replay/change_detector.dart b/lib/src/replay/change_detector.dart index ed1a6787..5f280b96 100644 --- a/lib/src/replay/change_detector.dart +++ b/lib/src/replay/change_detector.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; -import 'package:posthog_flutter/posthog_flutter.dart'; /// A class that detects changes in the UI and executes a callback when changes occur. /// @@ -51,11 +50,6 @@ class ChangeDetector { /// Executes the [onChange] callback and schedules itself for the next frame /// if the change detector is still running. void _onFrameRendered() { - final config = Posthog().config; - if (config == null || !config.sessionReplay) { - return; - } - if (!_isRunning) { return; } From c92d893249730de291c3a6275fe904070c12f8c6 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 10 Feb 2026 14:47:25 +0200 Subject: [PATCH 4/6] fix: skip recording operations for macos --- lib/src/posthog_flutter_io.dart | 6 +++--- lib/src/util/platform_io_real.dart | 4 ++++ lib/src/util/platform_io_stub.dart | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/posthog_flutter_io.dart b/lib/src/posthog_flutter_io.dart index 495f9aa8..70fd22e5 100644 --- a/lib/src/posthog_flutter_io.dart +++ b/lib/src/posthog_flutter_io.dart @@ -661,7 +661,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { @override Future startSessionRecording({bool resumeCurrent = true}) async { - if (!isSupportedPlatform()) { + if (!isSupportedPlatform() || isMacOS()) { return; } @@ -674,7 +674,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { @override Future stopSessionRecording() async { - if (!isSupportedPlatform()) { + if (!isSupportedPlatform() || isMacOS()) { return; } @@ -687,7 +687,7 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface { @override Future isSessionReplayActive() async { - if (!isSupportedPlatform()) { + if (!isSupportedPlatform() || isMacOS()) { return false; } diff --git a/lib/src/util/platform_io_real.dart b/lib/src/util/platform_io_real.dart index 94295579..c6da3748 100644 --- a/lib/src/util/platform_io_real.dart +++ b/lib/src/util/platform_io_real.dart @@ -7,3 +7,7 @@ bool isSupportedPlatform() { } return !(Platform.isLinux || Platform.isWindows); } + +bool isMacOS() { + return Platform.isMacOS; +} diff --git a/lib/src/util/platform_io_stub.dart b/lib/src/util/platform_io_stub.dart index 658d07d5..33472076 100644 --- a/lib/src/util/platform_io_stub.dart +++ b/lib/src/util/platform_io_stub.dart @@ -3,3 +3,7 @@ import 'package:flutter/foundation.dart'; bool isSupportedPlatform() { return kIsWeb; } + +bool isMacOS() { + return false; +} From 3d81bfb31cf17459516d0805ec470ac50b18bc5e Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 10 Feb 2026 18:03:21 +0200 Subject: [PATCH 5/6] fix: prevent external mutations of sessionRecordingActive --- lib/src/posthog.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/posthog.dart b/lib/src/posthog.dart index 897e6dbd..aba18bb5 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -14,8 +14,10 @@ class Posthog { PostHogConfig? _config; + final _sessionRecordingActive = ValueNotifier(false); + @internal - final sessionRecordingActive = ValueNotifier(false); + ValueListenable get sessionRecordingActive => _sessionRecordingActive; factory Posthog() { return _instance; @@ -46,7 +48,7 @@ class Posthog { _config = config; // Store the config if (config.sessionReplay) { - sessionRecordingActive.value = true; + _sessionRecordingActive.value = true; } _installFlutterIntegrations(config); @@ -207,7 +209,7 @@ class Posthog { Future close() { _config = null; _currentScreen = null; - sessionRecordingActive.value = false; + _sessionRecordingActive.value = false; PosthogObserver.clearCurrentContext(); // Uninstall Flutter integrations @@ -227,7 +229,7 @@ class Posthog { /// If false, starts a new session and begins recording. Future startSessionRecording({bool resumeCurrent = true}) async { await _posthog.startSessionRecording(resumeCurrent: resumeCurrent); - sessionRecordingActive.value = true; + _sessionRecordingActive.value = true; } /// Stops the current session recording if one is in progress. @@ -235,7 +237,7 @@ class Posthog { /// This method will have no effect if PostHog is not enabled. Future stopSessionRecording() async { await _posthog.stopSessionRecording(); - sessionRecordingActive.value = false; + _sessionRecordingActive.value = false; } /// Returns whether session replay is currently active. From d0df77e72077ee360297e93b1c9251dbed0528a6 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 10 Feb 2026 19:01:04 +0200 Subject: [PATCH 6/6] fix: add PostHogInternalEvents --- lib/src/posthog.dart | 14 +++++--------- lib/src/posthog_internal_events.dart | 8 ++++++++ lib/src/posthog_widget.dart | 9 ++++++--- 3 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 lib/src/posthog_internal_events.dart diff --git a/lib/src/posthog.dart b/lib/src/posthog.dart index aba18bb5..339d3131 100644 --- a/lib/src/posthog.dart +++ b/lib/src/posthog.dart @@ -4,6 +4,7 @@ import 'package:posthog_flutter/src/error_tracking/posthog_error_tracking_autoca import 'package:posthog_flutter/src/error_tracking/posthog_exception.dart'; import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; +import 'posthog_internal_events.dart'; import 'posthog_observer.dart'; class Posthog { @@ -14,11 +15,6 @@ class Posthog { PostHogConfig? _config; - final _sessionRecordingActive = ValueNotifier(false); - - @internal - ValueListenable get sessionRecordingActive => _sessionRecordingActive; - factory Posthog() { return _instance; } @@ -48,7 +44,7 @@ class Posthog { _config = config; // Store the config if (config.sessionReplay) { - _sessionRecordingActive.value = true; + PostHogInternalEvents.sessionRecordingActive.value = true; } _installFlutterIntegrations(config); @@ -209,7 +205,7 @@ class Posthog { Future close() { _config = null; _currentScreen = null; - _sessionRecordingActive.value = false; + PostHogInternalEvents.sessionRecordingActive.value = false; PosthogObserver.clearCurrentContext(); // Uninstall Flutter integrations @@ -229,7 +225,7 @@ class Posthog { /// If false, starts a new session and begins recording. Future startSessionRecording({bool resumeCurrent = true}) async { await _posthog.startSessionRecording(resumeCurrent: resumeCurrent); - _sessionRecordingActive.value = true; + PostHogInternalEvents.sessionRecordingActive.value = true; } /// Stops the current session recording if one is in progress. @@ -237,7 +233,7 @@ class Posthog { /// This method will have no effect if PostHog is not enabled. Future stopSessionRecording() async { await _posthog.stopSessionRecording(); - _sessionRecordingActive.value = false; + PostHogInternalEvents.sessionRecordingActive.value = false; } /// Returns whether session replay is currently active. diff --git a/lib/src/posthog_internal_events.dart b/lib/src/posthog_internal_events.dart new file mode 100644 index 00000000..98992efc --- /dev/null +++ b/lib/src/posthog_internal_events.dart @@ -0,0 +1,8 @@ +import 'package:flutter/foundation.dart'; + +@internal +class PostHogInternalEvents { + PostHogInternalEvents._(); // private init + + static final sessionRecordingActive = ValueNotifier(false); +} diff --git a/lib/src/posthog_widget.dart b/lib/src/posthog_widget.dart index b88af39b..72e9174e 100644 --- a/lib/src/posthog_widget.dart +++ b/lib/src/posthog_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:posthog_flutter/posthog_flutter.dart'; +import 'package:posthog_flutter/src/posthog_internal_events.dart'; import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart'; import 'replay/change_detector.dart'; @@ -41,7 +42,8 @@ class PostHogWidgetState extends State { } // start listening for session recording toggles - Posthog().sessionRecordingActive.addListener(_onSessionRecordingChanged); + PostHogInternalEvents.sessionRecordingActive + .addListener(_onSessionRecordingChanged); } void _initComponents(PostHogConfig config) { @@ -52,7 +54,7 @@ class PostHogWidgetState extends State { } void _onSessionRecordingChanged() { - if (Posthog().sessionRecordingActive.value) { + if (PostHogInternalEvents.sessionRecordingActive.value) { _startRecording(); } else { _stopRecording(); @@ -129,7 +131,8 @@ class PostHogWidgetState extends State { @override void dispose() { - Posthog().sessionRecordingActive.removeListener(_onSessionRecordingChanged); + PostHogInternalEvents.sessionRecordingActive + .removeListener(_onSessionRecordingChanged); _throttleTimer?.cancel(); _throttleTimer = null;