Skip to content
Merged
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-in-new-session-when-replay-wanted.md
Original file line number Diff line number Diff line change
@@ -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()`.
20 changes: 16 additions & 4 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -2552,8 +2562,10 @@ public class PostHog private constructor(

it.start(true)
} else {
endSession()
startSession()
if (rotateSession) {
endSession()
startSession()
}

if (!shouldRecordSession()) {
return
Expand Down
40 changes: 40 additions & 0 deletions posthog/src/test/java/com/posthog/PostHogTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>())

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()
Expand Down
Loading