From a5ba25816f3346f291f0f8b96531e637cb4754b3 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 20 Mar 2025 11:04:01 +0100 Subject: [PATCH 1/5] chore: remote config api --- posthog/api/posthog.api | 6 +- posthog/src/main/java/com/posthog/PostHog.kt | 48 +++- .../main/java/com/posthog/PostHogConfig.kt | 6 + .../java/com/posthog/internal/PostHogApi.kt | 33 +++ .../posthog/internal/PostHogDecideResponse.kt | 4 +- ...FeatureFlags.kt => PostHogRemoteConfig.kt} | 243 ++++++++++++------ .../internal/PostHogRemoteConfigResponse.kt | 13 + ...lagsTest.kt => PostHogRemoteConfigTest.kt} | 6 +- 8 files changed, 263 insertions(+), 96 deletions(-) rename posthog/src/main/java/com/posthog/internal/{PostHogFeatureFlags.kt => PostHogRemoteConfig.kt} (52%) create mode 100644 posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt rename posthog/src/test/java/com/posthog/internal/{PostHogFeatureFlagsTest.kt => PostHogRemoteConfigTest.kt} (99%) diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index b8e8f004..7793dcc9 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -72,8 +72,8 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface public class com/posthog/PostHogConfig { public static final field Companion Lcom/posthog/PostHogConfig$Companion; public static final field DEFAULT_HOST Ljava/lang/String; - public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V public final fun getApiKey ()Ljava/lang/String; public final fun getCachePreferences ()Lcom/posthog/internal/PostHogPreferences; @@ -96,6 +96,7 @@ public class com/posthog/PostHogConfig { public final fun getPersonProfiles ()Lcom/posthog/PersonProfiles; public final fun getPreloadFeatureFlags ()Z public final fun getPropertiesSanitizer ()Lcom/posthog/PostHogPropertiesSanitizer; + public final fun getRemoteConfig ()Z public final fun getReplayStoragePrefix ()Ljava/lang/String; public final fun getReuseAnonymousId ()Z public final fun getSdkName ()Ljava/lang/String; @@ -124,6 +125,7 @@ public class com/posthog/PostHogConfig { public final fun setPersonProfiles (Lcom/posthog/PersonProfiles;)V public final fun setPreloadFeatureFlags (Z)V public final fun setPropertiesSanitizer (Lcom/posthog/PostHogPropertiesSanitizer;)V + public final fun setRemoteConfig (Z)V public final fun setReplayStoragePrefix (Ljava/lang/String;)V public final fun setReuseAnonymousId (Z)V public final fun setSdkName (Ljava/lang/String;)V diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index eb322259..d8bc6e04 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -2,7 +2,6 @@ package com.posthog import com.posthog.internal.PostHogApi import com.posthog.internal.PostHogApiEndpoint -import com.posthog.internal.PostHogFeatureFlags import com.posthog.internal.PostHogMemoryPreferences import com.posthog.internal.PostHogNoOpLogger import com.posthog.internal.PostHogPreferences @@ -17,6 +16,7 @@ import com.posthog.internal.PostHogPreferences.Companion.PERSON_PROCESSING import com.posthog.internal.PostHogPreferences.Companion.VERSION import com.posthog.internal.PostHogPrintLogger import com.posthog.internal.PostHogQueue +import com.posthog.internal.PostHogRemoteConfig import com.posthog.internal.PostHogSendCachedEventsIntegration import com.posthog.internal.PostHogSerializer import com.posthog.internal.PostHogSessionManager @@ -59,7 +59,7 @@ public class PostHog private constructor( private var config: PostHogConfig? = null - private var featureFlags: PostHogFeatureFlags? = null + private var remoteConfig: PostHogRemoteConfig? = null private var queue: PostHogQueue? = null private var replayQueue: PostHogQueue? = null private var memoryPreferences = PostHogMemoryPreferences() @@ -86,7 +86,7 @@ public class PostHog private constructor( val api = PostHogApi(config) val queue = PostHogQueue(config, api, PostHogApiEndpoint.BATCH, config.storagePrefix, queueExecutor) val replayQueue = PostHogQueue(config, api, PostHogApiEndpoint.SNAPSHOT, config.replayStoragePrefix, replayExecutor) - val featureFlags = PostHogFeatureFlags(config, api, featureFlagsExecutor) + val featureFlags = PostHogRemoteConfig(config, api, featureFlagsExecutor) // no need to lock optOut here since the setup is locked already val optOut = @@ -110,7 +110,7 @@ public class PostHog private constructor( this.config = config this.queue = queue this.replayQueue = replayQueue - this.featureFlags = featureFlags + this.remoteConfig = featureFlags config.addIntegration(sendCachedEventsIntegration) @@ -131,7 +131,9 @@ public class PostHog private constructor( } // only because of testing in isolation, this flag is always enabled - if (reloadFeatureFlags && config.preloadFeatureFlags) { + if (reloadFeatureFlags && config.remoteConfig) { + loadRemoteConfigRequest(config.onFeatureFlags) + } else if (reloadFeatureFlags && config.preloadFeatureFlags) { loadFeatureFlagsRequest(config.onFeatureFlags) } } catch (e: Throwable) { @@ -299,7 +301,7 @@ public class PostHog private constructor( } if (config?.sendFeatureFlagEvent == true) { - featureFlags?.getFeatureFlags()?.let { + remoteConfig?.getFeatureFlags()?.let { if (it.isNotEmpty()) { val keys = mutableListOf() for (entry in it.entries) { @@ -702,7 +704,29 @@ public class PostHog private constructor( return } - featureFlags?.loadFeatureFlags(distinctId, anonymousId = anonymousId, groups, onFeatureFlags) + val calledFromRemoteConfig = config?.remoteConfig ?: false + // only process remote config if the remote config API is disabled + remoteConfig?.loadFeatureFlags( + distinctId, + anonymousId = anonymousId, + groups, + onFeatureFlags, + calledFromRemoteConfig = calledFromRemoteConfig, + ) + } + + private fun loadRemoteConfigRequest(onFeatureFlags: PostHogOnFeatureFlags?) { + @Suppress("UNCHECKED_CAST") + val groups = getPreferences().getValue(GROUPS) as? Map + + val distinctId = this.distinctId + var anonymousId: String? = null + + if (config?.reuseAnonymousId != true) { + anonymousId = this.anonymousId + } + + remoteConfig?.loadRemoteConfig(distinctId, anonymousId = anonymousId, groups, onFeatureFlags) } public override fun isFeatureEnabled( @@ -712,7 +736,7 @@ public class PostHog private constructor( if (!isEnabled()) { return defaultValue } - val value = featureFlags?.isFeatureEnabled(key, defaultValue) ?: defaultValue + val value = remoteConfig?.isFeatureEnabled(key, defaultValue) ?: defaultValue sendFeatureFlagCalled(key, value) @@ -751,7 +775,7 @@ public class PostHog private constructor( if (!isEnabled()) { return defaultValue } - val value = featureFlags?.getFeatureFlag(key, defaultValue) ?: defaultValue + val value = remoteConfig?.getFeatureFlag(key, defaultValue) ?: defaultValue sendFeatureFlagCalled(key, value) @@ -765,7 +789,7 @@ public class PostHog private constructor( if (!isEnabled()) { return defaultValue } - return featureFlags?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue + return remoteConfig?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue } public override fun flush() { @@ -790,7 +814,7 @@ public class PostHog private constructor( except.add(ANONYMOUS_ID) } getPreferences().clear(except = except.toList()) - featureFlags?.clear() + remoteConfig?.clear() featureFlagsCalled.clear() synchronized(identifiedLock) { isIdentifiedLoaded = false @@ -878,7 +902,7 @@ public class PostHog private constructor( // this is used in cases where we know the session is already active // so we spare another locker private fun isSessionReplayFlagActive(): Boolean { - return config?.sessionReplay == true && featureFlags?.isSessionReplayFlagActive() == true + return config?.sessionReplay == true && remoteConfig?.isSessionReplayFlagActive() == true } override fun isSessionReplayActive(): Boolean { diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index bfc9b07a..f300e60d 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -50,6 +50,12 @@ public open class PostHogConfig( * Defaults to true */ public var preloadFeatureFlags: Boolean = true, + /** + * Preload PostHog remote config automatically + * Defaults to true + */ + @PostHogExperimental + public var remoteConfig: Boolean = true, /** * Number of minimum events before they are sent over the wire * Defaults to 20 diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 075b2532..320681b9 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -117,4 +117,37 @@ internal class PostHogApi( return null } } + + @Throws(PostHogApiError::class, IOException::class) + fun loadRemoteConfig(): PostHogRemoteConfigResponse? { + var host = theHost + host = + when (host) { + "https://us.i.posthog.com" -> { + "https://us-assets.i.posthog.com" + } + "https://eu.i.posthog.com" -> { + "https://eu-assets.i.posthog.com" + } + else -> { + host + } + } + + val request = + Request.Builder() + .url("$host/array/${config.apiKey}/config") + .header("User-Agent", config.userAgent) + .get() + .build() + + client.newCall(request).execute().use { + if (!it.isSuccessful) throw PostHogApiError(it.code, it.message, it.body) + + it.body?.let { body -> + return config.serializer.deserialize(body.charStream().buffered()) + } + return null + } + } } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt b/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt index 83a770e2..073be3d9 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt @@ -15,7 +15,5 @@ internal data class PostHogDecideResponse( val errorsWhileComputingFlags: Boolean = false, val featureFlags: Map?, val featureFlagPayloads: Map?, - // its either a boolean or a map, see https://github.com/PostHog/posthog-js/blob/10fd7f4fa083f997d31a4a4c7be7d311d0a95e74/src/types.ts#L235-L243 - val sessionRecording: Any? = false, val quotaLimited: List? = null, -) +) : PostHogRemoteConfigResponse() diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt similarity index 52% rename from posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt rename to posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt index aae2c570..91687e2f 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt @@ -14,14 +14,16 @@ import java.util.concurrent.atomic.AtomicBoolean * @property api the API * @property executor the Executor */ -internal class PostHogFeatureFlags( +internal class PostHogRemoteConfig( private val config: PostHogConfig, private val api: PostHogApi, private val executor: ExecutorService, ) { private var isLoadingFeatureFlags = AtomicBoolean(false) + private var isLoadingRemoteConfig = AtomicBoolean(false) private val featureFlagsLock = Any() + private val remoteConfigLock = Any() private var featureFlags: Map? = null private var featureFlagPayloads: Map? = null @@ -29,6 +31,9 @@ internal class PostHogFeatureFlags( @Volatile private var isFeatureFlagsLoaded = false + @Volatile + private var isRemoteConfigLoaded = false + @Volatile private var sessionReplayFlagActive = false @@ -81,7 +86,7 @@ internal class PostHogFeatureFlags( return recordingActive } - fun loadFeatureFlags( + fun loadRemoteConfig( distinctId: String, anonymousId: String?, groups: Map?, @@ -93,102 +98,183 @@ internal class PostHogFeatureFlags( return@executeSafely } - if (isLoadingFeatureFlags.getAndSet(true)) { - config.logger.log("Feature flags are being loaded already.") + if (isLoadingRemoteConfig.getAndSet(true)) { + config.logger.log("Remote Config is being loaded already.") return@executeSafely } try { - val response = api.decide(distinctId, anonymousId = anonymousId, groups) + val response = api.loadRemoteConfig() response?.let { - synchronized(featureFlagsLock) { - if (response.quotaLimited?.contains("feature_flags") == true) { - config.logger.log( - """Feature flags are quota limited, clearing existing flags. - Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts""", - ) - this.featureFlags = null - this.featureFlagPayloads = null - config.cachePreferences?.let { preferences -> - preferences.remove(FEATURE_FLAGS) - preferences.remove(FEATURE_FLAGS_PAYLOAD) + synchronized(remoteConfigLock) { + processSessionRecordingConfig(it.sessionRecording) + + val hasFlags = it.hasFeatureFlags ?: false + + if (hasFlags && config.preloadFeatureFlags) { + if (distinctId.isNotBlank()) { + // do not process session recording from decide API + // since its already cached via the remote config API + loadFeatureFlags( + distinctId, + anonymousId, + groups, + onFeatureFlags, + calledFromRemoteConfig = true, + ) + } else { + config.logger.log("Feature flags not loaded, distinctId is invalid: $distinctId") } - return@let } - if (response.errorsWhileComputingFlags) { - // if not all flags were computed, we upsert flags instead of replacing them - this.featureFlags = - (this.featureFlags ?: mapOf()) + (response.featureFlags ?: mapOf()) + isRemoteConfigLoaded = true + } + } ?: run { + isRemoteConfigLoaded = false + } + } catch (e: Throwable) { + config.logger.log("Loading remote config failed: $e") + } finally { + isLoadingRemoteConfig.set(false) + } + } + } - val normalizedPayloads = normalizePayloads(response.featureFlagPayloads) + private fun processSessionRecordingConfig(sessionRecording: Any?) { + when (sessionRecording) { + is Boolean -> { + // if sessionRecording is a Boolean, its always disabled + // so we don't enable sessionReplayFlagActive here + sessionReplayFlagActive = sessionRecording + + if (!sessionRecording) { + config.cachePreferences?.remove(SESSION_REPLAY) + } else { + // do nothing + } + } - this.featureFlagPayloads = - (this.featureFlagPayloads ?: mapOf()) + normalizedPayloads - } else { - this.featureFlags = response.featureFlags + is Map<*, *> -> { + @Suppress("UNCHECKED_CAST") + (sessionRecording as? Map)?.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 = it["endpoint"] as? String + ?: config.snapshotEndpoint - val normalizedPayloads = normalizePayloads(response.featureFlagPayloads) - this.featureFlagPayloads = normalizedPayloads - } + sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it) + config.cachePreferences?.setValue(SESSION_REPLAY, it) - when (val sessionRecording = response.sessionRecording) { - is Boolean -> { - // if sessionRecording is a Boolean, its always disabled - // so we don't enable sessionReplayFlagActive here - sessionReplayFlagActive = sessionRecording - - if (!sessionRecording) { - config.cachePreferences?.remove(SESSION_REPLAY) - } else { - // do nothing - } - } + // TODO: + // consoleLogRecordingEnabled -> Boolean or null + // networkPayloadCapture -> Boolean or null, can also be networkPayloadCapture={recordBody=true, recordHeaders=true} + // sampleRate, etc + } + } + else -> { + // do nothing + } + } + } - is Map<*, *> -> { - @Suppress("UNCHECKED_CAST") - (sessionRecording as? Map)?.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 = it["endpoint"] as? String - ?: config.snapshotEndpoint - - sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it) - config.cachePreferences?.setValue(SESSION_REPLAY, it) - - // TODO: - // consoleLogRecordingEnabled -> Boolean or null - // networkPayloadCapture -> Boolean or null, can also be networkPayloadCapture={recordBody=true, recordHeaders=true} - // sampleRate, etc - } - } - else -> { - // do nothing - } + private fun executeFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + onFeatureFlags: PostHogOnFeatureFlags?, + calledFromRemoteConfig: Boolean, + ) { + if (config.networkStatus?.isConnected() == false) { + config.logger.log("Network isn't connected.") + return + } + + if (isLoadingFeatureFlags.getAndSet(true)) { + config.logger.log("Feature flags are being loaded already.") + return + } + + try { + val response = api.decide(distinctId, anonymousId = anonymousId, groups) + + response?.let { + synchronized(featureFlagsLock) { + if (it.quotaLimited?.contains("feature_flags") == true) { + config.logger.log( + """Feature flags are quota limited, clearing existing flags. + Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts""", + ) + this.featureFlags = null + this.featureFlagPayloads = null + config.cachePreferences?.let { preferences -> + preferences.remove(FEATURE_FLAGS) + preferences.remove(FEATURE_FLAGS_PAYLOAD) } + return@let } - 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) + if (it.errorsWhileComputingFlags) { + // if not all flags were computed, we upsert flags instead of replacing them + this.featureFlags = + (this.featureFlags ?: mapOf()) + (it.featureFlags ?: mapOf()) + + val normalizedPayloads = normalizePayloads(it.featureFlagPayloads) + + this.featureFlagPayloads = + (this.featureFlagPayloads ?: mapOf()) + normalizedPayloads + } else { + this.featureFlags = it.featureFlags + + val normalizedPayloads = normalizePayloads(it.featureFlagPayloads) + this.featureFlagPayloads = normalizedPayloads } - isFeatureFlagsLoaded = true + + // only process and cache session recording config from decide API + // if not yet done by the remote config API + if (!calledFromRemoteConfig) { + processSessionRecordingConfig(it.sessionRecording) + } + } + 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) { + config.logger.log("Loading feature flags failed: $e") + } finally { + try { + onFeatureFlags?.loaded() } catch (e: Throwable) { - config.logger.log("Loading feature flags failed: $e") + config.logger.log("Executing the feature flags callback failed: $e") } finally { - try { - onFeatureFlags?.loaded() - } catch (e: Throwable) { - config.logger.log("Executing the feature flags callback failed: $e") - } finally { - isLoadingFeatureFlags.set(false) - } + isLoadingFeatureFlags.set(false) + } + } + } + + fun loadFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + onFeatureFlags: PostHogOnFeatureFlags?, + calledFromRemoteConfig: Boolean = false, + ) { + // only run within the executor if not called from an executor already + if (!calledFromRemoteConfig) { + executor.executeSafely { + executeFeatureFlags(distinctId, anonymousId, groups, onFeatureFlags, calledFromRemoteConfig) } + } else { + executeFeatureFlags(distinctId, anonymousId, groups, onFeatureFlags, calledFromRemoteConfig) } } @@ -328,6 +414,7 @@ internal class PostHogFeatureFlags( featureFlags = null featureFlagPayloads = null sessionReplayFlagActive = false + isFeatureFlagsLoaded = false config.cachePreferences?.let { preferences -> preferences.remove(FEATURE_FLAGS) @@ -335,5 +422,9 @@ internal class PostHogFeatureFlags( preferences.remove(SESSION_REPLAY) } } + + synchronized(remoteConfigLock) { + isRemoteConfigLoaded = false + } } } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt new file mode 100644 index 00000000..6bfc8066 --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt @@ -0,0 +1,13 @@ +package com.posthog.internal + +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +@IgnoreJRERequirement +internal open class PostHogRemoteConfigResponse( + // its either a boolean or a map, see https://github.com/PostHog/posthog-js/blob/10fd7f4fa083f997d31a4a4c7be7d311d0a95e74/src/types.ts#L235-L243 + val sessionRecording: Any? = false, + // its eitger a boolean or a map + val surveys: Any? = false, + // Indicates if the team has any flags enabled (if not we don't need to load them) + val hasFeatureFlags: Boolean? = false, +) diff --git a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt similarity index 99% rename from posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt rename to posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt index 7f33791f..83807056 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt @@ -19,7 +19,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -internal class PostHogFeatureFlagsTest { +internal class PostHogRemoteConfigTest { private val executor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("Test")) private val file = File("src/test/resources/json/basic-decide-no-errors.json") @@ -31,14 +31,14 @@ internal class PostHogFeatureFlagsTest { private fun getSut( host: String, networkStatus: PostHogNetworkStatus? = null, - ): PostHogFeatureFlags { + ): PostHogRemoteConfig { config = PostHogConfig(API_KEY, host).apply { this.networkStatus = networkStatus cachePreferences = preferences } val api = PostHogApi(config!!) - return PostHogFeatureFlags(config!!, api, executor = executor) + return PostHogRemoteConfig(config!!, api, executor = executor) } @BeforeTest From 9be67c344181c6a7254f0ca794e120228e1bf3d6 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 20 Mar 2025 11:18:03 +0100 Subject: [PATCH 2/5] fix tests --- posthog/src/main/java/com/posthog/PostHog.kt | 8 ++--- .../posthog/internal/PostHogRemoteConfig.kt | 4 +-- .../com/posthog/PostHogPersonProfilesTest.kt | 4 +-- .../src/test/java/com/posthog/PostHogTest.kt | 26 ++++++++------- .../internal/PostHogRemoteConfigTest.kt | 32 +++++++++---------- 5 files changed, 38 insertions(+), 36 deletions(-) diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index d8bc6e04..9f15dbd8 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -35,9 +35,9 @@ public class PostHog private constructor( Executors.newSingleThreadScheduledExecutor( PostHogThreadFactory("PostHogReplayQueueThread"), ), - private val featureFlagsExecutor: ExecutorService = + private val remoteConfigExecutor: ExecutorService = Executors.newSingleThreadScheduledExecutor( - PostHogThreadFactory("PostHogFeatureFlagsThread"), + PostHogThreadFactory("PostHogRemoteConfigThread"), ), private val cachedEventsExecutor: ExecutorService = Executors.newSingleThreadScheduledExecutor( @@ -86,7 +86,7 @@ public class PostHog private constructor( val api = PostHogApi(config) val queue = PostHogQueue(config, api, PostHogApiEndpoint.BATCH, config.storagePrefix, queueExecutor) val replayQueue = PostHogQueue(config, api, PostHogApiEndpoint.SNAPSHOT, config.replayStoragePrefix, replayExecutor) - val featureFlags = PostHogRemoteConfig(config, api, featureFlagsExecutor) + val featureFlags = PostHogRemoteConfig(config, api, remoteConfigExecutor) // no need to lock optOut here since the setup is locked already val optOut = @@ -710,8 +710,8 @@ public class PostHog private constructor( distinctId, anonymousId = anonymousId, groups, - onFeatureFlags, calledFromRemoteConfig = calledFromRemoteConfig, + onFeatureFlags, ) } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt index 91687e2f..bd8cda65 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt @@ -120,8 +120,8 @@ internal class PostHogRemoteConfig( distinctId, anonymousId, groups, - onFeatureFlags, calledFromRemoteConfig = true, + onFeatureFlags, ) } else { config.logger.log("Feature flags not loaded, distinctId is invalid: $distinctId") @@ -265,8 +265,8 @@ internal class PostHogRemoteConfig( distinctId: String, anonymousId: String?, groups: Map?, - onFeatureFlags: PostHogOnFeatureFlags?, calledFromRemoteConfig: Boolean = false, + onFeatureFlags: PostHogOnFeatureFlags?, ) { // only run within the executor if not called from an executor already if (!calledFromRemoteConfig) { diff --git a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt index c14be5b1..385a65d3 100644 --- a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt @@ -18,7 +18,7 @@ internal class PostHogPersonProfilesTest { private val queueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestQueue")) private val replayQueueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestReplayQueue")) - private val featureFlagsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestFeatureFlags")) + private val remoteConfigExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestRemoteConfig")) private val cachedEventsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestCachedEvents")) private val serializer = PostHogSerializer(PostHogConfig(API_KEY)) private lateinit var config: PostHogConfig @@ -46,7 +46,7 @@ internal class PostHogPersonProfilesTest { config, queueExecutor, replayQueueExecutor, - featureFlagsExecutor, + remoteConfigExecutor, cachedEventsExecutor, false, ) diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index 5ea57897..beef8e4c 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -28,7 +28,7 @@ internal class PostHogTest { private val queueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestQueue")) private val replayQueueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestReplayQueue")) - private val featureFlagsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestFeatureFlags")) + private val remoteConfigExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestRemoteConfig")) private val cachedEventsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestCachedEvents")) private val serializer = PostHogSerializer(PostHogConfig(API_KEY)) private lateinit var config: PostHogConfig @@ -46,6 +46,7 @@ internal class PostHogTest { sendFeatureFlagEvent: Boolean = true, reuseAnonymousId: Boolean = false, integration: PostHogIntegration? = null, + remoteConfig: Boolean = false, cachePreferences: PostHogMemoryPreferences = PostHogMemoryPreferences(), propertiesSanitizer: PostHogPropertiesSanitizer? = null, ): PostHogInterface { @@ -63,12 +64,13 @@ internal class PostHogTest { this.reuseAnonymousId = reuseAnonymousId this.cachePreferences = cachePreferences this.propertiesSanitizer = propertiesSanitizer + this.remoteConfig = remoteConfig } return PostHog.withInternal( config, queueExecutor, replayQueueExecutor, - featureFlagsExecutor, + remoteConfigExecutor, cachedEventsExecutor, reloadFeatureFlags, ) @@ -162,7 +164,7 @@ internal class PostHogTest { val sut = getSut(url.toString()) - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() val request = http.takeRequest() assertEquals(1, http.requestCount) @@ -184,7 +186,7 @@ internal class PostHogTest { reloaded = true } - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() assertTrue(reloaded) @@ -205,7 +207,7 @@ internal class PostHogTest { sut.reloadFeatureFlags() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() assertTrue(sut.isFeatureEnabled("4535-funnel-bar-viz")) @@ -226,7 +228,7 @@ internal class PostHogTest { sut.reloadFeatureFlags() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz") as Boolean) @@ -254,7 +256,7 @@ internal class PostHogTest { sut.reloadFeatureFlags() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() // remove from the http queue http.takeRequest() @@ -312,7 +314,7 @@ internal class PostHogTest { sut.reloadFeatureFlags() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() // remove from the http queue http.takeRequest() @@ -357,7 +359,7 @@ internal class PostHogTest { sut.reloadFeatureFlags() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() assertTrue(sut.getFeatureFlagPayload("thePayload") as Boolean) @@ -371,7 +373,7 @@ internal class PostHogTest { val sut = getSut(url.toString(), preloadFeatureFlags = false) - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() assertEquals(0, http.requestCount) @@ -1113,7 +1115,7 @@ internal class PostHogTest { sut.reloadFeatureFlags() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() // remove from the http queue http.takeRequest() @@ -1292,7 +1294,7 @@ internal class PostHogTest { sut.reset() - featureFlagsExecutor.shutdownAndAwaitTermination() + remoteConfigExecutor.shutdownAndAwaitTermination() val request = http.takeRequest() assertEquals(1, http.requestCount) diff --git a/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt index 83807056..b9d3972d 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt @@ -61,7 +61,7 @@ internal class PostHogRemoteConfigTest { false }) - sut.loadFeatureFlags("distinctId", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("distinctId", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -105,7 +105,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -132,7 +132,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -152,7 +152,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -171,7 +171,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.awaitExecution() @@ -182,7 +182,7 @@ internal class PostHogRemoteConfigTest { .setBody(file.readText()) http.enqueue(response) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -207,7 +207,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -231,7 +231,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -284,7 +284,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -312,7 +312,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -357,7 +357,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -380,7 +380,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -403,7 +403,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -426,7 +426,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) - sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), false, null) executor.shutdownAndAwaitTermination() @@ -453,7 +453,7 @@ internal class PostHogRemoteConfigTest { val sut = getSut(host = url.toString()) // Load initial flags - sut.loadFeatureFlags("test_id", null, null, null) + sut.loadFeatureFlags("test_id", null, null, false, null) executor.awaitExecution() // Verify flags are loaded @@ -472,7 +472,7 @@ internal class PostHogRemoteConfigTest { ) // Reload flags - sut.loadFeatureFlags("test_id", null, null, null) + sut.loadFeatureFlags("test_id", null, null, false, null) executor.awaitExecution() // Verify flags are cleared From d909a61b6a7b614e8b55b832fc6e5308ff5f76a1 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 20 Mar 2025 12:45:52 +0100 Subject: [PATCH 3/5] fix --- posthog-android/lint-baseline.xml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/posthog-android/lint-baseline.xml b/posthog-android/lint-baseline.xml index 1abed766..f0af1a85 100644 --- a/posthog-android/lint-baseline.xml +++ b/posthog-android/lint-baseline.xml @@ -45,4 +45,26 @@ column="17"/> + + + + + + + + From 902a42308473932a42d93801c74d6dc5888b4a8c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 20 Mar 2025 13:12:04 +0100 Subject: [PATCH 4/5] tests --- CHANGELOG.md | 2 + posthog/api/posthog.api | 4 + .../main/java/com/posthog/PostHogConfig.kt | 9 ++- .../java/com/posthog/internal/PostHogApi.kt | 21 +++-- .../posthog/internal/PostHogRemoteConfig.kt | 2 +- .../src/test/java/com/posthog/PostHogTest.kt | 77 +++++++++++++++++++ .../com/posthog/internal/PostHogApiTest.kt | 43 +++++++++++ .../json/basic-remote-config-no-flags.json | 44 +++++++++++ .../resources/json/basic-remote-config.json | 44 +++++++++++ 9 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 posthog/src/test/resources/json/basic-remote-config-no-flags.json create mode 100644 posthog/src/test/resources/json/basic-remote-config.json diff --git a/CHANGELOG.md b/CHANGELOG.md index edf1730b..785d151d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- feat: use remote config API ([#233](https://github.com/PostHog/posthog-android/pull/233)) + ## 3.12.0 - 2025-03-10 - feat: support reuse of `anonymousId` between user changes ([#229](https://github.com/PostHog/posthog-android/pull/229)) diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index 7793dcc9..910e21e5 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -71,7 +71,11 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface public class com/posthog/PostHogConfig { public static final field Companion Lcom/posthog/PostHogConfig$Companion; + public static final field DEFAULT_EU_ASSETS_HOST Ljava/lang/String; + public static final field DEFAULT_EU_HOST Ljava/lang/String; public static final field DEFAULT_HOST Ljava/lang/String; + public static final field DEFAULT_US_ASSETS_HOST Ljava/lang/String; + public static final field DEFAULT_US_HOST Ljava/lang/String; public fun (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index f300e60d..4e72eb1f 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -218,6 +218,13 @@ public open class PostHogConfig( } public companion object { - public const val DEFAULT_HOST: String = "https://us.i.posthog.com" + public const val DEFAULT_US_HOST: String = "https://us.i.posthog.com" + public const val DEFAULT_US_ASSETS_HOST: String = "https://us-assets.i.posthog.com" + + // flutter uses it + public const val DEFAULT_HOST: String = DEFAULT_US_HOST + + public const val DEFAULT_EU_HOST: String = "https://eu.i.posthog.com" + public const val DEFAULT_EU_ASSETS_HOST: String = "https://eu-assets.i.posthog.com" } } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 320681b9..baa23ae2 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -1,6 +1,10 @@ package com.posthog.internal import com.posthog.PostHogConfig +import com.posthog.PostHogConfig.Companion.DEFAULT_EU_ASSETS_HOST +import com.posthog.PostHogConfig.Companion.DEFAULT_EU_HOST +import com.posthog.PostHogConfig.Companion.DEFAULT_US_ASSETS_HOST +import com.posthog.PostHogConfig.Companion.DEFAULT_US_HOST import com.posthog.PostHogEvent import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -17,10 +21,14 @@ import java.io.OutputStream internal class PostHogApi( private val config: PostHogConfig, ) { + private companion object { + private const val APP_JSON_UTF_8 = "application/json; charset=utf-8" + } + private val mediaType by lazy { try { // can throw IllegalArgumentException - "application/json; charset=utf-8".toMediaType() + APP_JSON_UTF_8.toMediaType() } catch (ignored: Throwable) { null } @@ -119,15 +127,15 @@ internal class PostHogApi( } @Throws(PostHogApiError::class, IOException::class) - fun loadRemoteConfig(): PostHogRemoteConfigResponse? { + fun remoteConfig(): PostHogRemoteConfigResponse? { var host = theHost host = when (host) { - "https://us.i.posthog.com" -> { - "https://us-assets.i.posthog.com" + DEFAULT_US_HOST -> { + DEFAULT_US_ASSETS_HOST } - "https://eu.i.posthog.com" -> { - "https://eu-assets.i.posthog.com" + DEFAULT_EU_HOST -> { + DEFAULT_EU_ASSETS_HOST } else -> { host @@ -138,6 +146,7 @@ internal class PostHogApi( Request.Builder() .url("$host/array/${config.apiKey}/config") .header("User-Agent", config.userAgent) + .header("Content-Type", APP_JSON_UTF_8) .get() .build() diff --git a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt index bd8cda65..f9896ed1 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt @@ -104,7 +104,7 @@ internal class PostHogRemoteConfig( } try { - val response = api.loadRemoteConfig() + val response = api.remoteConfig() response?.let { synchronized(remoteConfigLock) { diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index beef8e4c..d24cc33b 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -193,6 +193,83 @@ internal class PostHogTest { sut.close() } + @Test + fun `preload remote config if enabled`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), remoteConfig = true, preloadFeatureFlags = false) + + remoteConfigExecutor.shutdownAndAwaitTermination() + + val request = http.takeRequest() + assertEquals(1, http.requestCount) + assertEquals("/array/${API_KEY}/config", request.path) + + sut.close() + } + + @Test + fun `preload remote config and flags if enabled`() { + val file = File("src/test/resources/json/basic-remote-config.json") + val responseText = file.readText() + + val http = + mockHttp( + response = + MockResponse() + .setBody(responseText), + ) + http.enqueue( + MockResponse() + .setBody(responseDecideApi), + ) + val url = http.url("/") + + val sut = getSut(url.toString(), remoteConfig = true) + + remoteConfigExecutor.shutdownAndAwaitTermination() + + val remoteConfigRequest = http.takeRequest() + + assertEquals(2, http.requestCount) + assertEquals("/array/${API_KEY}/config", remoteConfigRequest.path) + + val decideApiRequest = http.takeRequest() + assertEquals("/decide/?v=3", decideApiRequest.path) + + sut.close() + } + + @Test + fun `preload remote config but no flags`() { + val file = File("src/test/resources/json/basic-remote-config-no-flags.json") + val responseText = file.readText() + + val http = + mockHttp( + response = + MockResponse() + .setBody(responseText), + ) + http.enqueue( + MockResponse() + .setBody(responseDecideApi), + ) + val url = http.url("/") + + val sut = getSut(url.toString(), remoteConfig = true) + + remoteConfigExecutor.shutdownAndAwaitTermination() + + val remoteConfigRequest = http.takeRequest() + + assertEquals(1, http.requestCount) + assertEquals("/array/${API_KEY}/config", remoteConfigRequest.path) + + sut.close() + } + @Test fun `isFeatureEnabled returns value after reloaded`() { val http = diff --git a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt index f501b41d..efe93180 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt @@ -102,4 +102,47 @@ internal class PostHogApiTest { assertEquals("Client Error", exc.message) assertNotNull(exc.body) } + + @Test + fun `remote config returns successful response`() { + val file = File("src/test/resources/json/basic-remote-config.json") + val responseApi = file.readText() + + val http = + mockHttp( + response = + MockResponse() + .setBody(responseApi), + ) + val url = http.url("/") + + val sut = getSut(host = url.toString()) + + val response = sut.remoteConfig() + + val request = http.takeRequest() + + assertNotNull(response) + assertEquals("posthog-java/${BuildConfig.VERSION_NAME}", request.headers["User-Agent"]) + assertEquals("GET", request.method) + assertEquals("/array/${API_KEY}/config", request.path) + assertEquals("gzip", request.headers["Accept-Encoding"]) + assertEquals("application/json; charset=utf-8", request.headers["Content-Type"]) + } + + @Test + fun `remote config throws if not successful`() { + val http = mockHttp(response = MockResponse().setResponseCode(400).setBody("error")) + val url = http.url("/") + + val sut = getSut(host = url.toString()) + + val exc = + assertThrows(PostHogApiError::class.java) { + sut.remoteConfig() + } + assertEquals(400, exc.statusCode) + assertEquals("Client Error", exc.message) + assertNotNull(exc.body) + } } diff --git a/posthog/src/test/resources/json/basic-remote-config-no-flags.json b/posthog/src/test/resources/json/basic-remote-config-no-flags.json new file mode 100644 index 00000000..e043f862 --- /dev/null +++ b/posthog/src/test/resources/json/basic-remote-config-no-flags.json @@ -0,0 +1,44 @@ +{ + "token": "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D", + "supportedCompression": [ + "gzip", + "gzip-js" + ], + "hasFeatureFlags": false, + "captureDeadClicks": false, + "capturePerformance": { + "network_timing": true, + "web_vitals": false, + "web_vitals_allowed_metrics": null + }, + "autocapture_opt_out": true, + "autocaptureExceptions": false, + "analytics": { + "endpoint": "/i/v0/e/" + }, + "elementsChainAsString": true, + "sessionRecording": { + "endpoint": "/s/", + "consoleLogRecordingEnabled": true, + "recorderVersion": "v2", + "sampleRate": null, + "minimumDurationMilliseconds": 1000, + "linkedFlag": null, + "networkPayloadCapture": { + "recordBody": true, + "recordHeaders": true + }, + "masking": null, + "urlTriggers": [], + "urlBlocklist": [], + "eventTriggers": [], + "scriptConfig": null, + "recordCanvas": true, + "canvasFps": 3, + "canvasQuality": "0.4" + }, + "heatmaps": false, + "surveys": false, + "defaultIdentifiedOnly": true, + "siteApps": [] +} \ No newline at end of file diff --git a/posthog/src/test/resources/json/basic-remote-config.json b/posthog/src/test/resources/json/basic-remote-config.json new file mode 100644 index 00000000..5622d615 --- /dev/null +++ b/posthog/src/test/resources/json/basic-remote-config.json @@ -0,0 +1,44 @@ +{ + "token": "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D", + "supportedCompression": [ + "gzip", + "gzip-js" + ], + "hasFeatureFlags": true, + "captureDeadClicks": false, + "capturePerformance": { + "network_timing": true, + "web_vitals": false, + "web_vitals_allowed_metrics": null + }, + "autocapture_opt_out": true, + "autocaptureExceptions": false, + "analytics": { + "endpoint": "/i/v0/e/" + }, + "elementsChainAsString": true, + "sessionRecording": { + "endpoint": "/s/", + "consoleLogRecordingEnabled": true, + "recorderVersion": "v2", + "sampleRate": null, + "minimumDurationMilliseconds": 1000, + "linkedFlag": null, + "networkPayloadCapture": { + "recordBody": true, + "recordHeaders": true + }, + "masking": null, + "urlTriggers": [], + "urlBlocklist": [], + "eventTriggers": [], + "scriptConfig": null, + "recordCanvas": true, + "canvasFps": 3, + "canvasQuality": "0.4" + }, + "heatmaps": false, + "surveys": false, + "defaultIdentifiedOnly": true, + "siteApps": [] +} \ No newline at end of file From c77a6a7f88d41dbbc0f8e8b06e52b3ece206cefd Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 20 Mar 2025 13:16:56 +0100 Subject: [PATCH 5/5] tests --- .../internal/PostHogRemoteConfigTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt index b9d3972d..81331c18 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt @@ -479,4 +479,31 @@ internal class PostHogRemoteConfigTest { assertNull(sut.getFeatureFlags()) assertNull(preferences.getValue(FEATURE_FLAGS)) } + + @Test + fun `returns session replay enabled after remote config API call`() { + val file = File("src/test/resources/json/basic-remote-config-no-flags.json") + + val http = + mockHttp( + response = + MockResponse() + .setBody(file.readText()), + ) + val url = http.url("/") + + val sut = getSut(host = url.toString()) + + sut.loadRemoteConfig("my_identify", anonymousId = "anonId", emptyMap(), null) + + executor.shutdownAndAwaitTermination() + + assertTrue(sut.isSessionReplayFlagActive()) + assertEquals("/s/", config?.snapshotEndpoint) + assertEquals(1, http.requestCount) + + sut.clear() + + assertFalse(sut.isSessionReplayFlagActive()) + } }