Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/opt-out-uninstall-integrations.md
Original file line number Diff line number Diff line change
@@ -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.
90 changes: 49 additions & 41 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
}

/**
Expand Down
20 changes: 20 additions & 0 deletions posthog/src/test/java/com/posthog/PostHogTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down