Skip to content

Commit

Permalink
Cache and read feature flags on the disk (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Nov 14, 2023
1 parent 0e1fb69 commit 7decf9e
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Next

- Expose and allow to enable and disable the debug mode at runtime ([#60](https://github.com/PostHog/posthog-android/pull/60))
- Cache and read feature flags on the disk ([#61](https://github.com/PostHog/posthog-android/pull/61))
- Pick up consumer proguard rules correctly ([#62](https://github.com/PostHog/posthog-android/pull/62))

## 3.0.0-beta.4 - 2023-11-08
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ tasks.withType<KotlinCompile>().configureEach {
dependencies {
// also update PosthogBuildConfig.Kotlin.KOTLIN
val kotlinVersion = "1.8.10"
implementation("com.android.tools.build:gradle:8.1.1")
implementation("com.android.tools.build:gradle:8.1.3")
// kotlin version has to match kotlinCompilerExtensionVersion
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")

Expand All @@ -31,5 +31,5 @@ dependencies {
implementation("io.github.gradle-nexus:publish-plugin:1.3.0")

// tests
implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.7.3")
implementation("org.jetbrains.kotlinx:kover-gradle-plugin:0.7.4")
}
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
15 changes: 10 additions & 5 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Expand Down Expand Up @@ -133,10 +131,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi

# Increase the maximum file descriptors if we can.
Expand Down Expand Up @@ -197,6 +198,10 @@ if "$cygwin" || "$msys" ; then
done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
Expand Down
2 changes: 1 addition & 1 deletion posthog-android/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<issue
id="GradleDependency"
message="A newer version of org.mockito.kotlin:mockito-kotlin than 4.1.0 is available: 5.1.0"
message="A newer version of org.mockito.kotlin:mockito-kotlin than 4.1.0 is available: 5.0.0"
errorLine1=" testImplementation(&quot;org.mockito.kotlin:mockito-kotlin:${PosthogBuildConfig.Dependencies.MOCKITO}&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
Expand Down
44 changes: 39 additions & 5 deletions posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.posthog.internal

import com.posthog.PostHogConfig
import com.posthog.PostHogOnFeatureFlags
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS_PAYLOAD
import java.util.concurrent.ExecutorService
import java.util.concurrent.atomic.AtomicBoolean

Expand Down Expand Up @@ -64,12 +66,16 @@ internal class PostHogFeatureFlags(
this.featureFlagPayloads = normalizedPayloads
}
}
config.cachePreferences?.let { preferences ->
val flags = this.featureFlags ?: mapOf()
preferences.setValue(FEATURE_FLAGS, flags)

val payloads = this.featureFlagPayloads ?: mapOf()
preferences.setValue(FEATURE_FLAGS_PAYLOAD, payloads)
}
isFeatureFlagsLoaded = true
} ?: run {
isFeatureFlagsLoaded = false
}
} catch (e: Throwable) {
isFeatureFlagsLoaded = false
config.logger.log("Loading feature flags failed: $e")
} finally {
try {
Expand All @@ -83,6 +89,29 @@ internal class PostHogFeatureFlags(
}
}

private fun loadFeatureFlagsFromCache() {
config.cachePreferences?.let { preferences ->
@Suppress("UNCHECKED_CAST")
val flags = preferences.getValue(
FEATURE_FLAGS,
mapOf<String, Any>(),
) as? Map<String, Any> ?: mapOf()

@Suppress("UNCHECKED_CAST")
val payloads = preferences.getValue(
FEATURE_FLAGS_PAYLOAD,
mapOf<String, Any?>(),
) as? Map<String, Any?> ?: mapOf()

synchronized(featureFlagsLock) {
this.featureFlags = flags
this.featureFlagPayloads = payloads

isFeatureFlagsLoaded = true
}
}
}

private fun normalizePayloads(featureFlagPayloads: Map<String, Any?>?): Map<String, Any?> {
val parsedPayloads = (featureFlagPayloads ?: mapOf()).toMutableMap()

Expand All @@ -106,7 +135,7 @@ internal class PostHogFeatureFlags(

fun isFeatureEnabled(key: String, defaultValue: Boolean): Boolean {
if (!isFeatureFlagsLoaded) {
return defaultValue
loadFeatureFlagsFromCache()
}
val value: Any?

Expand All @@ -128,7 +157,7 @@ internal class PostHogFeatureFlags(

private fun readFeatureFlag(key: String, defaultValue: Any?, flags: Map<String, Any?>?): Any? {
if (!isFeatureFlagsLoaded) {
return defaultValue
loadFeatureFlagsFromCache()
}
val value: Any?

Expand Down Expand Up @@ -159,6 +188,11 @@ internal class PostHogFeatureFlags(
synchronized(featureFlagsLock) {
featureFlags = null
featureFlagPayloads = null

config.cachePreferences?.let { preferences ->
preferences.remove(FEATURE_FLAGS)
preferences.remove(FEATURE_FLAGS_PAYLOAD)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public interface PostHogPreferences {
internal const val ANONYMOUS_ID = "anonymousId"
internal const val DISTINCT_ID = "distinctId"
internal const val OPT_OUT = "opt-out"
internal const val FEATURE_FLAGS = "featureFlags"
internal const val FEATURE_FLAGS_PAYLOAD = "featureFlagsPayload"
public const val VERSION: String = "version"
public const val BUILD: String = "build"
public const val STRINGIFIED_KEYS: String = "stringifiedKeys"
Expand All @@ -32,6 +34,8 @@ public interface PostHogPreferences {
ANONYMOUS_ID,
DISTINCT_ID,
OPT_OUT,
FEATURE_FLAGS,
FEATURE_FLAGS_PAYLOAD,
VERSION,
BUILD,
STRINGIFIED_KEYS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ package com.posthog.internal
import com.posthog.PostHogConfig
import com.posthog.apiKey
import com.posthog.awaitExecution
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS_PAYLOAD
import com.posthog.mockHttp
import com.posthog.shutdownAndAwaitTermination
import okhttp3.mockwebserver.MockResponse
import java.io.File
import java.util.concurrent.Executors
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

Expand All @@ -19,19 +23,26 @@ internal class PostHogFeatureFlagsTest {

private val file = File("src/test/resources/json/basic-decide-no-errors.json")
private val responseDecideApi = file.readText()
private val preferences = PostHogMemoryPreferences()

private fun getSut(
host: String,
networkStatus: PostHogNetworkStatus? = null,
): PostHogFeatureFlags {
val config = PostHogConfig(apiKey, host).apply {
this.networkStatus = networkStatus
cachePreferences = preferences
}
val dateProvider = PostHogCalendarDateProvider()
val api = PostHogApi(config, dateProvider)
return PostHogFeatureFlags(config, api, executor = executor)
}

@BeforeTest
fun `set up`() {
preferences.clear()
}

@Test
fun `load flags bails out if not connected`() {
val http = mockHttp(
Expand Down Expand Up @@ -91,10 +102,14 @@ internal class PostHogFeatureFlagsTest {
executor.shutdownAndAwaitTermination()

assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false) as Boolean)
assertNotNull(preferences.getValue(FEATURE_FLAGS))
assertNotNull(preferences.getValue(FEATURE_FLAGS_PAYLOAD))

sut.clear()

assertNull(sut.getFeatureFlags())
assertNull(preferences.getValue(FEATURE_FLAGS))
assertNull(preferences.getValue(FEATURE_FLAGS_PAYLOAD))
}

@Test
Expand Down Expand Up @@ -222,4 +237,48 @@ internal class PostHogFeatureFlagsTest {
assertNull(sut.getFeatureFlagPayload("theNull", defaultValue = null))
assertEquals("[1, 2", sut.getFeatureFlagPayload("theBroken", defaultValue = null) as String)
}

@Test
fun `cache feature flags after loading from the network`() {
// preload items
preferences.setValue(FEATURE_FLAGS, mapOf("foo" to true))
preferences.setValue(FEATURE_FLAGS_PAYLOAD, mapOf("foo" to true))

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

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

assertTrue(sut.isFeatureEnabled("foo", defaultValue = false))
assertTrue(sut.getFeatureFlagPayload("foo", defaultValue = false) as Boolean)
}

@Test
fun `load feature flags from cache if not loaded from the network yet`() {
val http = mockHttp(
response =
MockResponse()
.setBody(responseDecideApi),
)
val url = http.url("/")

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

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

executor.shutdownAndAwaitTermination()

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

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

assertTrue(flags?.get("4535-funnel-bar-viz") as Boolean)
assertTrue(payloads?.get("thePayload") as Boolean)
}
}

0 comments on commit 7decf9e

Please sign in to comment.