Skip to content
1 change: 1 addition & 0 deletions .changeset/android-v4-major.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
- **Breaking:** return `null` instead of the raw string for a malformed, empty or whitespace-only feature flag payload from `getFeatureFlagResult(key)?.payload`; payloads must be valid JSON (a JSON `null` payload now also reads as `null` instead of the string `"null"`)
- **Breaking:** make SDK-internal helpers in the `com.posthog.internal` and `com.posthog.android.replay.internal` packages non-public
- **Breaking:** remove the deprecated `remoteConfig` config option; the SDK now always loads remote config at setup
- **Breaking:** replace `PostHogOnFeatureFlags` with `PostHogFeatureFlagsCallback` in `PostHogConfig.onFeatureFlags` and `reloadFeatureFlags()`; it receives a `PostHogFeatureFlagsLoaded` with the enabled flags, their values, and `errorsLoading`
5 changes: 5 additions & 0 deletions .changeset/server-keep-on-feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-server': minor
---

Move `PostHogOnFeatureFlags` into `posthog-server`; its API is unchanged
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(CONFIG_PATH, "/flags/?v=2").single { it.path == "/flags/?v=2" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions posthog-server/api/posthog-server.api
Original file line number Diff line number Diff line change
@@ -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 <init> ()V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ public open class PostHogConfig constructor(
maxBatchSize = maxBatchSize,
flushIntervalSeconds = flushIntervalSeconds,
encryption = encryption,
onFeatureFlags = onFeatureFlags,
proxy = proxy,
remoteConfigProvider = { config, api, _, _, _, _ ->
PostHogFeatureFlags(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ internal class PostHogConfigTest {
assertEquals(30, coreConfig.maxBatchSize)
assertEquals(45, coreConfig.flushIntervalSeconds)
assertEquals(mockEncryption, coreConfig.encryption)
assertEquals(mockOnFeatureFlags, coreConfig.onFeatureFlags)
assertNull(coreConfig.onFeatureFlags)
}

@Test
Expand Down
41 changes: 24 additions & 17 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIIIILcom/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 <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIIIILcom/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 <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIIIILcom/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 <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIIIILcom/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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -237,7 +237,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
Expand Down Expand Up @@ -344,6 +344,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 <init> (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
Expand Down Expand Up @@ -396,8 +407,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
Expand Down Expand Up @@ -431,7 +442,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
Expand All @@ -453,10 +464,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
}
Expand Down Expand Up @@ -1042,10 +1049,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
Expand Down
36 changes: 22 additions & 14 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -397,8 +397,12 @@ 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)
try {
internalOnFeatureFlagsLoaded()
} catch (e: Throwable) {
config.logger.log("Executing the internal feature flags callback failed: $e")
}
notifyFeatureFlagsCallback(config.onFeatureFlags, remoteConfig?.getFeatureFlags(), errorsLoading = false)
}
loadRemoteConfigRequest(
internalOnFeatureFlagsLoaded,
Expand Down Expand Up @@ -1915,18 +1919,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<String, Any>?,
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(
Expand All @@ -1936,8 +1944,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<String, String>
Expand All @@ -1952,7 +1960,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
}

Expand All @@ -1966,8 +1974,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<String, String>
Expand Down Expand Up @@ -2705,7 +2713,7 @@ public class PostHog private constructor(
)
}

public override fun reloadFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags?) {
public override fun reloadFeatureFlags(onFeatureFlags: PostHogFeatureFlagsCallback?) {
shared.reloadFeatureFlags(onFeatureFlags)
}

Expand Down
5 changes: 3 additions & 2 deletions posthog/src/main/java/com/posthog/PostHogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,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
Expand Down
Loading
Loading