diff --git a/.changeset/android-v4-major.md b/.changeset/android-v4-major.md index 1b6846771..4d232426d 100644 --- a/.changeset/android-v4-major.md +++ b/.changeset/android-v4-major.md @@ -8,3 +8,4 @@ - **Breaking:** remove the deprecated `evaluationEnvironments` config option; use `evaluationContexts` instead - **Breaking:** remove the deprecated `getFeatureFlagPayload()`; use `getFeatureFlagResult(key)?.payload` instead - **Breaking:** remove the deprecated `PostHogSessionReplayConfig.debouncerDelayMs`; use `throttleDelayMs` instead +- **Breaking:** replace `PostHogOnFeatureFlags` with `PostHogFeatureFlagsCallback` in `PostHogConfig.onFeatureFlags` and `reloadFeatureFlags()`; it receives a `PostHogFeatureFlagsLoaded` with the enabled flags, their values, and `errorsLoading` diff --git a/.changeset/server-keep-on-feature-flags.md b/.changeset/server-keep-on-feature-flags.md new file mode 100644 index 000000000..ca5fb7278 --- /dev/null +++ b/.changeset/server-keep-on-feature-flags.md @@ -0,0 +1,5 @@ +--- +'posthog-server': minor +--- + +Move `PostHogOnFeatureFlags` into `posthog-server`; its API is unchanged diff --git a/posthog-android/src/test/java/com/posthog/android/PostHogAndroidEventSnapshotsTest.kt b/posthog-android/src/test/java/com/posthog/android/PostHogAndroidEventSnapshotsTest.kt index e59c0977c..c1533d2d1 100644 --- a/posthog-android/src/test/java/com/posthog/android/PostHogAndroidEventSnapshotsTest.kt +++ b/posthog-android/src/test/java/com/posthog/android/PostHogAndroidEventSnapshotsTest.kt @@ -3,8 +3,8 @@ package com.posthog.android import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.gson.GsonBuilder +import com.posthog.PostHogFeatureFlagsCallback import com.posthog.PostHogInterface -import com.posthog.PostHogOnFeatureFlags import com.posthog.internal.PostHogContext import com.posthog.internal.PostHogDateProvider import com.posthog.internal.PostHogDeviceDateProvider @@ -67,7 +67,7 @@ internal class PostHogAndroidEventSnapshotsTest { fun `batch request and enriched event shapes match snapshot`() { val fixture = createFixture() val flagsLoaded = CountDownLatch(1) - fixture.config.onFeatureFlags = PostHogOnFeatureFlags { flagsLoaded.countDown() } + fixture.config.onFeatureFlags = PostHogFeatureFlagsCallback { flagsLoaded.countDown() } val sut = fixture.client sut.capture( @@ -115,7 +115,7 @@ internal class PostHogAndroidEventSnapshotsTest { mapOf("industry" to "analytics"), reloadFeatureFlags = false, ) - fixture.client.reloadFeatureFlags(PostHogOnFeatureFlags { loaded.countDown() }) + fixture.client.reloadFeatureFlags(PostHogFeatureFlagsCallback { loaded.countDown() }) assertTrue(loaded.await(10, TimeUnit.SECONDS), "Feature flags request did not finish") val request = fixture.server.takeRequests("/flags/?v=2").single() diff --git a/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt b/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt index 5b328a9ce..bfdb0e45e 100644 --- a/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt +++ b/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt @@ -2,7 +2,7 @@ package com.posthog.android.sample import android.app.Application import android.os.StrictMode -import com.posthog.PostHogOnFeatureFlags +import com.posthog.PostHogFeatureFlagsCallback import com.posthog.android.PostHogAndroid import com.posthog.android.PostHogAndroidConfig @@ -39,7 +39,7 @@ class MyApp : Application() { sessionReplay = true preloadFeatureFlags = true sendFeatureFlagEvent = false - onFeatureFlags = PostHogOnFeatureFlags { print("feature flags loaded") } + onFeatureFlags = PostHogFeatureFlagsCallback { print("feature flags loaded") } addBeforeSend { event -> if (event.event == "test_event") { null diff --git a/posthog-server/api/posthog-server.api b/posthog-server/api/posthog-server.api index 2a8716456..97e021bb0 100644 --- a/posthog-server/api/posthog-server.api +++ b/posthog-server/api/posthog-server.api @@ -1,3 +1,7 @@ +public abstract interface class com/posthog/PostHogOnFeatureFlags { + public abstract fun loaded ()V +} + public final class com/posthog/server/PostHog : com/posthog/PostHogStateless, com/posthog/server/PostHogInterface { public static final field Companion Lcom/posthog/server/PostHog$Companion; public fun ()V diff --git a/posthog/src/main/java/com/posthog/PostHogOnFeatureFlags.kt b/posthog-server/src/main/java/com/posthog/PostHogOnFeatureFlags.kt similarity index 100% rename from posthog/src/main/java/com/posthog/PostHogOnFeatureFlags.kt rename to posthog-server/src/main/java/com/posthog/PostHogOnFeatureFlags.kt diff --git a/posthog-server/src/main/java/com/posthog/server/PostHogConfig.kt b/posthog-server/src/main/java/com/posthog/server/PostHogConfig.kt index 264e46bee..1e2e87cb4 100644 --- a/posthog-server/src/main/java/com/posthog/server/PostHogConfig.kt +++ b/posthog-server/src/main/java/com/posthog/server/PostHogConfig.kt @@ -312,7 +312,6 @@ public open class PostHogConfig constructor( maxBatchSize = maxBatchSize, flushIntervalSeconds = flushIntervalSeconds, encryption = encryption, - onFeatureFlags = onFeatureFlags, proxy = proxy, remoteConfigProvider = { config, api, _, _, _, _ -> PostHogFeatureFlags( diff --git a/posthog-server/src/test/java/com/posthog/server/PostHogConfigTest.kt b/posthog-server/src/test/java/com/posthog/server/PostHogConfigTest.kt index 6d36783ea..6d0dff6a8 100644 --- a/posthog-server/src/test/java/com/posthog/server/PostHogConfigTest.kt +++ b/posthog-server/src/test/java/com/posthog/server/PostHogConfigTest.kt @@ -181,7 +181,7 @@ internal class PostHogConfigTest { assertEquals(30, coreConfig.maxBatchSize) assertEquals(45, coreConfig.flushIntervalSeconds) assertEquals(mockEncryption, coreConfig.encryption) - assertEquals(mockOnFeatureFlags, coreConfig.onFeatureFlags) + assertNull(coreConfig.onFeatureFlags) } @Test diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index c744204de..7d086cb98 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -52,7 +52,7 @@ public final class com/posthog/PostHog : com/posthog/PostHogStateless, com/posth public fun optOut ()V public fun register (Ljava/lang/String;Ljava/lang/Object;)V public fun registerPushNotificationToken (Ljava/lang/String;Ljava/lang/String;)V - public fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V + public fun reloadFeatureFlags (Lcom/posthog/PostHogFeatureFlagsCallback;)V public fun reset ()V public fun resetGroupPropertiesForFlags (Ljava/lang/String;Z)V public fun resetPersonPropertiesForFlags (Z)V @@ -101,7 +101,7 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface public final fun overrideSharedInstance (Lcom/posthog/PostHogInterface;)V public fun register (Ljava/lang/String;Ljava/lang/Object;)V public fun registerPushNotificationToken (Ljava/lang/String;Ljava/lang/String;)V - public fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V + public fun reloadFeatureFlags (Lcom/posthog/PostHogFeatureFlagsCallback;)V public fun reset ()V public fun resetGroupPropertiesForFlags (Ljava/lang/String;Z)V public fun resetPersonPropertiesForFlags (Z)V @@ -156,8 +156,8 @@ public class com/posthog/PostHogConfig { public static final field DEFAULT_MAX_QUEUE_SIZE I 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;ZZZIZLjava/util/List;ZZIIIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lcom/posthog/logs/PostHogLogsConfig;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Lcom/posthog/errortracking/PostHogErrorTrackingConfig;Ljava/lang/String;Lcom/posthog/PostHogBootstrapConfig;Lkotlin/jvm/functions/Function3;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZZIIIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lcom/posthog/logs/PostHogLogsConfig;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Lcom/posthog/errortracking/PostHogErrorTrackingConfig;Ljava/lang/String;Lcom/posthog/PostHogBootstrapConfig;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZZIIIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogFeatureFlagsCallback;ZLkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lcom/posthog/logs/PostHogLogsConfig;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Lcom/posthog/errortracking/PostHogErrorTrackingConfig;Ljava/lang/String;Lcom/posthog/PostHogBootstrapConfig;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZZIIIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogFeatureFlagsCallback;ZLkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lcom/posthog/logs/PostHogLogsConfig;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function5;Lcom/posthog/errortracking/PostHogErrorTrackingConfig;Ljava/lang/String;Lcom/posthog/PostHogBootstrapConfig;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addBeforeSend (Lcom/posthog/PostHogBeforeSend;)V public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V public final fun getBeforeSendList ()Ljava/util/List; @@ -186,7 +186,7 @@ public class com/posthog/PostHogConfig { public final fun getMaxQueueSize ()I public final fun getMaxRetries ()I public final fun getNetworkStatus ()Lcom/posthog/internal/PostHogNetworkStatus; - public final fun getOnFeatureFlags ()Lcom/posthog/PostHogOnFeatureFlags; + public final fun getOnFeatureFlags ()Lcom/posthog/PostHogFeatureFlagsCallback; public final fun getOptOut ()Z public final fun getPersistOptOut ()Z public final fun getPersonProfiles ()Lcom/posthog/PersonProfiles; @@ -238,7 +238,7 @@ public class com/posthog/PostHogConfig { public final fun setMaxQueueSize (I)V public final fun setMaxRetries (I)V public final fun setNetworkStatus (Lcom/posthog/internal/PostHogNetworkStatus;)V - public final fun setOnFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V + public final fun setOnFeatureFlags (Lcom/posthog/PostHogFeatureFlagsCallback;)V public final fun setOptOut (Z)V public final fun setPersistOptOut (Z)V public final fun setPersonProfiles (Lcom/posthog/PersonProfiles;)V @@ -346,6 +346,17 @@ public final class com/posthog/PostHogEventName$Companion { public abstract interface annotation class com/posthog/PostHogExperimental : java/lang/annotation/Annotation { } +public abstract interface class com/posthog/PostHogFeatureFlagsCallback { + public abstract fun loaded (Lcom/posthog/PostHogFeatureFlagsLoaded;)V +} + +public final class com/posthog/PostHogFeatureFlagsLoaded { + public fun (Ljava/util/Map;Z)V + public final fun getErrorsLoading ()Z + public final fun getFlags ()Ljava/util/List; + public final fun getVariants ()Ljava/util/Map; +} + public abstract interface class com/posthog/PostHogIntegration { public fun install (Lcom/posthog/PostHogInterface;)V public fun onChange ()V @@ -398,8 +409,8 @@ public abstract interface class com/posthog/PostHogInterface : com/posthog/PostH public abstract fun isSessionReplayActive ()Z public abstract fun register (Ljava/lang/String;Ljava/lang/Object;)V public abstract fun registerPushNotificationToken (Ljava/lang/String;Ljava/lang/String;)V - public abstract fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V - public static synthetic fun reloadFeatureFlags$default (Lcom/posthog/PostHogInterface;Lcom/posthog/PostHogOnFeatureFlags;ILjava/lang/Object;)V + public abstract fun reloadFeatureFlags (Lcom/posthog/PostHogFeatureFlagsCallback;)V + public static synthetic fun reloadFeatureFlags$default (Lcom/posthog/PostHogInterface;Lcom/posthog/PostHogFeatureFlagsCallback;ILjava/lang/Object;)V public abstract fun reset ()V public abstract fun resetGroupPropertiesForFlags (Ljava/lang/String;Z)V public static synthetic fun resetGroupPropertiesForFlags$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZILjava/lang/Object;)V @@ -433,7 +444,7 @@ public final class com/posthog/PostHogInterface$DefaultImpls { public static synthetic fun getFeatureFlagResult$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/posthog/FeatureFlagResult; public static synthetic fun group$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZLjava/lang/Boolean;ILjava/lang/Object;)Z - public static synthetic fun reloadFeatureFlags$default (Lcom/posthog/PostHogInterface;Lcom/posthog/PostHogOnFeatureFlags;ILjava/lang/Object;)V + public static synthetic fun reloadFeatureFlags$default (Lcom/posthog/PostHogInterface;Lcom/posthog/PostHogFeatureFlagsCallback;ILjava/lang/Object;)V public static synthetic fun resetGroupPropertiesForFlags$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZILjava/lang/Object;)V public static synthetic fun resetPersonPropertiesForFlags$default (Lcom/posthog/PostHogInterface;ZILjava/lang/Object;)V public static synthetic fun screen$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V @@ -455,10 +466,6 @@ public final class com/posthog/PostHogOkHttpInterceptor : okhttp3/Interceptor { public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; } -public abstract interface class com/posthog/PostHogOnFeatureFlags { - public abstract fun loaded ()V -} - public abstract interface class com/posthog/PostHogOptInReceiver { public abstract fun onOptIn ()V } @@ -1062,10 +1069,10 @@ public final class com/posthog/internal/PostHogRemoteConfig : com/posthog/intern public fun isMinimalFlagCalledEventsEnabled ()Z public final fun isNativeCrashCaptureEnabled ()Z public final fun isSessionReplayFlagActive ()Z - public final fun loadFeatureFlags (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/posthog/PostHogOnFeatureFlags;Lcom/posthog/PostHogOnFeatureFlags;)V - public static synthetic fun loadFeatureFlags$default (Lcom/posthog/internal/PostHogRemoteConfig;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/posthog/PostHogOnFeatureFlags;Lcom/posthog/PostHogOnFeatureFlags;ILjava/lang/Object;)V - public final fun loadRemoteConfig (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/posthog/PostHogOnFeatureFlags;Lcom/posthog/PostHogOnFeatureFlags;)V - public static synthetic fun loadRemoteConfig$default (Lcom/posthog/internal/PostHogRemoteConfig;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/posthog/PostHogOnFeatureFlags;Lcom/posthog/PostHogOnFeatureFlags;ILjava/lang/Object;)V + public final fun loadFeatureFlags (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;Lcom/posthog/PostHogFeatureFlagsCallback;)V + public static synthetic fun loadFeatureFlags$default (Lcom/posthog/internal/PostHogRemoteConfig;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;Lcom/posthog/PostHogFeatureFlagsCallback;ILjava/lang/Object;)V + public final fun loadRemoteConfig (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;Lcom/posthog/PostHogFeatureFlagsCallback;)V + public static synthetic fun loadRemoteConfig$default (Lcom/posthog/internal/PostHogRemoteConfig;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;Lcom/posthog/PostHogFeatureFlagsCallback;ILjava/lang/Object;)V public final fun makeSamplingDecision (Ljava/lang/String;)Z public final fun persistPushConfigCache ()V public final fun resetGroupPropertiesForFlags (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 793e9c933..867bdf7c2 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -188,8 +188,8 @@ public class PostHog private constructor( private var isPersonProcessingLoaded: Boolean = false // this is called if the feature flags are loaded for the first time and recording isn't started yet - private val internalOnFeatureFlagsLoaded = - PostHogOnFeatureFlags { + private val internalOnFeatureFlagsLoaded: () -> Unit = + { // under setupLock so a concurrent optOut can't uninstall replay between the check and the start synchronized(setupLock) { sessionReplayHandler?.let { @@ -398,8 +398,8 @@ public class PostHog private constructor( // Bootstrap already seeded the flags in RemoteConfig's init; fire the flags-loaded // callbacks now so listeners aren't blocked on the first network response. if (remoteConfig?.hasBootstrapFlags() == true) { - notifyFeatureFlagsCallback(internalOnFeatureFlagsLoaded) - notifyFeatureFlagsCallback(config.onFeatureFlags) + internalOnFeatureFlagsLoaded() + notifyFeatureFlagsCallback(config.onFeatureFlags, remoteConfig?.getFeatureFlags(), errorsLoading = false) } when { config.remoteConfig -> @@ -1922,18 +1922,22 @@ public class PostHog private constructor( } // Invokes the feature flags callback, swallowing exceptions like runOnFeatureFlagsCallbacks. - private fun notifyFeatureFlagsCallback(onFeatureFlags: PostHogOnFeatureFlags?) { + private fun notifyFeatureFlagsCallback( + onFeatureFlags: PostHogFeatureFlagsCallback?, + featureFlags: Map?, + errorsLoading: Boolean, + ) { try { - onFeatureFlags?.loaded() + onFeatureFlags?.loaded(PostHogFeatureFlagsLoaded(featureFlags ?: emptyMap(), errorsLoading)) } catch (e: Throwable) { config?.logger?.log("Executing the feature flags callback failed: $e") } } - public override fun reloadFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags?) { + public override fun reloadFeatureFlags(onFeatureFlags: PostHogFeatureFlagsCallback?) { if (!isEnabled()) { // Still invoke the callback so awaiting callers aren't left hanging. - notifyFeatureFlagsCallback(onFeatureFlags) + notifyFeatureFlagsCallback(onFeatureFlags, featureFlags = null, errorsLoading = true) return } loadFeatureFlagsRequest( @@ -1943,8 +1947,8 @@ public class PostHog private constructor( } private fun loadFeatureFlagsRequest( - internalOnFeatureFlags: PostHogOnFeatureFlags, - onFeatureFlags: PostHogOnFeatureFlags? = null, + internalOnFeatureFlags: () -> Unit, + onFeatureFlags: PostHogFeatureFlagsCallback? = null, ) { @Suppress("UNCHECKED_CAST") val groups = getPreferences().getValue(GROUPS) as? Map @@ -1959,7 +1963,7 @@ public class PostHog private constructor( if (distinctId.isBlank()) { config?.logger?.log("Feature flags not loaded, distinctId is invalid: $distinctId") // Still invoke the callback so awaiting callers aren't left hanging. - notifyFeatureFlagsCallback(onFeatureFlags) + notifyFeatureFlagsCallback(onFeatureFlags, remoteConfig?.getFeatureFlags(), errorsLoading = true) return } @@ -1973,8 +1977,8 @@ public class PostHog private constructor( } private fun loadRemoteConfigRequest( - internalOnFeatureFlags: PostHogOnFeatureFlags?, - onFeatureFlags: PostHogOnFeatureFlags?, + internalOnFeatureFlags: (() -> Unit)?, + onFeatureFlags: PostHogFeatureFlagsCallback?, ) { @Suppress("UNCHECKED_CAST") val groups = getPreferences().getValue(GROUPS) as? Map @@ -2712,7 +2716,7 @@ public class PostHog private constructor( ) } - public override fun reloadFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags?) { + public override fun reloadFeatureFlags(onFeatureFlags: PostHogFeatureFlagsCallback?) { shared.reloadFeatureFlags(onFeatureFlags) } diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index c01078500..91528484b 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -163,10 +163,11 @@ public open class PostHogConfig( */ public var encryption: PostHogEncryption? = null, /** - * Hook that is called when feature flags are loaded + * Hook that is called when feature flags are loaded, or loading them failed + * It receives a [PostHogFeatureFlagsLoaded]; see [PostHogFeatureFlagsCallback] for the thread it runs on * Defaults to no callback */ - public var onFeatureFlags: PostHogOnFeatureFlags? = null, + public var onFeatureFlags: PostHogFeatureFlagsCallback? = null, /** * Enable Recording of Session Replays for Android * Requires Record user sessions to be enabled in the PostHog Project Settings diff --git a/posthog/src/main/java/com/posthog/PostHogFeatureFlagsCallback.kt b/posthog/src/main/java/com/posthog/PostHogFeatureFlagsCallback.kt new file mode 100644 index 000000000..2252f392a --- /dev/null +++ b/posthog/src/main/java/com/posthog/PostHogFeatureFlagsCallback.kt @@ -0,0 +1,27 @@ +package com.posthog + +/** + * Callback invoked when a feature flags load finishes, whether it succeeded or failed. + * + * It's used by `PostHogConfig.onFeatureFlags` and `reloadFeatureFlags`. It runs on a background + * thread, or right away on the calling thread when no load is attempted (for example, the SDK is + * disabled). With bootstrapped flags, `PostHogConfig.onFeatureFlags` also runs once on the thread + * calling `setup()`, before the first load finishes. Hop to the main thread before touching UI. + * + * Example: + * ```kotlin + * config.onFeatureFlags = PostHogFeatureFlagsCallback { loaded -> + * if (!loaded.errorsLoading && "new-checkout" in loaded.flags) { + * // The flag is enabled + * } + * } + * ``` + */ +public fun interface PostHogFeatureFlagsCallback { + /** + * Called when feature flags are loaded, or loading them failed. + * + * @param result the enabled flags, their values, and whether loading failed + */ + public fun loaded(result: PostHogFeatureFlagsLoaded) +} diff --git a/posthog/src/main/java/com/posthog/PostHogFeatureFlagsLoaded.kt b/posthog/src/main/java/com/posthog/PostHogFeatureFlagsLoaded.kt new file mode 100644 index 000000000..095744361 --- /dev/null +++ b/posthog/src/main/java/com/posthog/PostHogFeatureFlagsLoaded.kt @@ -0,0 +1,47 @@ +package com.posthog + +/** + * The feature flags passed to a [PostHogFeatureFlagsCallback] callback. + * + * It holds the enabled flags as they were when the callback was invoked, and whether the latest + * attempt to load them failed. Disabled flags are left out. + * + * Example: + * ```kotlin + * PostHog.reloadFeatureFlags { loaded -> + * if (loaded.errorsLoading) { + * // The values are the last known (cached) flags + * } + * if (loaded.variants["new-checkout"] == "test") { + * // Show the test variant + * } + * } + * ``` + * + * @param featureFlags the flag values, keyed by flag key; disabled flags and non-flag values are dropped + * @property errorsLoading `true` if the latest attempt to load feature flags failed. When it did, + * [flags] and [variants] hold the last known values, which can be empty. It's also `true`, with no + * values, when the SDK isn't set up or is disabled and `reloadFeatureFlags` is called. + */ +public class PostHogFeatureFlagsLoaded + @PostHogInternal + public constructor( + featureFlags: Map, + public val errorsLoading: Boolean, + ) { + /** + * The values of the enabled feature flags, keyed by flag key. + * + * A value is `true` for an enabled boolean flag, or the variant `String` for a multivariate flag. + * Disabled flags are not included. + */ + public val variants: Map = + featureFlags.entries + .mapNotNull { (key, value) -> if (value == true || value is String) key to value else null } + .toMap() + + /** + * The keys of the enabled feature flags. + */ + public val flags: List = variants.keys.toList() + } diff --git a/posthog/src/main/java/com/posthog/PostHogInterface.kt b/posthog/src/main/java/com/posthog/PostHogInterface.kt index 50e76b49b..e56fbf98b 100644 --- a/posthog/src/main/java/com/posthog/PostHogInterface.kt +++ b/posthog/src/main/java/com/posthog/PostHogInterface.kt @@ -124,9 +124,22 @@ public interface PostHogInterface : PostHogCoreInterface { /** * Reloads the feature flags - * @param onFeatureFlags the callback to get notified once feature flags is ready to use + * + * The callback is invoked when the reload finishes, on a background thread. If the request fails, + * [PostHogFeatureFlagsLoaded.errorsLoading] is `true` and the values are the last known flags. If + * the SDK isn't set up, it's invoked right away with no flags and `errorsLoading` set to `true`. + * + * ```kotlin + * PostHog.reloadFeatureFlags { loaded -> + * if (loaded.errorsLoading) { + * // The reload failed. loaded.variants holds the last known flags. + * } + * } + * ``` + * + * @param onFeatureFlags the callback to get notified once the reload finishes, defaults to none */ - public fun reloadFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags? = null) + public fun reloadFeatureFlags(onFeatureFlags: PostHogFeatureFlagsCallback? = null) /** * Returns if a feature flag is enabled, the feature flag must be a Boolean diff --git a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt index 03ba9946a..c1bb8ab0d 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt @@ -2,8 +2,9 @@ package com.posthog.internal import com.posthog.FeatureFlagResult import com.posthog.PostHogConfig +import com.posthog.PostHogFeatureFlagsCallback +import com.posthog.PostHogFeatureFlagsLoaded import com.posthog.PostHogInternal -import com.posthog.PostHogOnFeatureFlags import com.posthog.internal.PostHogPreferences.Companion.CAPTURE_PERFORMANCE import com.posthog.internal.PostHogPreferences.Companion.DEVICE_ID import com.posthog.internal.PostHogPreferences.Companion.ERROR_TRACKING @@ -59,8 +60,8 @@ public class PostHogRemoteConfig( val distinctId: String, val anonymousId: String?, val groups: Map?, - val internalOnFeatureFlags: List, - val onFeatureFlags: List, + val internalOnFeatureFlags: List<() -> Unit>, + val onFeatureFlags: List, ) private var pendingFeatureFlagsRequest: PendingFeatureFlagsRequest? = null @@ -247,16 +248,26 @@ public class PostHogRemoteConfig( return recordingActive } - private fun List.asSingleCallback(): PostHogOnFeatureFlags? = + @JvmName("asSingleInternalCallback") + private fun List<() -> Unit>.asSingleCallback(): (() -> Unit)? = when (size) { 0 -> null 1 -> first() - else -> PostHogOnFeatureFlags { forEach { it.runSafely() } } + else -> { + { forEach { runSafely(it) } } + } + } + + private fun List.asSingleCallback(): PostHogFeatureFlagsCallback? = + when (size) { + 0 -> null + 1 -> first() + else -> PostHogFeatureFlagsCallback { result -> forEach { runSafely { it.loaded(result) } } } } - private fun PostHogOnFeatureFlags.runSafely() { + private fun runSafely(callback: () -> Unit) { try { - loaded() + callback() } catch (e: Throwable) { try { config.logger.log("Executing the feature flags callback failed: $e") @@ -267,13 +278,16 @@ public class PostHogRemoteConfig( } private fun runOnFeatureFlagsCallbacks( - internalOnFeatureFlags: PostHogOnFeatureFlags?, - onFeatureFlags: PostHogOnFeatureFlags?, + internalOnFeatureFlags: (() -> Unit)?, + onFeatureFlags: PostHogFeatureFlagsCallback?, + errorsLoading: Boolean, ) { // if we don't load the feature flags (because there are none), we need to call the callback // because the app might be waiting for it. - internalOnFeatureFlags?.runSafely() - onFeatureFlags?.runSafely() + internalOnFeatureFlags?.let { runSafely(it) } + onFeatureFlags?.let { callback -> + runSafely { callback.loaded(PostHogFeatureFlagsLoaded(getFeatureFlags() ?: emptyMap(), errorsLoading)) } + } } // Notifies that a remote config resolution attempt finished. Callers set @@ -291,8 +305,8 @@ public class PostHogRemoteConfig( distinctId: String, anonymousId: String?, groups: Map?, - internalOnFeatureFlags: PostHogOnFeatureFlags? = null, - onFeatureFlags: PostHogOnFeatureFlags? = null, + internalOnFeatureFlags: (() -> Unit)? = null, + onFeatureFlags: PostHogFeatureFlagsCallback? = null, ) { executor.executeSafely { if (config.networkStatus?.isConnected() == false) { @@ -300,6 +314,7 @@ public class PostHogRemoteConfig( runOnFeatureFlagsCallbacks( internalOnFeatureFlags = internalOnFeatureFlags, onFeatureFlags = onFeatureFlags, + errorsLoading = true, ) if (!remoteConfigHasFetched.get()) { notifyRemoteConfigResolved() @@ -378,6 +393,8 @@ public class PostHogRemoteConfig( runOnFeatureFlagsCallbacks( internalOnFeatureFlags = internalOnFeatureFlags, onFeatureFlags = onFeatureFlags, + // the project has flags, but the distinctId was invalid so none were loaded + errorsLoading = it.hasFeatureFlags == true, ) } @@ -388,6 +405,7 @@ public class PostHogRemoteConfig( runOnFeatureFlagsCallbacks( internalOnFeatureFlags = internalOnFeatureFlags, onFeatureFlags = onFeatureFlags, + errorsLoading = true, ) if (!remoteConfigHasFetched.get()) { notifyRemoteConfigResolved() @@ -401,6 +419,7 @@ public class PostHogRemoteConfig( runOnFeatureFlagsCallbacks( internalOnFeatureFlags = internalOnFeatureFlags, onFeatureFlags = onFeatureFlags, + errorsLoading = true, ) config.logger.log("Loading remote config failed: $e") if (!remoteConfigHasFetched.get()) { @@ -827,8 +846,8 @@ public class PostHogRemoteConfig( distinctId: String, anonymousId: String?, groups: Map?, - internalOnFeatureFlags: PostHogOnFeatureFlags?, - onFeatureFlags: PostHogOnFeatureFlags?, + internalOnFeatureFlags: (() -> Unit)?, + onFeatureFlags: PostHogFeatureFlagsCallback?, notifyRemoteConfigLoaded: Boolean = true, ) { if (config.networkStatus?.isConnected() == false) { @@ -836,6 +855,7 @@ public class PostHogRemoteConfig( runOnFeatureFlagsCallbacks( internalOnFeatureFlags = internalOnFeatureFlags, onFeatureFlags = onFeatureFlags, + errorsLoading = true, ) if (notifyRemoteConfigLoaded && !remoteConfigHasFetched.get()) { notifyRemoteConfigResolved() @@ -875,6 +895,7 @@ public class PostHogRemoteConfig( return } + var errorsLoading = true try { val deviceId = config.cachePreferences?.getValue(DEVICE_ID) as? String @@ -916,6 +937,8 @@ public class PostHogRemoteConfig( // remote config for the current identity — mark fetched + notify (outside the // lock) so the replay buffer-and-decide gate disarms instead of staying stuck. quotaLimited = true + // like posthog-ios, a quota-limited response counts as loaded with the cached flags + errorsLoading = false return@let } @@ -1025,6 +1048,7 @@ public class PostHogRemoteConfig( } isFeatureFlagsLoaded = true setFlagsLoadedFromRemote() + errorsLoading = false if (notifyRemoteConfigLoaded) { // The flags path also resolves the remote config for the current identity (it's @@ -1054,6 +1078,7 @@ public class PostHogRemoteConfig( runOnFeatureFlagsCallbacks( internalOnFeatureFlags = internalOnFeatureFlags, onFeatureFlags = onFeatureFlags, + errorsLoading = errorsLoading, ) } catch (ignored: Throwable) { // never skip the release below: leaving the claim set stops all future flag loads @@ -1084,8 +1109,8 @@ public class PostHogRemoteConfig( distinctId: String, anonymousId: String?, groups: Map?, - internalOnFeatureFlags: PostHogOnFeatureFlags? = null, - onFeatureFlags: PostHogOnFeatureFlags? = null, + internalOnFeatureFlags: (() -> Unit)? = null, + onFeatureFlags: PostHogFeatureFlagsCallback? = null, ) { executor.executeSafely { executeFeatureFlags( diff --git a/posthog/src/test/java/com/posthog/PostHogBootstrapTest.kt b/posthog/src/test/java/com/posthog/PostHogBootstrapTest.kt index b4d4c80b1..6e8a4c9ce 100644 --- a/posthog/src/test/java/com/posthog/PostHogBootstrapTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogBootstrapTest.kt @@ -42,7 +42,7 @@ internal class PostHogBootstrapTest { storagePrefix: String = tmpDir.newFolder().absolutePath, logger: TestLogger? = null, optOut: Boolean = false, - onFeatureFlags: PostHogOnFeatureFlags? = null, + onFeatureFlags: PostHogFeatureFlagsCallback? = null, ): PostHogInterface { config = PostHogConfig(PROJECT_TOKEN, host).apply { @@ -367,14 +367,15 @@ internal class PostHogBootstrapTest { @Test fun `bootstrap flags fire the flags-loaded callback immediately`() { - var loaded = false + var loaded: PostHogFeatureFlagsLoaded? = null val sut = getSut( - bootstrap = PostHogBootstrapConfig(featureFlags = mapOf("beta-ui" to true)), - onFeatureFlags = PostHogOnFeatureFlags { loaded = true }, + bootstrap = PostHogBootstrapConfig(featureFlags = mapOf("beta-ui" to true, "checkout" to "test")), + onFeatureFlags = PostHogFeatureFlagsCallback { loaded = it }, ) - assertTrue(loaded) + assertEquals(mapOf("beta-ui" to true, "checkout" to "test"), loaded!!.variants) + assertFalse(loaded!!.errorsLoading) assertEquals(true, sut.getFeatureFlag("beta-ui", sendFeatureFlagEvent = false)) sut.close() diff --git a/posthog/src/test/java/com/posthog/PostHogFeatureFlagsTest.kt b/posthog/src/test/java/com/posthog/PostHogFeatureFlagsTest.kt index 82575e966..31603d9dc 100644 --- a/posthog/src/test/java/com/posthog/PostHogFeatureFlagsTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogFeatureFlagsTest.kt @@ -393,11 +393,13 @@ internal class PostHogFeatureFlagsTest { val sut = getSut(url.toString(), preloadFeatureFlags = false) sut.close() - var called = false - sut.reloadFeatureFlags { called = true } + var result: PostHogFeatureFlagsLoaded? = null + sut.reloadFeatureFlags { result = it } // isEnabled() is false after close(), so reloadFeatureFlags early-returns. The // callback must still fire (synchronously) rather than leave awaiting callers hanging. - assertTrue(called) + assertTrue(result!!.errorsLoading) + assertTrue(result!!.flags.isEmpty()) + assertTrue(result!!.variants.isEmpty()) } } diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index f4c0e118e..21916d135 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -1515,10 +1515,11 @@ internal class PostHogTest { val sut = getSut(url.toString(), cachePreferences = myPrefs, preloadFeatureFlags = false, integration = integration) config.sessionReplay = true + @Suppress("UNCHECKED_CAST") val onFlagsLoaded = PostHog::class.java.getDeclaredField("internalOnFeatureFlagsLoaded") .apply { isAccessible = true } - .get(sut) as PostHogOnFeatureFlags + .get(sut) as () -> Unit // optOut runs while the flags-loaded callback is between reading the handler and starting it var optOut: Thread? = null @@ -1532,7 +1533,7 @@ internal class PostHogTest { } } } - onFlagsLoaded.loaded() + onFlagsLoaded() optOut!!.join() assertTrue(sut.isOptOut()) diff --git a/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt index 5701d46e4..e5468c6c3 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt @@ -3,7 +3,8 @@ package com.posthog.internal import com.posthog.PROJECT_TOKEN import com.posthog.PostHogBootstrapConfig import com.posthog.PostHogConfig -import com.posthog.PostHogOnFeatureFlags +import com.posthog.PostHogFeatureFlagsCallback +import com.posthog.PostHogFeatureFlagsLoaded import com.posthog.TestHttpServers import com.posthog.internal.PostHogPreferences.Companion.CAPTURE_PERFORMANCE import com.posthog.internal.PostHogPreferences.Companion.ERROR_TRACKING @@ -20,6 +21,7 @@ import org.junit.Rule import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors +import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.test.AfterTest @@ -197,7 +199,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { firstLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { firstLatch.countDown() }, ) assertTrue(firstLatch.await(5, TimeUnit.SECONDS), "first flags load should complete") assertTrue(sut.isSessionReplayFlagActive()) @@ -211,7 +213,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { secondLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { secondLatch.countDown() }, ) assertTrue(secondLatch.await(5, TimeUnit.SECONDS), "second flags load should complete") @@ -235,7 +237,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -332,7 +334,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { firstLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { firstLatch.countDown() }, ) assertTrue(firstLatch.await(5, TimeUnit.SECONDS), "first flags load should complete") assertTrue(sut.hasRemoteConfigFetched()) @@ -346,7 +348,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { secondLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { secondLatch.countDown() }, ) assertTrue(secondLatch.await(5, TimeUnit.SECONDS), "second (offline) flags load should complete") @@ -397,7 +399,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -430,7 +432,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -463,7 +465,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -488,7 +490,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -602,7 +604,7 @@ internal class PostHogRemoteConfigTest { "user-in-rollout", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { firstLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { firstLatch.countDown() }, ) assertTrue(firstLatch.await(5, TimeUnit.SECONDS), "first flags load should complete") assertTrue(sut.isSessionReplayFlagActive()) @@ -614,7 +616,7 @@ internal class PostHogRemoteConfigTest { "user-outside-rollout", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { secondLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { secondLatch.countDown() }, ) assertTrue(secondLatch.await(5, TimeUnit.SECONDS), "second flags load should complete") @@ -747,7 +749,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -824,7 +826,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "quota limited remote config load should complete") @@ -917,15 +919,14 @@ internal class PostHogRemoteConfigTest { var called = false var calledInternal = false - val onFeatureFlags: PostHogOnFeatureFlags = - PostHogOnFeatureFlags { + val onFeatureFlags: PostHogFeatureFlagsCallback = + PostHogFeatureFlagsCallback { called = true } - val internalOnFeatureFlags: PostHogOnFeatureFlags = - PostHogOnFeatureFlags { - calledInternal = true - } + val internalOnFeatureFlags = { + calledInternal = true + } val sut = getSut(host = url.toString()) @@ -1101,12 +1102,12 @@ internal class PostHogRemoteConfigTest { val secondCallbackLatch = CountDownLatch(1) val firstCallback = - PostHogOnFeatureFlags { + PostHogFeatureFlagsCallback { firstCallbackLatch.countDown() } val secondCallback = - PostHogOnFeatureFlags { + PostHogFeatureFlagsCallback { secondCallbackLatch.countDown() } @@ -1180,7 +1181,7 @@ internal class PostHogRemoteConfigTest { anonymousId = "anon_id_for_hash_key", groups = mapOf("company" to "posthog"), onFeatureFlags = - PostHogOnFeatureFlags { + PostHogFeatureFlagsCallback { secondCallbackLatch.countDown() }, ) @@ -1730,7 +1731,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -1764,7 +1765,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2554,7 +2555,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2583,7 +2584,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2620,7 +2621,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2674,7 +2675,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2705,7 +2706,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "remote config load should complete") @@ -2757,7 +2758,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2786,7 +2787,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2829,7 +2830,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { latch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { latch.countDown() }, ) assertTrue(latch.await(5, TimeUnit.SECONDS), "flags load should complete") @@ -2867,7 +2868,7 @@ internal class PostHogRemoteConfigTest { "user-1", anonymousId = "anon-1", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { firstLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { firstLatch.countDown() }, ) assertTrue(firstLatch.await(5, TimeUnit.SECONDS), "first flags load should complete") // a complete /flags response replaces the served flags, so the bootstrapped-only key is dropped @@ -2884,7 +2885,7 @@ internal class PostHogRemoteConfigTest { "user-2", anonymousId = "anon-2", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { secondLatch.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { secondLatch.countDown() }, ) assertTrue(secondLatch.await(5, TimeUnit.SECONDS), "second flags load should complete") @@ -2950,7 +2951,7 @@ internal class PostHogRemoteConfigTest { anonymousId = "anonId", emptyMap(), onFeatureFlags = - PostHogOnFeatureFlags { + PostHogFeatureFlagsCallback { counts[index].incrementAndGet() fired.countDown() }, @@ -2985,7 +2986,7 @@ internal class PostHogRemoteConfigTest { val invocations = AtomicInteger(0) val completed = CountDownLatch(2) val shared = - PostHogOnFeatureFlags { + PostHogFeatureFlagsCallback { invocations.incrementAndGet() completed.countDown() } @@ -3042,7 +3043,7 @@ internal class PostHogRemoteConfigTest { anonymousId = "anonId", emptyMap(), onFeatureFlags = - PostHogOnFeatureFlags { + PostHogFeatureFlagsCallback { first.countDown() throw IllegalStateException("callback blew up") }, @@ -3055,7 +3056,7 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - onFeatureFlags = PostHogOnFeatureFlags { second.countDown() }, + onFeatureFlags = PostHogFeatureFlagsCallback { second.countDown() }, ) assertTrue(second.await(5, TimeUnit.SECONDS), "a later reload never ran - the in-flight claim was stranded") @@ -3065,6 +3066,50 @@ internal class PostHogRemoteConfigTest { http.shutdown() } + @Test + fun `the feature flags callback receives the enabled flags and their values`() { + val flags = File("src/test/resources/json/flags-v1/basic-flags-with-non-active-flags.json").readText() + val http = mockHttp(response = MockResponse().setBody(flags)) + val sut = getSut(host = http.url("/").toString()) + + val results = LinkedBlockingQueue() + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), onFeatureFlags = { results.add(it) }) + + val loaded = results.poll(5, TimeUnit.SECONDS)!! + assertEquals(mapOf("4535-funnel-bar-viz" to true, "splashScreenName" to "SplashV2"), loaded.variants) + assertEquals(setOf("4535-funnel-bar-viz", "splashScreenName"), loaded.flags.toSet()) + assertFalse(loaded.errorsLoading) + } + + @Test + fun `a failed flags load reports errorsLoading with the last known flags`() { + val http = mockHttp(response = MockResponse().setBody(responseFlagsApi)) + http.enqueue(MockResponse().setResponseCode(500)) + val sut = getSut(host = http.url("/").toString()) + + val results = LinkedBlockingQueue() + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), onFeatureFlags = { results.add(it) }) + sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), onFeatureFlags = { results.add(it) }) + + assertFalse(results.poll(5, TimeUnit.SECONDS)!!.errorsLoading) + val failed = results.poll(5, TimeUnit.SECONDS)!! + assertTrue(failed.errorsLoading) + assertEquals(mapOf("4535-funnel-bar-viz" to true), failed.variants) + } + + @Test + fun `a failed remote config load reports errorsLoading`() { + val http = mockHttp(response = MockResponse().setResponseCode(500)) + val sut = getSut(host = http.url("/").toString()) + + val results = LinkedBlockingQueue() + sut.loadRemoteConfig("my_identify", anonymousId = "anonId", emptyMap(), onFeatureFlags = { results.add(it) }) + + val loaded = results.poll(5, TimeUnit.SECONDS)!! + assertTrue(loaded.errorsLoading) + assertTrue(loaded.flags.isEmpty()) + } + @Test fun `a throwing internal callback does not suppress the caller's callback`() { val http = mockHttp(response = MockResponse().setBody(responseFlagsApi)) @@ -3075,8 +3120,8 @@ internal class PostHogRemoteConfigTest { "my_identify", anonymousId = "anonId", emptyMap(), - internalOnFeatureFlags = PostHogOnFeatureFlags { throw IllegalStateException("internal blew up") }, - onFeatureFlags = PostHogOnFeatureFlags { userCallbackRan.countDown() }, + internalOnFeatureFlags = { throw IllegalStateException("internal blew up") }, + onFeatureFlags = PostHogFeatureFlagsCallback { userCallbackRan.countDown() }, ) assertTrue(userCallbackRan.await(5, TimeUnit.SECONDS), "the caller's callback was suppressed by the internal one") diff --git a/posthog/src/testFixtures/java/com/posthog/PostHogFake.kt b/posthog/src/testFixtures/java/com/posthog/PostHogFake.kt index b76f57c4e..ca61394fe 100644 --- a/posthog/src/testFixtures/java/com/posthog/PostHogFake.kt +++ b/posthog/src/testFixtures/java/com/posthog/PostHogFake.kt @@ -79,7 +79,7 @@ public class PostHogFake : PostHogInterface { ) { } - override fun reloadFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags?) { + override fun reloadFeatureFlags(onFeatureFlags: PostHogFeatureFlagsCallback?) { } override fun isFeatureEnabled(