From fc37d68c96c56340329d19b19307f6aa558f2399 Mon Sep 17 00:00:00 2001 From: Omid Mirzaei Date: Sun, 4 Oct 2026 13:03:22 +0400 Subject: [PATCH] fix: uninstall integrations when the user opts out Tear down autocapture and session replay while opted out, then reinstall on opt-in with a new replay session. Fixes #242. --- .changeset/opt-out-uninstall-integrations.md | 6 ++ posthog/src/main/java/com/posthog/PostHog.kt | 90 ++++++++++--------- .../src/test/java/com/posthog/PostHogTest.kt | 20 +++++ 3 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 .changeset/opt-out-uninstall-integrations.md diff --git a/.changeset/opt-out-uninstall-integrations.md b/.changeset/opt-out-uninstall-integrations.md new file mode 100644 index 000000000..2cbdc943a --- /dev/null +++ b/.changeset/opt-out-uninstall-integrations.md @@ -0,0 +1,6 @@ +--- +"posthog": patch +"posthog-android": patch +--- + +Uninstall capture integrations on opt-out and reinstall them on opt-in, starting session replay with a fresh session after the user opts back in. diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index b74829757..e7eed9eaf 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -350,32 +350,7 @@ public class PostHog private constructor( startSession() - config.integrations.forEach { - try { - it.install(this) - - if (it is PostHogSessionReplayHandler) { - sessionReplayHandler = it - - // resume because we just created the session id above with - // the startSession call - if (isSessionReplayConfigEnabled()) { - startSessionReplay(resumeCurrent = true) - } - } else if (it is PostHogSurveysHandler) { - // surveys integration so we can notify it about captured events - surveysHandler = it - // Immediately push any cached surveys from remote config - try { - remoteConfig?.getSurveys()?.let(it::onSurveysLoaded) - } catch (e: Throwable) { - config.logger.log("Pushing cached surveys to integration failed: $e.") - } - } - } catch (e: Throwable) { - config.logger.log("Integration ${it.javaClass.name} failed to install: $e.") - } - } + installIntegrations(resumeSessionReplay = true) // only because of testing in isolation, this flag is always enabled @Suppress("DEPRECATION") @@ -422,6 +397,50 @@ public class PostHog private constructor( } } + private fun installIntegrations(resumeSessionReplay: Boolean) { + val config = config ?: return + config.integrations.forEach { + try { + it.install(this) + + if (it is PostHogSessionReplayHandler) { + sessionReplayHandler = it + + if (isSessionReplayConfigEnabled()) { + startSessionReplay(resumeCurrent = resumeSessionReplay) + } + } else if (it is PostHogSurveysHandler) { + surveysHandler = it + try { + remoteConfig?.getSurveys()?.let(it::onSurveysLoaded) + } catch (e: Throwable) { + config.logger.log("Pushing cached surveys to integration failed: $e.") + } + } + } catch (e: Throwable) { + config.logger.log("Integration ${it.javaClass.name} failed to install: $e.") + } + } + } + + private fun uninstallIntegrations() { + stopSessionReplay() + val config = config ?: return + config.integrations.forEach { + try { + it.uninstall() + + if (it is PostHogSessionReplayHandler) { + sessionReplayHandler = null + } else if (it is PostHogSurveysHandler) { + surveysHandler = null + } + } catch (e: Throwable) { + config.logger.log("Integration ${it.javaClass.name} failed to uninstall: $e.") + } + } + } + private fun legacyPreferences( config: PostHogConfig, serializer: PostHogSerializer, @@ -545,21 +564,7 @@ public class PostHog private constructor( config?.let { config -> apiKeys.remove(config.apiKey) - - config.integrations.forEach { - try { - it.uninstall() - - if (it is PostHogSessionReplayHandler) { - sessionReplayHandler = null - } else if (it is PostHogSurveysHandler) { - surveysHandler = null - } - } catch (e: Throwable) { - config.logger - .log("Integration ${it.javaClass.name} failed to uninstall: $e.") - } - } + uninstallIntegrations() } queue?.stop() @@ -1226,6 +1231,8 @@ public class PostHog private constructor( optOutLoaded = true } + installIntegrations(resumeSessionReplay = false) + // Re-arm integrations that stood down while opted out (e.g. push refetches the token and // re-registers, since a logout unregister cleared it and opt-in alone leaves it unsubscribed). // Gated on the opt-in capability interface so the public PostHogIntegration contract is unchanged. @@ -1255,6 +1262,7 @@ public class PostHog private constructor( // Clear cached identity-token state so a stale token/401 flag isn't reused after // re-opting-in; the send guard in the manager already blocks sends while opted out. pushSubscriptionManager?.onOptOut() + uninstallIntegrations() } /** diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index 90f52559e..9cbcbb4ab 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -1305,6 +1305,26 @@ internal class PostHogTest { sut.optIn() assertEquals(1, integration.onOptInCount) + assertTrue(integration.installed) + + sut.close() + } + + @Test + fun `optOut uninstalls integrations and optIn reinstalls them`() { + val http = mockHttp() + val url = http.url("/") + val integration = FakePostHogIntegration() + + val sut = getSut(url.toString(), integration = integration, preloadFeatureFlags = false) + + assertTrue(integration.installed) + + sut.optOut() + assertFalse(integration.installed) + + sut.optIn() + assertTrue(integration.installed) sut.close() }