diff --git a/.changeset/opt-in-new-session-when-replay-wanted.md b/.changeset/opt-in-new-session-when-replay-wanted.md new file mode 100644 index 000000000..fafbc947a --- /dev/null +++ b/.changeset/opt-in-new-session-when-replay-wanted.md @@ -0,0 +1,6 @@ +--- +"posthog": patch +"posthog-android": patch +--- + +Fix session replay recording into the opted-out session when a linked feature flag or pending remote config delayed its restart after `optIn()`. diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index 281503ecb..6b35d791a 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -458,7 +458,8 @@ public class PostHog private constructor( sessionReplayHandler = it if (startSessionReplay) { - startSessionReplayInternal(resumeCurrent = resumeSessionReplay) + // setup() resumes the current session and optIn() has already started a new one + startSessionReplayInternal(resumeCurrent = resumeSessionReplay, rotateSession = false) } } else if (it is PostHogSurveysHandler) { surveysHandler = it @@ -1364,6 +1365,12 @@ public class PostHog private constructor( } if (wasOptedOut) { + // rotate even if the linked flag or remote config then holds replay off, so a later start + // doesn't resume the session that was active while opted out + if (sessionReplayWanted) { + PostHogSessionManager.endSession() + PostHogSessionManager.startSession() + } installIntegrations(resumeSessionReplay = false, startSessionReplay = sessionReplayWanted, surveys = surveys) // install re-arms each integration from local config; re-apply the cached remote // config (e.g. exception autocapture turned off remotely) instead of waiting for a reload @@ -2527,7 +2534,10 @@ public class PostHog private constructor( } } - private fun startSessionReplayInternal(resumeCurrent: Boolean) { + private fun startSessionReplayInternal( + resumeCurrent: Boolean, + rotateSession: Boolean = !resumeCurrent, + ) { if (!isEnabled()) { return } @@ -2552,8 +2562,10 @@ public class PostHog private constructor( it.start(true) } else { - endSession() - startSession() + if (rotateSession) { + endSession() + startSession() + } if (!shouldRecordSession()) { return diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index 01286caba..10d06b33c 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -1508,6 +1508,46 @@ internal class PostHogTest { sut.close() } + @Test + fun `optIn starts a new session when replay is wanted but its flag holds it off`() { + val http = mockHttp() + val url = http.url("/") + val integration = PostHogSessionReplayHandlerFake(false) + + val sut = getSut(url.toString(), sessionReplay = true, preloadFeatureFlags = false, integration = integration) + val sessionAtOptOut = sut.getSessionId() + + sut.optOut() + integration.reset() + sut.optIn() + + assertFalse(integration.startCalled) + assertNotEquals(sessionAtOptOut, sut.getSessionId()) + + sut.close() + } + + @Test + fun `optIn keeps the session when the app stopped replay before opting out`() { + val http = mockHttp() + val url = http.url("/") + val integration = PostHogSessionReplayHandlerFake(false) + val myPrefs = PostHogMemoryPreferences() + myPrefs.setValue(SESSION_REPLAY, emptyMap()) + + val sut = + getSut(url.toString(), sessionReplay = true, cachePreferences = myPrefs, preloadFeatureFlags = false, integration = integration) + sut.stopSessionReplay() + val sessionAtOptOut = sut.getSessionId() + + sut.optOut() + sut.optIn() + + assertEquals(sessionAtOptOut, sut.getSessionId()) + + sut.close() + } + @Test fun `flags-loaded replay start can't land after a concurrent optOut`() { val http = mockHttp()