Skip to content

Commit

Permalink
recording: session replay respect feature flag variants (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Oct 14, 2024
1 parent d770f8b commit 4078dbe
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- recording: session replay respect feature flag variants ([#197](https://github.com/PostHog/posthog-android/pull/197))

## 3.8.1 - 2024-10-09

- recording: `OnTouchEventListener` try catch guard to swallow unexpected errors take 2 ([#196](https://github.com/PostHog/posthog-android/pull/196))
Expand Down
66 changes: 53 additions & 13 deletions posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ internal class PostHogFeatureFlags(
preloadSessionReplayFlag()
}

private fun isRecordingActive(
featureFlags: Map<String, Any>,
sessionRecording: Map<String, Any>,
): Boolean {
var recordingActive = true

// Check for boolean flags
val linkedFlag = sessionRecording["linkedFlag"]
if (linkedFlag is String) {
val value = featureFlags[linkedFlag]
if (value is Boolean) {
recordingActive = value
}
} else if (linkedFlag is Map<*, *>) {
// Check for specific flag variant
val flag = linkedFlag["flag"] as? String
val variant = linkedFlag["variant"] as? String
if (flag != null && variant != null) {
val value = featureFlags[flag] as? String
recordingActive = value == variant
}
}
// check for multi flag variant (any)
// val linkedFlag = sessionRecording["linkedFlag"] as? String,
// featureFlags[linkedFlag] != nil
// is also a valid check but since we cannot check the value of the flag,
// we consider session recording is active

return recordingActive
}

fun loadFeatureFlags(
distinctId: String,
anonymousId: String?,
Expand Down Expand Up @@ -74,26 +105,30 @@ internal class PostHogFeatureFlags(
this.featureFlagPayloads = normalizedPayloads
}

when (response.sessionRecording) {
when (val sessionRecording = response.sessionRecording) {
is Boolean -> {
// if sessionRecording is a Boolean, its always disabled
// so we don't enable sessionReplayFlagActive here
sessionReplayFlagActive = false
sessionReplayFlagActive = sessionRecording

config.cachePreferences?.remove(SESSION_REPLAY)
if (!sessionRecording) {
config.cachePreferences?.remove(SESSION_REPLAY)
} else {
// do nothing
}
}

is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
(response.sessionRecording as? Map<String, Any?>)?.let { sessionRecording ->
(sessionRecording as? Map<String, Any>)?.let {
// keeps the value from config.sessionReplay since having sessionRecording
// means its enabled on the project settings, but its only enabled
// when local config.sessionReplay is also enabled
config.snapshotEndpoint = sessionRecording["endpoint"] as? String
config.snapshotEndpoint = it["endpoint"] as? String
?: config.snapshotEndpoint

sessionReplayFlagActive = true
config.cachePreferences?.setValue(SESSION_REPLAY, sessionRecording)
sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it)
config.cachePreferences?.setValue(SESSION_REPLAY, it)

// TODO:
// consoleLogRecordingEnabled -> Boolean or null
Expand Down Expand Up @@ -131,14 +166,19 @@ internal class PostHogFeatureFlags(

private fun preloadSessionReplayFlag() {
synchronized(featureFlagsLock) {
@Suppress("UNCHECKED_CAST")
val sessionRecording = config.cachePreferences?.getValue(SESSION_REPLAY) as? Map<String, Any>
config.cachePreferences?.let { preferences ->
@Suppress("UNCHECKED_CAST")
val sessionRecording = preferences.getValue(SESSION_REPLAY) as? Map<String, Any>

@Suppress("UNCHECKED_CAST")
val flags = preferences.getValue(FEATURE_FLAGS) as? Map<String, Any>

if (sessionRecording != null) {
sessionReplayFlagActive = true
if (sessionRecording != null) {
sessionReplayFlagActive = isRecordingActive(flags ?: mapOf(), sessionRecording)

config.snapshotEndpoint = sessionRecording["endpoint"] as? String
?: config.snapshotEndpoint
config.snapshotEndpoint = sessionRecording["endpoint"] as? String
?: config.snapshotEndpoint
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,96 @@ internal class PostHogFeatureFlagsTest {
assertTrue(sut.isSessionReplayFlagActive())
assertEquals("/b/", config?.snapshotEndpoint)
}

@Test
fun `returns isSessionReplayFlagActive true if bool linked flag is enabled`() {
val file = File("src/test/resources/json/basic-decide-recording-bool-linked-enabled.json")

val http =
mockHttp(
response =
MockResponse()
.setBody(file.readText()),
)
val url = http.url("/")

val sut = getSut(host = url.toString())

sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)

executor.shutdownAndAwaitTermination()

assertTrue(sut.isSessionReplayFlagActive())

sut.clear()
}

@Test
fun `returns isSessionReplayFlagActive false if bool linked flag is disabled`() {
val file = File("src/test/resources/json/basic-decide-recording-bool-linked-disabled.json")

val http =
mockHttp(
response =
MockResponse()
.setBody(file.readText()),
)
val url = http.url("/")

val sut = getSut(host = url.toString())

sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)

executor.shutdownAndAwaitTermination()

assertFalse(sut.isSessionReplayFlagActive())

sut.clear()
}

@Test
fun `returns isSessionReplayFlagActive true if multi variant linked flag is a match`() {
val file = File("src/test/resources/json/basic-decide-recording-bool-linked-variant-match.json")

val http =
mockHttp(
response =
MockResponse()
.setBody(file.readText()),
)
val url = http.url("/")

val sut = getSut(host = url.toString())

sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)

executor.shutdownAndAwaitTermination()

assertTrue(sut.isSessionReplayFlagActive())

sut.clear()
}

@Test
fun `returns isSessionReplayFlagActive false if multi variant linked flag is not a match`() {
val file = File("src/test/resources/json/basic-decide-recording-bool-linked-variant-not-match.json")

val http =
mockHttp(
response =
MockResponse()
.setBody(file.readText()),
)
val url = http.url("/")

val sut = getSut(host = url.toString())

sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)

executor.shutdownAndAwaitTermination()

assertFalse(sut.isSessionReplayFlagActive())

sut.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"autocaptureExceptions": false,
"toolbarParams": {},
"errorsWhileComputingFlags": false,
"capturePerformance": true,
"autocapture_opt_out": false,
"isAuthenticated": false,
"supportedCompression": [
"gzip",
"gzip-js"
],
"config": {
"enable_collect_everything": true
},
"featureFlagPayloads": {
"thePayload": true
},
"featureFlags": {
"4535-funnel-bar-viz": true,
"session-replay-flag": false
},
"sessionRecording": {
"endpoint": "/b/",
"linkedFlag": "session-replay-flag"
},
"siteApps": [
{
"id": 21039.0,
"url": "/site_app/21039/EOsOSePYNyTzHkZ3f4mjrjUap8Hy8o2vUTAc6v1ZMFP/576ac89bc8aed72a21d9b19221c2c626/"
}
],
"editorParams": {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"autocaptureExceptions": false,
"toolbarParams": {},
"errorsWhileComputingFlags": false,
"capturePerformance": true,
"autocapture_opt_out": false,
"isAuthenticated": false,
"supportedCompression": [
"gzip",
"gzip-js"
],
"config": {
"enable_collect_everything": true
},
"featureFlagPayloads": {
"thePayload": true
},
"featureFlags": {
"4535-funnel-bar-viz": true,
"session-replay-flag": true
},
"sessionRecording": {
"endpoint": "/b/",
"linkedFlag": "session-replay-flag"
},
"siteApps": [
{
"id": 21039.0,
"url": "/site_app/21039/EOsOSePYNyTzHkZ3f4mjrjUap8Hy8o2vUTAc6v1ZMFP/576ac89bc8aed72a21d9b19221c2c626/"
}
],
"editorParams": {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"autocaptureExceptions": false,
"toolbarParams": {},
"errorsWhileComputingFlags": false,
"capturePerformance": true,
"autocapture_opt_out": false,
"isAuthenticated": false,
"supportedCompression": [
"gzip",
"gzip-js"
],
"config": {
"enable_collect_everything": true
},
"featureFlagPayloads": {
"thePayload": true
},
"featureFlags": {
"4535-funnel-bar-viz": true,
"session-replay-flag": "variant-1"
},
"sessionRecording": {
"endpoint": "/b/",
"linkedFlag": {
"flag": "session-replay-flag",
"variant": "variant-1"
}
},
"siteApps": [
{
"id": 21039.0,
"url": "/site_app/21039/EOsOSePYNyTzHkZ3f4mjrjUap8Hy8o2vUTAc6v1ZMFP/576ac89bc8aed72a21d9b19221c2c626/"
}
],
"editorParams": {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"autocaptureExceptions": false,
"toolbarParams": {},
"errorsWhileComputingFlags": false,
"capturePerformance": true,
"autocapture_opt_out": false,
"isAuthenticated": false,
"supportedCompression": [
"gzip",
"gzip-js"
],
"config": {
"enable_collect_everything": true
},
"featureFlagPayloads": {
"thePayload": true
},
"featureFlags": {
"4535-funnel-bar-viz": true,
"session-replay-flag": "variant-2"
},
"sessionRecording": {
"endpoint": "/b/",
"linkedFlag": {
"flag": "session-replay-flag",
"variant": "variant-1"
}
},
"siteApps": [
{
"id": 21039.0,
"url": "/site_app/21039/EOsOSePYNyTzHkZ3f4mjrjUap8Hy8o2vUTAc6v1ZMFP/576ac89bc8aed72a21d9b19221c2c626/"
}
],
"editorParams": {

}
}

0 comments on commit 4078dbe

Please sign in to comment.