Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache and read feature flags on the disk #61

Merged
merged 4 commits into from
Nov 14, 2023
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
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
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)
}
}