From 87f9554e1eae199f4cddeb9cb5e63537fe431ca6 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 3 May 2024 14:57:24 +0200 Subject: [PATCH] sync flags --- .../java/com/posthog/android/PostHogFake.kt | 4 + .../src/main/java/Main.kt | 7 +- posthog/api/posthog.api | 32 +++---- posthog/src/main/java/com/posthog/PostHog.kt | 85 +++++++++++++++--- .../main/java/com/posthog/PostHogInterface.kt | 4 + .../posthog/internal/PostHogFeatureFlags.kt | 85 +++++++++--------- .../internal/PostHogFeatureFlagsInterface.kt | 84 +++++++++++++++++ .../internal/PostHogFeatureFlagsSync.kt | 90 +++++++++++++++++++ 8 files changed, 320 insertions(+), 71 deletions(-) create mode 100644 posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt create mode 100644 posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt diff --git a/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt b/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt index cd6d6324..1834b3ca 100644 --- a/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt +++ b/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt @@ -22,6 +22,7 @@ public class PostHogFake : PostHogInterface { properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, + groups: Map?, groupProperties: Map?, ) { this.event = event @@ -43,6 +44,7 @@ public class PostHogFake : PostHogInterface { key: String, defaultValue: Boolean, distinctId: String?, + groups: Map?, ): Boolean { return false } @@ -51,6 +53,7 @@ public class PostHogFake : PostHogInterface { key: String, defaultValue: Any?, distinctId: String?, + groups: Map?, ): Any? { return null } @@ -59,6 +62,7 @@ public class PostHogFake : PostHogInterface { key: String, defaultValue: Any?, distinctId: String?, + groups: Map?, ): Any? { return null } diff --git a/posthog-samples/posthog-console-sample/src/main/java/Main.kt b/posthog-samples/posthog-console-sample/src/main/java/Main.kt index a6989c1a..701d1b00 100644 --- a/posthog-samples/posthog-console-sample/src/main/java/Main.kt +++ b/posthog-samples/posthog-console-sample/src/main/java/Main.kt @@ -10,11 +10,12 @@ public fun main() { } PostHog.setup(config) - PostHog.capture("Hello World!", distinctId = "123") +// PostHog.capture("Hello World!", distinctId = "123") + PostHog.isFeatureEnabled("myFlag", defaultValue = false, distinctId = "123") -// PostHog.flush() + PostHog.flush() // -// PostHog.close() + PostHog.close() while (Thread.activeCount() > 1) { println("threads still active") diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index 2ef7580e..254a8349 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -2,18 +2,18 @@ public final class com/posthog/PostHog : com/posthog/PostHogInterface { public static final field Companion Lcom/posthog/PostHog$Companion; public synthetic fun (Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun alias (Ljava/lang/String;Ljava/lang/String;)V - public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V public fun close ()V public fun debug (Z)V public fun distinctId ()Ljava/lang/String; public fun endSession ()V public fun flush ()V public fun getConfig ()Lcom/posthog/PostHogConfig; - public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; - public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; + public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;)Z + public fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;)Z public fun isOptOut ()Z public fun isSessionActive ()Z public fun optIn ()V @@ -29,18 +29,18 @@ public final class com/posthog/PostHog : com/posthog/PostHogInterface { public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface { public fun alias (Ljava/lang/String;Ljava/lang/String;)V - public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V public fun close ()V public fun debug (Z)V public fun distinctId ()Ljava/lang/String; public fun endSession ()V public fun flush ()V public fun getConfig ()Lcom/posthog/PostHogConfig; - public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; - public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; + public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;)Z + public fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;)Z public fun isOptOut ()Z public fun isSessionActive ()Z public fun optIn ()V @@ -175,18 +175,18 @@ public final class com/posthog/PostHogIntegration$DefaultImpls { public abstract interface class com/posthog/PostHogInterface { public abstract fun alias (Ljava/lang/String;Ljava/lang/String;)V - public abstract fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public abstract fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V public abstract fun close ()V public abstract fun debug (Z)V public abstract fun distinctId ()Ljava/lang/String; public abstract fun endSession ()V public abstract fun flush ()V public abstract fun getConfig ()Lcom/posthog/PostHogConfig; - public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; - public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; + public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; public abstract fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public abstract fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public abstract fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;)Z + public abstract fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;)Z public abstract fun isOptOut ()Z public abstract fun isSessionActive ()Z public abstract fun optIn ()V @@ -202,13 +202,13 @@ public abstract interface class com/posthog/PostHogInterface { public final class com/posthog/PostHogInterface$DefaultImpls { public static synthetic fun alias$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V - public static synthetic fun capture$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V + public static synthetic fun capture$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V public static synthetic fun debug$default (Lcom/posthog/PostHogInterface;ZILjava/lang/Object;)V - public static synthetic fun getFeatureFlag$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun getFeatureFlagPayload$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun getFeatureFlag$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun getFeatureFlagPayload$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun group$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun identify$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V - public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Z + public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Z public static synthetic fun reloadFeatureFlags$default (Lcom/posthog/PostHogInterface;Lcom/posthog/PostHogOnFeatureFlags;ILjava/lang/Object;)V public static synthetic fun screen$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)V } diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index 45bb8dfb..258865fd 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -3,6 +3,8 @@ package com.posthog import com.posthog.internal.PostHogApi import com.posthog.internal.PostHogApiEndpoint import com.posthog.internal.PostHogFeatureFlags +import com.posthog.internal.PostHogFeatureFlagsInterface +import com.posthog.internal.PostHogFeatureFlagsSync import com.posthog.internal.PostHogMemoryPreferences import com.posthog.internal.PostHogPreferences import com.posthog.internal.PostHogPreferences.Companion.ALL_INTERNAL_KEYS @@ -59,7 +61,7 @@ public class PostHog private constructor( private var config: PostHogConfig? = null - private var featureFlags: PostHogFeatureFlags? = null + private var featureFlags: PostHogFeatureFlagsInterface? = null private var queue: PostHogQueue? = null private var replayQueue: PostHogQueue? = null private var memoryPreferences = PostHogMemoryPreferences() @@ -92,9 +94,14 @@ public class PostHog private constructor( } else { replayExecutor.shutdownSafely() cachedEventsExecutor.shutdownSafely() + featureFlagsExecutor.shutdownSafely() } - - val featureFlags = PostHogFeatureFlags(config, api, featureFlagsExecutor) + val featureFlags: PostHogFeatureFlagsInterface = + if (isClientSDK) { + PostHogFeatureFlags(config, api, featureFlagsExecutor) + } else { + PostHogFeatureFlagsSync(config, api) + } // no need to lock optOut here since the setup is locked already val optOut = @@ -262,9 +269,11 @@ public class PostHog private constructor( private fun buildProperties( distinctId: String, + anonymousId: String?, properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, + groups: Map?, groupProperties: Map?, appendSharedProps: Boolean = true, appendGroups: Boolean = true, @@ -289,7 +298,7 @@ public class PostHog private constructor( } if (config?.sendFeatureFlagEvent == true) { - featureFlags?.getFeatureFlags()?.let { + featureFlags?.getFeatureFlags(distinctId, anonymousId = anonymousId, groups = groups)?.let { if (it.isNotEmpty()) { val keys = mutableListOf() for (entry in it.entries) { @@ -378,6 +387,7 @@ public class PostHog private constructor( properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, + groups: Map?, groupProperties: Map?, ) { try { @@ -411,9 +421,11 @@ public class PostHog private constructor( val mergedProperties = buildProperties( newDistinctId, + anonymousId = this.anonymousId, properties = properties, userProperties = userProperties, userPropertiesSetOnce = userPropertiesSetOnce, + groups = groups, groupProperties = groupProperties, // only append shared props if not a snapshot event appendSharedProps = !snapshotEvent, @@ -615,7 +627,7 @@ public class PostHog private constructor( capture(GROUP_IDENTIFY, distinctId = newDistinctId, properties = props) // only because of testing in isolation, this flag is always enabled - if (reloadFeatureFlags && reloadFeatureFlagsIfNewGroup) { + if (reloadFeatureFlags && reloadFeatureFlagsIfNewGroup && isClientSDK()) { loadFeatureFlagsRequest(null) } } @@ -646,11 +658,26 @@ public class PostHog private constructor( key: String, defaultValue: Boolean, distinctId: String?, + groups: Map?, ): Boolean { if (!isEnabled()) { return defaultValue } - return featureFlags?.isFeatureEnabled(key, defaultValue) ?: defaultValue + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("isFeatureEnabled call not allowed, distinctId is invalid: $newDistinctId.") + return defaultValue + } + + return featureFlags?.isFeatureEnabled( + key, + defaultValue, + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: defaultValue } // TODO: only_evaluate_locally @@ -658,11 +685,27 @@ public class PostHog private constructor( key: String, defaultValue: Any?, distinctId: String?, + groups: Map?, ): Any? { if (!isEnabled()) { return defaultValue } - val value = featureFlags?.getFeatureFlag(key, defaultValue) ?: defaultValue + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("getFeatureFlag call not allowed, distinctId is invalid: $newDistinctId.") + return defaultValue + } + + val value = + featureFlags?.getFeatureFlag( + key, + defaultValue, + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: defaultValue var shouldSendFeatureFlagEvent = true synchronized(featureFlagsCalledLock) { @@ -691,11 +734,26 @@ public class PostHog private constructor( key: String, defaultValue: Any?, distinctId: String?, + groups: Map?, ): Any? { if (!isEnabled()) { return defaultValue } - return featureFlags?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("getFeatureFlagPayload call not allowed, distinctId is invalid: $newDistinctId.") + return defaultValue + } + + return featureFlags?.getFeatureFlagPayload( + key, + defaultValue, + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: defaultValue } public override fun flush() { @@ -855,6 +913,7 @@ public class PostHog private constructor( properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, + groups: Map?, groupProperties: Map?, ) { shared.capture( @@ -863,6 +922,7 @@ public class PostHog private constructor( properties = properties, userProperties = userProperties, userPropertiesSetOnce = userPropertiesSetOnce, + groups = groups, groupProperties = groupProperties, ) } @@ -887,19 +947,22 @@ public class PostHog private constructor( key: String, defaultValue: Boolean, distinctId: String?, - ): Boolean = shared.isFeatureEnabled(key, defaultValue = defaultValue, distinctId = distinctId) + groups: Map?, + ): Boolean = shared.isFeatureEnabled(key, defaultValue = defaultValue, distinctId = distinctId, groups = groups) public override fun getFeatureFlag( key: String, defaultValue: Any?, distinctId: String?, - ): Any? = shared.getFeatureFlag(key, defaultValue = defaultValue, distinctId = distinctId) + groups: Map?, + ): Any? = shared.getFeatureFlag(key, defaultValue = defaultValue, distinctId = distinctId, groups = groups) public override fun getFeatureFlagPayload( key: String, defaultValue: Any?, distinctId: String?, - ): Any? = shared.getFeatureFlagPayload(key, defaultValue = defaultValue, distinctId = distinctId) + groups: Map?, + ): Any? = shared.getFeatureFlagPayload(key, defaultValue = defaultValue, distinctId = distinctId, groups = groups) public override fun flush() { shared.flush() diff --git a/posthog/src/main/java/com/posthog/PostHogInterface.kt b/posthog/src/main/java/com/posthog/PostHogInterface.kt index b860cd79..d65a5524 100644 --- a/posthog/src/main/java/com/posthog/PostHogInterface.kt +++ b/posthog/src/main/java/com/posthog/PostHogInterface.kt @@ -29,6 +29,7 @@ public interface PostHogInterface { properties: Map? = null, userProperties: Map? = null, userPropertiesSetOnce: Map? = null, + groups: Map? = null, groupProperties: Map? = null, ) @@ -61,6 +62,7 @@ public interface PostHogInterface { key: String, defaultValue: Boolean = false, distinctId: String? = null, + groups: Map? = null, ): Boolean /** @@ -73,6 +75,7 @@ public interface PostHogInterface { key: String, defaultValue: Any? = null, distinctId: String? = null, + groups: Map? = null, ): Any? /** @@ -85,6 +88,7 @@ public interface PostHogInterface { key: String, defaultValue: Any? = null, distinctId: String? = null, + groups: Map? = null, ): Any? /** diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt index 39e69a8c..b30988e4 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt @@ -17,7 +17,7 @@ internal class PostHogFeatureFlags( private val config: PostHogConfig, private val api: PostHogApi, private val executor: ExecutorService, -) { +) : PostHogFeatureFlagsInterface { private var isLoadingFeatureFlags = AtomicBoolean(false) private val featureFlagsLock = Any() @@ -28,7 +28,7 @@ internal class PostHogFeatureFlags( @Volatile private var isFeatureFlagsLoaded = false - fun loadFeatureFlags( + override fun loadFeatureFlags( distinctId: String, anonymousId: String?, groups: Map?, @@ -55,13 +55,13 @@ internal class PostHogFeatureFlags( this.featureFlags = (this.featureFlags ?: mapOf()) + (response.featureFlags ?: mapOf()) - val normalizedPayloads = normalizePayloads(response.featureFlagPayloads) + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) this.featureFlagPayloads = (this.featureFlagPayloads ?: mapOf()) + normalizedPayloads } else { this.featureFlags = response.featureFlags - val normalizedPayloads = normalizePayloads(response.featureFlagPayloads) + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) this.featureFlagPayloads = normalizedPayloads } @@ -107,6 +107,26 @@ internal class PostHogFeatureFlags( } } + private fun loadFeatureFlagsFromNetwork( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Pair, Map>? { + try { + val response = api.decide(distinctId, anonymousId = anonymousId, groups = groups) + + response?.let { + val featureFlags = response.featureFlags ?: mapOf() + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) + + return Pair(featureFlags, normalizedPayloads) + } + } catch (e: Throwable) { + config.logger.log("Loading feature flags from network failed: $e") + } + return null + } + private fun loadFeatureFlagsFromCache() { config.cachePreferences?.let { preferences -> @Suppress("UNCHECKED_CAST") @@ -132,32 +152,14 @@ internal class PostHogFeatureFlags( } } - private fun normalizePayloads(featureFlagPayloads: Map?): Map { - val parsedPayloads = (featureFlagPayloads ?: mapOf()).toMutableMap() - - for (item in parsedPayloads) { - val value = item.value - - try { - // only try to parse if its a String, since the JSON values are stringified - if (value is String) { - // try to deserialize as Any? - config.serializer.deserializeString(value)?.let { - parsedPayloads[item.key] = it - } - } - } catch (ignored: Throwable) { - // if it fails, we keep the original value - } - } - return parsedPayloads - } - - fun isFeatureEnabled( + override fun isFeatureEnabled( key: String, defaultValue: Boolean, + distinctId: String, + anonymousId: String?, + groups: Map?, ): Boolean { - if (!isFeatureFlagsLoaded) { + if (!isFeatureFlagsLoaded && config.isClientSDK) { loadFeatureFlagsFromCache() } val value: Any? @@ -166,16 +168,7 @@ internal class PostHogFeatureFlags( value = featureFlags?.get(key) } - return if (value != null) { - if (value is Boolean) { - value - } else { - // if its multivariant flag, its enabled by default - true - } - } else { - defaultValue - } + return normalizeBoolean(value, defaultValue) } private fun readFeatureFlag( @@ -195,21 +188,31 @@ internal class PostHogFeatureFlags( return value ?: defaultValue } - fun getFeatureFlag( + override fun getFeatureFlag( key: String, defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, ): Any? { return readFeatureFlag(key, defaultValue, featureFlags) } - fun getFeatureFlagPayload( + override fun getFeatureFlagPayload( key: String, defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, ): Any? { return readFeatureFlag(key, defaultValue, featureFlagPayloads) } - fun getFeatureFlags(): Map? { + override fun getFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Map? { val flags: Map? synchronized(featureFlagsLock) { flags = featureFlags?.toMap() @@ -217,7 +220,7 @@ internal class PostHogFeatureFlags( return flags } - fun clear() { + override fun clear() { synchronized(featureFlagsLock) { featureFlags = null featureFlagPayloads = null diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt new file mode 100644 index 00000000..cb3e4a72 --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt @@ -0,0 +1,84 @@ +package com.posthog.internal + +import com.posthog.PostHogOnFeatureFlags + +internal interface PostHogFeatureFlagsInterface { + fun loadFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + onFeatureFlags: PostHogOnFeatureFlags?, + ) + + fun isFeatureEnabled( + key: String, + defaultValue: Boolean, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Boolean + + fun getFeatureFlagPayload( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Any? + + fun getFeatureFlag( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Any? + + fun getFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Map? + + fun clear() + + fun normalizePayloads( + serializer: PostHogSerializer, + featureFlagPayloads: Map?, + ): Map { + val parsedPayloads = (featureFlagPayloads ?: mapOf()).toMutableMap() + + for (item in parsedPayloads) { + val value = item.value + + try { + // only try to parse if it's a String, since the JSON values are stringified + if (value is String) { + // try to deserialize as Any? + serializer.deserializeString(value)?.let { + parsedPayloads[item.key] = it + } + } + } catch (ignored: Throwable) { + // if it fails, we keep the original value + } + } + return parsedPayloads + } + + fun normalizeBoolean( + value: Any?, + defaultValue: Boolean, + ): Boolean { + return if (value != null) { + if (value is Boolean) { + value + } else { + // if its multivariant flag, its enabled by default + true + } + } else { + defaultValue + } + } +} diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt new file mode 100644 index 00000000..925db953 --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt @@ -0,0 +1,90 @@ +package com.posthog.internal + +import com.posthog.PostHogConfig +import com.posthog.PostHogOnFeatureFlags + +internal class PostHogFeatureFlagsSync( + private val config: PostHogConfig, + private val api: PostHogApi, +) : PostHogFeatureFlagsInterface { + override fun loadFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + onFeatureFlags: PostHogOnFeatureFlags?, + ) { + // NoOp since its all sync - not cached + } + + private fun loadFeatureFlagsFromNetwork( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Pair, Map> { + try { + val response = api.decide(distinctId, anonymousId = anonymousId, groups = groups) + + response?.let { + val featureFlags = response.featureFlags ?: mapOf() + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) + + return Pair(featureFlags, normalizedPayloads) + } + } catch (e: Throwable) { + config.logger.log("Loading feature flags from network failed: $e") + } + return Pair(mapOf(), mapOf()) + } + + override fun isFeatureEnabled( + key: String, + defaultValue: Boolean, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Boolean { + val (flags, _) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + val value = flags[key] + + return normalizeBoolean(value, defaultValue) + } + + override fun getFeatureFlagPayload( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Any? { + val (_, payloads) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return payloads[key] ?: defaultValue + } + + override fun getFeatureFlag( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Any? { + val (flags, _) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return flags[key] ?: defaultValue + } + + override fun getFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Map? { + val (flags, _) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return flags.ifEmpty { null } + } + + override fun clear() { + // NoOp since its all sync - not cached + } +}