diff --git a/.changeset/refresh-parsers-native.md b/.changeset/refresh-parsers-native.md new file mode 100644 index 00000000..7a2850e7 --- /dev/null +++ b/.changeset/refresh-parsers-native.md @@ -0,0 +1,5 @@ +--- +"posthog_flutter": patch +--- + +Fix session replay masking on iOS and Android ignoring changed `maskAllTexts`/`maskAllImages` flags when `setup()` is called again diff --git a/posthog_flutter/lib/src/posthog.dart b/posthog_flutter/lib/src/posthog.dart index b38368e9..eb67afc9 100644 --- a/posthog_flutter/lib/src/posthog.dart +++ b/posthog_flutter/lib/src/posthog.dart @@ -12,6 +12,7 @@ import 'posthog_config.dart'; import 'posthog_flutter_platform_interface.dart'; import 'posthog_internal_events.dart'; import 'posthog_observer.dart'; +import 'replay/mask/posthog_mask_controller.dart'; import 'utils/before_send.dart'; /// Entry point for the PostHog Flutter SDK. @@ -67,6 +68,11 @@ class Posthog { _config = config; // Store the config + // The mask controller singleton may predate this setup() (or a previous + // setup() built it with different masking flags); without a refresh the + // stale parser map would keep deciding what replay masks on every platform. + PostHogMaskController.instance.refreshParsers(config.sessionReplayConfig); + if (config.sessionReplay) { PostHogInternalEvents.sessionRecordingActive.value = true; } diff --git a/posthog_flutter/lib/src/replay/screenshot/screenshot_capturer.dart b/posthog_flutter/lib/src/replay/screenshot/screenshot_capturer.dart index 7bae3cdb..46012b23 100644 --- a/posthog_flutter/lib/src/replay/screenshot/screenshot_capturer.dart +++ b/posthog_flutter/lib/src/replay/screenshot/screenshot_capturer.dart @@ -81,6 +81,12 @@ class ScreenshotCapturer { ScreenshotCapturer(this._config); + /// A second `setup()` replaces the SDK config object, but the capturer is + /// built once per widget lifecycle — resolving the live config at capture + /// time keeps masking flags from freezing at their first-setup values. + @visibleForTesting + PostHogConfig get effectiveConfig => Posthog().config ?? _config; + void cancel() { _cancelled = true; } @@ -448,7 +454,7 @@ class ScreenshotCapturer { srcHeight: srcHeight, ); - final replayConfig = _config.sessionReplayConfig; + final replayConfig = effectiveConfig.sessionReplayConfig; final postHogWidgetWrapperElements = PostHogMaskController.instance.getPostHogWidgetWrapperElements(); diff --git a/posthog_flutter/test/posthog_test.dart b/posthog_flutter/test/posthog_test.dart index d4ffa76c..e5050eb2 100644 --- a/posthog_flutter/test/posthog_test.dart +++ b/posthog_flutter/test/posthog_test.dart @@ -5,6 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:posthog_flutter/posthog_flutter.dart'; import 'package:posthog_flutter/src/posthog_flutter_platform_interface.dart'; import 'package:posthog_flutter/src/posthog_internal_events.dart'; +import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart'; +import 'package:posthog_flutter/src/replay/screenshot/screenshot_capturer.dart'; import 'posthog_flutter_platform_interface_fake.dart'; @@ -40,6 +42,44 @@ void main() { }, ); + test( + 'setup after close with different masking flags rebuilds the parser map', + () async { + final controller = PostHogMaskController.instance; + addTearDown(() => controller.refreshParsers(null)); + + final imagesMasked = PostHogConfig('test_project_token'); + await Posthog().setup(imagesMasked); + expect(controller.parsers.keys, contains('RenderImage')); + + await Posthog().close(); + final imagesUnmasked = PostHogConfig('test_project_token') + ..sessionReplayConfig.maskAllImages = false; + await Posthog().setup(imagesUnmasked); + expect(controller.parsers.keys, isNot(contains('RenderImage'))); + expect(controller.parsers.keys, contains('RenderParagraph')); + }, + ); + + test( + 'screenshot capturer resolves the live config after close and re-setup', + () async { + final first = PostHogConfig('test_project_token') + ..sessionReplayConfig.maskAllImages = false; + await Posthog().setup(first); + // PostHogWidget builds its capturer once and keeps it across a + // close()/setup() reconfigure, so the capturer must follow the live + // config rather than the one it was constructed with. + final capturer = ScreenshotCapturer(first); + expect(capturer.effectiveConfig, same(first)); + + await Posthog().close(); + final second = PostHogConfig('test_project_token'); + await Posthog().setup(second); + expect(capturer.effectiveConfig, same(second)); + }, + ); + group('setup with blank project token', () { const blankProjectTokens = { 'empty string': '',