diff --git a/build.gradle.kts b/build.gradle.kts index dca5ad74..be38a80c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,7 +54,7 @@ tasks.withType().configureEach { } apiValidation { - ignoredProjects.add("posthog-android-sample") + ignoredProjects.addAll(listOf("posthog-android-sample", "posthog-console-sample")) } nexusPublishing.postHogConfig() diff --git a/posthog-android/src/main/java/com/posthog/android/PostHogAndroidConfig.kt b/posthog-android/src/main/java/com/posthog/android/PostHogAndroidConfig.kt index d6cc10d9..6115996d 100644 --- a/posthog-android/src/main/java/com/posthog/android/PostHogAndroidConfig.kt +++ b/posthog-android/src/main/java/com/posthog/android/PostHogAndroidConfig.kt @@ -19,4 +19,14 @@ public open class PostHogAndroidConfig( public var captureScreenViews: Boolean = true, @PostHogExperimental public var sessionReplayConfig: PostHogSessionReplayConfig = PostHogSessionReplayConfig(), -) : PostHogConfig(apiKey, host) +) : PostHogConfig(apiKey, host) { + init { + isClientSDK = true + featureFlagsRequestTimeoutSeconds = 10 + flushAt = 20 + maxQueueSize = 1000 + maxBatchSize = 50 + disableGeoIP = false + preloadFeatureFlags = true + } +} diff --git a/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidContext.kt b/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidContext.kt index f1c38240..17aaa236 100644 --- a/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidContext.kt +++ b/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidContext.kt @@ -10,8 +10,6 @@ import android.os.Build import android.util.DisplayMetrics import com.posthog.android.PostHogAndroidConfig import com.posthog.internal.PostHogContext -import java.util.Locale -import java.util.TimeZone import kotlin.math.pow import kotlin.math.sqrt @@ -141,11 +139,6 @@ internal class PostHogAndroidContext( @SuppressLint("MissingPermission") override fun getDynamicContext(): Map { val dynamicContext = mutableMapOf() - dynamicContext["\$locale"] = "${Locale.getDefault().language}-${Locale.getDefault().country}" - System.getProperty("http.agent")?.let { - dynamicContext["\$user_agent"] = it - } - dynamicContext["\$timezone"] = TimeZone.getDefault().id context.connectivityManager()?.let { connectivityManager -> if (context.hasPermission(Manifest.permission.ACCESS_NETWORK_STATE)) { diff --git a/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt b/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt index d3ce847e..fece531a 100644 --- a/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt +++ b/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt @@ -22,7 +22,7 @@ public class PostHogFake : PostHogInterface { properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, - groupProperties: Map?, + groups: Map?, ) { this.event = event this.properties = properties @@ -42,6 +42,8 @@ public class PostHogFake : PostHogInterface { override fun isFeatureEnabled( key: String, defaultValue: Boolean, + distinctId: String?, + groups: Map?, ): Boolean { return false } @@ -49,6 +51,8 @@ public class PostHogFake : PostHogInterface { override fun getFeatureFlag( key: String, defaultValue: Any?, + distinctId: String?, + groups: Map?, ): Any? { return null } @@ -56,10 +60,26 @@ public class PostHogFake : PostHogInterface { override fun getFeatureFlagPayload( key: String, defaultValue: Any?, + distinctId: String?, + groups: Map?, ): Any? { return null } + override fun getAllFeatureFlags( + distinctId: String?, + groups: Map?, + ): Map? { + return null + } + + override fun getAllFeatureFlagsAndPayloads( + distinctId: String?, + groups: Map?, + ): Pair?, Map?> { + return Pair(null, null) + } + override fun flush() { } @@ -76,17 +96,22 @@ public class PostHogFake : PostHogInterface { type: String, key: String, groupProperties: Map?, + distinctId: String?, ) { } override fun screen( screenTitle: String, properties: Map?, + distinctId: String?, ) { this.screenTitle = screenTitle } - override fun alias(alias: String) { + override fun alias( + alias: String, + distinctId: String?, + ) { } override fun isOptOut(): Boolean { diff --git a/posthog-samples/posthog-console-sample/build.gradle.kts b/posthog-samples/posthog-console-sample/build.gradle.kts new file mode 100644 index 00000000..94ad225f --- /dev/null +++ b/posthog-samples/posthog-console-sample/build.gradle.kts @@ -0,0 +1,25 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +group = "com.posthog" +version = "1.0.0" + +dependencies { + implementation(project(mapOf("path" to ":posthog"))) + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} + +tasks.withType().configureEach { + kotlinOptions.postHogConfig() +} + +kotlin { + jvmToolchain(PosthogBuildConfig.Build.JAVA_VERSION.majorVersion.toInt()) +} diff --git a/posthog-samples/posthog-console-sample/src/main/java/Main.kt b/posthog-samples/posthog-console-sample/src/main/java/Main.kt new file mode 100644 index 00000000..3f446557 --- /dev/null +++ b/posthog-samples/posthog-console-sample/src/main/java/Main.kt @@ -0,0 +1,36 @@ +package com.posthog + +import java.lang.Thread.sleep + +public fun main() { + val config = + PostHogConfig("phc_pQ70jJhZKHRvDIL5ruOErnPy6xiAiWCqlL4ayELj4X8").apply { + debug = true + flushAt = 1 + } + PostHog.setup(config) + + PostHog.capture( + event = "Hello World!", + distinctId = "123", + properties = mapOf("test" to true), + userProperties = mapOf("name" to "my name"), + userPropertiesSetOnce = mapOf("age" to 33), + groups = mapOf("company" to "posthog"), + ) + PostHog.isFeatureEnabled( + "myFlag", + defaultValue = false, + distinctId = "123", + groups = mapOf("company" to "posthog"), + ) + + PostHog.flush() +// + PostHog.close() + + while (Thread.activeCount() > 1) { + println("threads still active") + sleep(10000) + } +} diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index 3b08e207..f638b5d5 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -1,19 +1,21 @@ public final class com/posthog/PostHog : com/posthog/PostHogInterface { public static final field Companion Lcom/posthog/PostHog$Companion; public synthetic fun (Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;Ljava/util/concurrent/ExecutorService;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun alias (Ljava/lang/String;)V + public fun alias (Ljava/lang/String;Ljava/lang/String;)V public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V public fun close ()V public fun debug (Z)V public fun distinctId ()Ljava/lang/String; public fun endSession ()V public fun flush ()V + public fun getAllFeatureFlags (Ljava/lang/String;Ljava/util/Map;)Ljava/util/Map; + public fun getAllFeatureFlagsAndPayloads (Ljava/lang/String;Ljava/util/Map;)Lkotlin/Pair; public fun getConfig ()Lcom/posthog/PostHogConfig; - public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; - public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; - public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public fun isFeatureEnabled (Ljava/lang/String;Z)Z + public fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;)Z public fun isOptOut ()Z public fun isSessionActive ()Z public fun optIn ()V @@ -21,26 +23,28 @@ public final class com/posthog/PostHog : com/posthog/PostHogInterface { public fun register (Ljava/lang/String;Ljava/lang/Object;)V public fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V public fun reset ()V - public fun screen (Ljava/lang/String;Ljava/util/Map;)V + public fun screen (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public fun setup (Lcom/posthog/PostHogConfig;)V public fun startSession ()V public fun unregister (Ljava/lang/String;)V } public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface { - public fun alias (Ljava/lang/String;)V + public fun alias (Ljava/lang/String;Ljava/lang/String;)V public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V public fun close ()V public fun debug (Z)V public fun distinctId ()Ljava/lang/String; public fun endSession ()V public fun flush ()V + public fun getAllFeatureFlags (Ljava/lang/String;Ljava/util/Map;)Ljava/util/Map; + public fun getAllFeatureFlagsAndPayloads (Ljava/lang/String;Ljava/util/Map;)Lkotlin/Pair; public fun getConfig ()Lcom/posthog/PostHogConfig; - public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; - public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; - public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public fun isFeatureEnabled (Ljava/lang/String;Z)Z + public fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;)Z public fun isOptOut ()Z public fun isSessionActive ()Z public fun optIn ()V @@ -50,7 +54,7 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface public fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V public fun reset ()V public final fun resetSharedInstance ()V - public fun screen (Ljava/lang/String;Ljava/util/Map;)V + public fun screen (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public fun setup (Lcom/posthog/PostHogConfig;)V public fun startSession ()V public fun unregister (Ljava/lang/String;)V @@ -60,15 +64,17 @@ 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;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;IIZ)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;IIZILkotlin/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; public final fun getContext ()Lcom/posthog/internal/PostHogContext; public final fun getDateProvider ()Lcom/posthog/internal/PostHogDateProvider; public final fun getDebug ()Z + public final fun getDisableGeoIP ()Z public final fun getEncryption ()Lcom/posthog/PostHogEncryption; + public final fun getFeatureFlagsRequestTimeoutSeconds ()I public final fun getFlushAt ()I public final fun getFlushIntervalSeconds ()I public final fun getHost ()Ljava/lang/String; @@ -83,6 +89,7 @@ public class com/posthog/PostHogConfig { public final fun getPreloadFeatureFlags ()Z public final fun getPropertiesSanitizer ()Lcom/posthog/PostHogPropertiesSanitizer; public final fun getReplayStoragePrefix ()Ljava/lang/String; + public final fun getRequestTimeoutSeconds ()I public final fun getSdkName ()Ljava/lang/String; public final fun getSdkVersion ()Ljava/lang/String; public final fun getSendFeatureFlagEvent ()Z @@ -90,12 +97,16 @@ public class com/posthog/PostHogConfig { public final fun getSessionReplay ()Z public final fun getSnapshotEndpoint ()Ljava/lang/String; public final fun getStoragePrefix ()Ljava/lang/String; + public final fun isClientSDK ()Z public final fun removeIntegration (Lcom/posthog/PostHogIntegration;)V public final fun setCachePreferences (Lcom/posthog/internal/PostHogPreferences;)V + public final fun setClientSDK (Z)V public final fun setContext (Lcom/posthog/internal/PostHogContext;)V public final fun setDateProvider (Lcom/posthog/internal/PostHogDateProvider;)V public final fun setDebug (Z)V + public final fun setDisableGeoIP (Z)V public final fun setEncryption (Lcom/posthog/PostHogEncryption;)V + public final fun setFeatureFlagsRequestTimeoutSeconds (I)V public final fun setFlushAt (I)V public final fun setFlushIntervalSeconds (I)V public final fun setLegacyStoragePrefix (Ljava/lang/String;)V @@ -108,6 +119,7 @@ public class com/posthog/PostHogConfig { public final fun setPreloadFeatureFlags (Z)V public final fun setPropertiesSanitizer (Lcom/posthog/PostHogPropertiesSanitizer;)V public final fun setReplayStoragePrefix (Ljava/lang/String;)V + public final fun setRequestTimeoutSeconds (I)V public final fun setSdkName (Ljava/lang/String;)V public final fun setSdkVersion (Ljava/lang/String;)V public final fun setSendFeatureFlagEvent (Z)V @@ -166,19 +178,21 @@ public final class com/posthog/PostHogIntegration$DefaultImpls { } public abstract interface class com/posthog/PostHogInterface { - public abstract fun alias (Ljava/lang/String;)V + public abstract fun alias (Ljava/lang/String;Ljava/lang/String;)V public abstract fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V public abstract fun close ()V public abstract fun debug (Z)V public abstract fun distinctId ()Ljava/lang/String; public abstract fun endSession ()V public abstract fun flush ()V + public abstract fun getAllFeatureFlags (Ljava/lang/String;Ljava/util/Map;)Ljava/util/Map; + public abstract fun getAllFeatureFlagsAndPayloads (Ljava/lang/String;Ljava/util/Map;)Lkotlin/Pair; public abstract fun getConfig ()Lcom/posthog/PostHogConfig; - public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; - public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; - public abstract fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/Object; + public abstract fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public abstract fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public abstract fun isFeatureEnabled (Ljava/lang/String;Z)Z + public abstract fun isFeatureEnabled (Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;)Z public abstract fun isOptOut ()Z public abstract fun isSessionActive ()Z public abstract fun optIn ()V @@ -186,22 +200,25 @@ public abstract interface class com/posthog/PostHogInterface { public abstract fun register (Ljava/lang/String;Ljava/lang/Object;)V public abstract fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V public abstract fun reset ()V - public abstract fun screen (Ljava/lang/String;Ljava/util/Map;)V + public abstract fun screen (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V public abstract fun setup (Lcom/posthog/PostHogConfig;)V public abstract fun startSession ()V public abstract fun unregister (Ljava/lang/String;)V } public final class com/posthog/PostHogInterface$DefaultImpls { + public static synthetic fun alias$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun capture$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V public static synthetic fun debug$default (Lcom/posthog/PostHogInterface;ZILjava/lang/Object;)V - public static synthetic fun getFeatureFlag$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun getFeatureFlagPayload$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object; - 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 getAllFeatureFlags$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Ljava/util/Map; + public static synthetic fun getAllFeatureFlagsAndPayloads$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lkotlin/Pair; + public static synthetic fun getFeatureFlag$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun getFeatureFlagPayload$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun group$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun identify$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V - public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZILjava/lang/Object;)Z + public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;ZLjava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Z public static synthetic fun reloadFeatureFlags$default (Lcom/posthog/PostHogInterface;Lcom/posthog/PostHogOnFeatureFlags;ILjava/lang/Object;)V - public static synthetic fun screen$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V + public static synthetic fun screen$default (Lcom/posthog/PostHogInterface;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)V } public abstract interface annotation class com/posthog/PostHogInternal : java/lang/annotation/Annotation { diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index 11a7efac..c5ed2752 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -3,6 +3,8 @@ package com.posthog import com.posthog.internal.PostHogApi import com.posthog.internal.PostHogApiEndpoint import com.posthog.internal.PostHogFeatureFlags +import com.posthog.internal.PostHogFeatureFlagsInterface +import com.posthog.internal.PostHogFeatureFlagsSync import com.posthog.internal.PostHogMemoryPreferences import com.posthog.internal.PostHogPreferences import com.posthog.internal.PostHogPreferences.Companion.ALL_INTERNAL_KEYS @@ -16,6 +18,9 @@ import com.posthog.internal.PostHogQueue import com.posthog.internal.PostHogSendCachedEventsIntegration import com.posthog.internal.PostHogSerializer import com.posthog.internal.PostHogThreadFactory +import com.posthog.internal.shutdownSafely +import java.util.Locale +import java.util.TimeZone import java.util.UUID import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -56,10 +61,11 @@ public class PostHog private constructor( private var config: PostHogConfig? = null - private var featureFlags: PostHogFeatureFlags? = null + private var featureFlags: PostHogFeatureFlagsInterface? = null private var queue: PostHogQueue? = null private var replayQueue: PostHogQueue? = null private var memoryPreferences = PostHogMemoryPreferences() + private val featureFlagsCalled = mutableMapOf>() public override fun setup(config: T) { @@ -74,12 +80,25 @@ public class PostHog private constructor( config.logger.log("API Key: ${config.apiKey} already has a PostHog instance.") } - val cachePreferences = config.cachePreferences ?: memoryPreferences + val isClientSDK = config.isClientSDK + val cachePreferences = (if (isClientSDK) config.cachePreferences else null) ?: memoryPreferences config.cachePreferences = cachePreferences 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: PostHogFeatureFlagsInterface + + if (isClientSDK) { + val replayQueue = PostHogQueue(config, api, PostHogApiEndpoint.SNAPSHOT, config.replayStoragePrefix, replayExecutor) + this.replayQueue = replayQueue + + featureFlags = PostHogFeatureFlags(config, api, featureFlagsExecutor) + } else { + replayExecutor.shutdownSafely() + + featureFlags = PostHogFeatureFlagsSync(config, api) + featureFlagsExecutor.shutdownSafely() + } // no need to lock optOut here since the setup is locked already val optOut = @@ -91,29 +110,34 @@ public class PostHog private constructor( config.optOut = optOut } - val startDate = config.dateProvider.currentDate() - val sendCachedEventsIntegration = - PostHogSendCachedEventsIntegration( - config, - api, - startDate, - cachedEventsExecutor, - ) - this.config = config this.queue = queue - this.replayQueue = replayQueue this.featureFlags = featureFlags - config.addIntegration(sendCachedEventsIntegration) - - legacyPreferences(config, config.serializer) + if (isClientSDK) { + val startDate = config.dateProvider.currentDate() + val sendCachedEventsIntegration = + PostHogSendCachedEventsIntegration( + config, + api, + startDate, + cachedEventsExecutor, + ) + + config.addIntegration(sendCachedEventsIntegration) + + legacyPreferences(config, config.serializer) + } else { + cachedEventsExecutor.shutdownSafely() + } enabled = true queue.start() - startSession() + if (isClientSDK) { + startSession() + } config.integrations.forEach { try { @@ -124,7 +148,7 @@ public class PostHog private constructor( } // only because of testing in isolation, this flag is always enabled - if (reloadFeatureFlags && config.preloadFeatureFlags) { + if (reloadFeatureFlags && config.preloadFeatureFlags && isClientSDK) { loadFeatureFlagsRequest(config.onFeatureFlags) } } catch (e: Throwable) { @@ -195,37 +219,60 @@ public class PostHog private constructor( private var anonymousId: String get() { - var anonymousId: String? - synchronized(anonymousLock) { - anonymousId = getPreferences().getValue(ANONYMOUS_ID) as? String - if (anonymousId.isNullOrBlank()) { - anonymousId = UUID.randomUUID().toString() - this.anonymousId = anonymousId ?: "" + if (isClientSDK()) { + var anonymousId: String? + synchronized(anonymousLock) { + anonymousId = getPreferences().getValue(ANONYMOUS_ID) as? String + if (anonymousId.isNullOrBlank()) { + anonymousId = UUID.randomUUID().toString() + this.anonymousId = anonymousId ?: "" + } } + return anonymousId ?: "" + } else { + return "" } - return anonymousId ?: "" } set(value) { - getPreferences().setValue(ANONYMOUS_ID, value) + if (isClientSDK()) { + getPreferences().setValue(ANONYMOUS_ID, value) + } } private var distinctId: String get() { - return getPreferences().getValue( - DISTINCT_ID, - defaultValue = anonymousId, - ) as? String ?: "" + return if (isClientSDK()) { + getPreferences().getValue( + DISTINCT_ID, + defaultValue = anonymousId, + ) as? String ?: "" + } else { + "" + } } set(value) { - getPreferences().setValue(DISTINCT_ID, value) + if (isClientSDK()) { + getPreferences().setValue(DISTINCT_ID, value) + } } + private fun getDynamicContext(): Map { + val dynamicContext = mutableMapOf() + dynamicContext["\$locale"] = "${Locale.getDefault().language}-${Locale.getDefault().country}" + System.getProperty("http.agent")?.let { + dynamicContext["\$user_agent"] = it + } + dynamicContext["\$timezone"] = TimeZone.getDefault().id + return dynamicContext + } + private fun buildProperties( distinctId: String, + anonymousId: String?, properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, - groupProperties: Map?, + groups: Map?, appendSharedProps: Boolean = true, appendGroups: Boolean = true, ): Map { @@ -241,12 +288,14 @@ public class PostHog private constructor( props.putAll(it) } + // dynamic context + props.putAll(getDynamicContext()) config?.context?.getDynamicContext()?.let { props.putAll(it) } if (config?.sendFeatureFlagEvent == true) { - featureFlags?.getFeatureFlags()?.let { + featureFlags?.getAllFeatureFlags(distinctId, anonymousId = anonymousId, groups = groups)?.let { if (it.isNotEmpty()) { val keys = mutableListOf() for (entry in it.entries) { @@ -265,14 +314,17 @@ public class PostHog private constructor( } } - synchronized(sessionLock) { - if (sessionId != sessionIdNone) { - val sessionId = sessionId.toString() - props["\$session_id"] = sessionId - if (config?.sessionReplay == true) { - // Session replay requires $window_id, so we set as the same as $session_id. - // the backend might fallback to $session_id if $window_id is not present next. - props["\$window_id"] = sessionId + val isClientSDK = isClientSDK() + if (isClientSDK) { + synchronized(sessionLock) { + if (sessionId != sessionIdNone) { + val sessionId = sessionId.toString() + props["\$session_id"] = sessionId + if (config?.sessionReplay == true) { + // Session replay requires $window_id, so we set as the same as $session_id. + // the backend might fallback to $session_id if $window_id is not present next. + props["\$window_id"] = sessionId + } } } } @@ -291,23 +343,25 @@ public class PostHog private constructor( if (appendGroups) { // merge groups - mergeGroups(groupProperties)?.let { + mergeGroups(groups)?.let { props["\$groups"] = it } } - // Replay needs distinct_id also in the props - // remove after https://github.com/PostHog/posthog/pull/18954 gets merged - val propDistinctId = props["distinct_id"] as? String - if (!appendSharedProps && config?.sessionReplay == true && propDistinctId.isNullOrBlank()) { - // distinctId is already validated hence not empty or blank - props["distinct_id"] = distinctId + if (isClientSDK) { + // Replay needs distinct_id also in the props + // remove after https://github.com/PostHog/posthog/pull/18954 gets merged + val propDistinctId = props["distinct_id"] as? String + if (!appendSharedProps && config?.sessionReplay == true && propDistinctId.isNullOrBlank()) { + // distinctId is already validated hence not empty or blank + props["distinct_id"] = distinctId + } } return props } - private fun mergeGroups(groupProperties: Map?): Map? { + private fun mergeGroups(givenGroups: Map?): Map? { val preferences = getPreferences() @Suppress("UNCHECKED_CAST") @@ -318,7 +372,7 @@ public class PostHog private constructor( newGroups.putAll(it) } - groupProperties?.let { + givenGroups?.let { newGroups.putAll(it) } @@ -331,7 +385,7 @@ public class PostHog private constructor( properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, - groupProperties: Map?, + groups: Map?, ) { try { if (!isEnabled()) { @@ -362,10 +416,11 @@ public class PostHog private constructor( val mergedProperties = buildProperties( newDistinctId, + anonymousId = this.anonymousId, properties = properties, userProperties = userProperties, userPropertiesSetOnce = userPropertiesSetOnce, - groupProperties = groupProperties, + groups = groups, // only append shared props if not a snapshot event appendSharedProps = !snapshotEvent, // only append groups if not a group identify event @@ -429,11 +484,14 @@ public class PostHog private constructor( public override fun screen( screenTitle: String, properties: Map?, + distinctId: String?, ) { if (!isEnabled()) { return } + val newDistinctId = distinctId ?: this.distinctId + val props = mutableMapOf() props["\$screen_name"] = screenTitle @@ -441,18 +499,27 @@ public class PostHog private constructor( props.putAll(it) } - capture("\$screen", properties = props) + capture("\$screen", distinctId = newDistinctId, properties = props) } - public override fun alias(alias: String) { + public override fun alias( + alias: String, + distinctId: String?, + ) { if (!isEnabled()) { return } + val newDistinctId = distinctId ?: this.distinctId + val props = mutableMapOf() props["alias"] = alias - capture("\$create_alias", properties = props) + capture("\$create_alias", distinctId = newDistinctId, properties = props) + } + + private fun isClientSDK(): Boolean { + return config?.isClientSDK == true } public override fun identify( @@ -472,11 +539,15 @@ public class PostHog private constructor( val previousDistinctId = this.distinctId val props = mutableMapOf() - val anonymousId = this.anonymousId - if (anonymousId.isNotBlank()) { - props["\$anon_distinct_id"] = anonymousId - } else { - config?.logger?.log("identify called with invalid anonymousId: $anonymousId.") + + val isClientSDK = isClientSDK() + if (isClientSDK) { + val anonymousId = this.anonymousId + if (anonymousId.isNotBlank()) { + props["\$anon_distinct_id"] = anonymousId + } else { + config?.logger?.log("identify called with invalid anonymousId: $anonymousId.") + } } capture( @@ -487,18 +558,20 @@ public class PostHog private constructor( userPropertiesSetOnce = userPropertiesSetOnce, ) - if (previousDistinctId != distinctId) { - // We keep the AnonymousId to be used by decide calls and identify to link the previousId - if (previousDistinctId.isNotBlank()) { - this.anonymousId = previousDistinctId - } else { - config?.logger?.log("identify called with invalid former distinctId: $previousDistinctId.") - } - this.distinctId = distinctId + if (isClientSDK) { + if (previousDistinctId != distinctId) { + // We keep the AnonymousId to be used by decide calls and identify to link the previousId + if (previousDistinctId.isNotBlank()) { + this.anonymousId = previousDistinctId + } else { + config?.logger?.log("identify called with invalid former distinctId: $previousDistinctId.") + } + this.distinctId = distinctId - // only because of testing in isolation, this flag is always enabled - if (reloadFeatureFlags) { - reloadFeatureFlags() + // only because of testing in isolation, this flag is always enabled + if (reloadFeatureFlags) { + reloadFeatureFlags() + } } } } @@ -507,11 +580,14 @@ public class PostHog private constructor( type: String, key: String, groupProperties: Map?, + distinctId: String?, ) { if (!isEnabled()) { return } + val newDistinctId = distinctId ?: this.distinctId + val props = mutableMapOf() props["\$group_type"] = type props["\$group_key"] = key @@ -541,10 +617,10 @@ public class PostHog private constructor( preferences.setValue(GROUPS, newGroups) } - capture(GROUP_IDENTIFY, properties = props) + capture(GROUP_IDENTIFY, distinctId = newDistinctId, properties = props) // only because of testing in isolation, this flag is always enabled - if (reloadFeatureFlags && reloadFeatureFlagsIfNewGroup) { + if (reloadFeatureFlags && reloadFeatureFlagsIfNewGroup && isClientSDK()) { loadFeatureFlagsRequest(null) } } @@ -574,30 +650,67 @@ public class PostHog private constructor( public override fun isFeatureEnabled( key: String, defaultValue: Boolean, + distinctId: String?, + groups: Map?, ): Boolean { if (!isEnabled()) { return defaultValue } - return featureFlags?.isFeatureEnabled(key, defaultValue) ?: defaultValue + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("isFeatureEnabled call not allowed, distinctId is invalid: $newDistinctId.") + return defaultValue + } + + return featureFlags?.isFeatureEnabled( + key, + defaultValue, + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: defaultValue } + // TODO: only_evaluate_locally public override fun getFeatureFlag( key: String, defaultValue: Any?, + distinctId: String?, + groups: Map?, ): Any? { if (!isEnabled()) { return defaultValue } - val value = featureFlags?.getFeatureFlag(key, defaultValue) ?: defaultValue + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("getFeatureFlag call not allowed, distinctId is invalid: $newDistinctId.") + return defaultValue + } + + val value = + featureFlags?.getFeatureFlag( + key, + defaultValue, + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: defaultValue var shouldSendFeatureFlagEvent = true - synchronized(featureFlagsCalledLock) { - val values = featureFlagsCalled[key] ?: mutableListOf() - if (values.contains(value)) { - shouldSendFeatureFlagEvent = false - } else { - values.add(value) - featureFlagsCalled[key] = values + + if (isClientSDK()) { + synchronized(featureFlagsCalledLock) { + val values = featureFlagsCalled[key] ?: mutableListOf() + if (values.contains(value)) { + shouldSendFeatureFlagEvent = false + } else { + values.add(value) + featureFlagsCalled[key] = values + } } } @@ -616,11 +729,71 @@ public class PostHog private constructor( public override fun getFeatureFlagPayload( key: String, defaultValue: Any?, + distinctId: String?, + groups: Map?, ): Any? { if (!isEnabled()) { return defaultValue } - return featureFlags?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("getFeatureFlagPayload call not allowed, distinctId is invalid: $newDistinctId.") + return defaultValue + } + + return featureFlags?.getFeatureFlagPayload( + key, + defaultValue, + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: defaultValue + } + + override fun getAllFeatureFlags( + distinctId: String?, + groups: Map?, + ): Map? { + if (!isEnabled()) { + return null + } + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("getFeatureFlagPayload call not allowed, distinctId is invalid: $newDistinctId.") + return null + } + + return featureFlags?.getAllFeatureFlags( + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) + } + + override fun getAllFeatureFlagsAndPayloads( + distinctId: String?, + groups: Map?, + ): Pair?, Map?> { + if (!isEnabled()) { + return Pair(null, null) + } + + val newDistinctId = distinctId ?: this.distinctId + + if (newDistinctId.isBlank()) { + config?.logger?.log("getFeatureFlagPayload call not allowed, distinctId is invalid: $newDistinctId.") + return Pair(null, null) + } + + return featureFlags?.getAllFeatureFlagsAndPayloads( + distinctId = newDistinctId, + anonymousId = this.anonymousId, + groups = groups, + ) ?: Pair(null, null) } public override fun flush() { @@ -780,7 +953,7 @@ public class PostHog private constructor( properties: Map?, userProperties: Map?, userPropertiesSetOnce: Map?, - groupProperties: Map?, + groups: Map?, ) { shared.capture( event, @@ -788,7 +961,7 @@ public class PostHog private constructor( properties = properties, userProperties = userProperties, userPropertiesSetOnce = userPropertiesSetOnce, - groupProperties = groupProperties, + groups = groups, ) } @@ -811,17 +984,37 @@ public class PostHog private constructor( public override fun isFeatureEnabled( key: String, defaultValue: Boolean, - ): Boolean = shared.isFeatureEnabled(key, defaultValue = defaultValue) + distinctId: String?, + groups: Map?, + ): Boolean = shared.isFeatureEnabled(key, defaultValue = defaultValue, distinctId = distinctId, groups = groups) public override fun getFeatureFlag( key: String, defaultValue: Any?, - ): Any? = shared.getFeatureFlag(key, defaultValue = defaultValue) + distinctId: String?, + groups: Map?, + ): Any? = shared.getFeatureFlag(key, defaultValue = defaultValue, distinctId = distinctId, groups = groups) public override fun getFeatureFlagPayload( key: String, defaultValue: Any?, - ): Any? = shared.getFeatureFlagPayload(key, defaultValue = defaultValue) + distinctId: String?, + groups: Map?, + ): Any? = shared.getFeatureFlagPayload(key, defaultValue = defaultValue, distinctId = distinctId, groups = groups) + + override fun getAllFeatureFlags( + distinctId: String?, + groups: Map?, + ): Map? { + return shared.getAllFeatureFlags(distinctId = distinctId, groups = groups) + } + + override fun getAllFeatureFlagsAndPayloads( + distinctId: String?, + groups: Map?, + ): Pair?, Map?> { + return shared.getAllFeatureFlagsAndPayloads(distinctId = distinctId, groups = groups) + } public override fun flush() { shared.flush() @@ -843,19 +1036,24 @@ public class PostHog private constructor( type: String, key: String, groupProperties: Map?, + distinctId: String?, ) { - shared.group(type, key, groupProperties = groupProperties) + shared.group(type, key, groupProperties = groupProperties, distinctId = distinctId) } public override fun screen( screenTitle: String, properties: Map?, + distinctId: String?, ) { - shared.screen(screenTitle, properties = properties) + shared.screen(screenTitle, properties = properties, distinctId = distinctId) } - public override fun alias(alias: String) { - shared.alias(alias) + public override fun alias( + alias: String, + distinctId: String?, + ) { + shared.alias(alias, distinctId = distinctId) } public override fun isOptOut(): Boolean = shared.isOptOut() diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index 2cf8d5eb..37fa9a76 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -46,23 +46,23 @@ public open class PostHogConfig( * Docs https://posthog.com/docs/feature-flags and https://posthog.com/docs/experiments * Defaults to true */ - public var preloadFeatureFlags: Boolean = true, + public var preloadFeatureFlags: Boolean = false, /** * Number of minimum events before they are sent over the wire * Defaults to 20 */ - public var flushAt: Int = 20, + public var flushAt: Int = 100, /** * Number of maximum events in memory and disk, when the maximum is exceed, the oldest * event is deleted and the new one takes place * Defaults to 1000 */ - public var maxQueueSize: Int = 1000, + public var maxQueueSize: Int = 10000, /** * Number of maximum events in a batch call * Defaults to 50 */ - public var maxBatchSize: Int = 50, + public var maxBatchSize: Int = 100, // (30).toDuration(DurationUnit.SECONDS) requires Kotlin 1.6 /** * Interval in seconds for sending events over the wire @@ -94,6 +94,9 @@ public open class PostHogConfig( * The hook is called before the event is cached or sent over the wire */ public var propertiesSanitizer: PostHogPropertiesSanitizer? = null, + public var featureFlagsRequestTimeoutSeconds: Int = 3, + public var requestTimeoutSeconds: Int = 15, + public var disableGeoIP: Boolean = true, ) { // fix me: https://stackoverflow.com/questions/53866865/leaking-this-in-constructor-warning-should-apply-to-final-classes-as-well-as @PostHogInternal @@ -136,6 +139,9 @@ public open class PostHogConfig( @PostHogInternal public var snapshotEndpoint: String = "/s/" + @PostHogInternal + public var isClientSDK: Boolean = false + @PostHogInternal public var dateProvider: PostHogDateProvider = PostHogDeviceDateProvider() diff --git a/posthog/src/main/java/com/posthog/PostHogInterface.kt b/posthog/src/main/java/com/posthog/PostHogInterface.kt index a6699734..663562f3 100644 --- a/posthog/src/main/java/com/posthog/PostHogInterface.kt +++ b/posthog/src/main/java/com/posthog/PostHogInterface.kt @@ -21,7 +21,7 @@ public interface PostHogInterface { * @param properties the custom properties * @param userProperties the user properties, set as a "$set" property, Docs https://posthog.com/docs/product-analytics/user-properties * @param userPropertiesSetOnce the user properties to set only once, set as a "$set_once" property, Docs https://posthog.com/docs/product-analytics/user-properties - * @param groupProperties the group properties, set as a "$groups" property, Docs https://posthog.com/docs/product-analytics/group-analytics + * @param groups the group properties, set as a "$groups" property, Docs https://posthog.com/docs/product-analytics/group-analytics */ public fun capture( event: String, @@ -29,7 +29,7 @@ public interface PostHogInterface { properties: Map? = null, userProperties: Map? = null, userPropertiesSetOnce: Map? = null, - groupProperties: Map? = null, + groups: Map? = null, ) /** @@ -60,6 +60,8 @@ public interface PostHogInterface { public fun isFeatureEnabled( key: String, defaultValue: Boolean = false, + distinctId: String? = null, + groups: Map? = null, ): Boolean /** @@ -71,6 +73,8 @@ public interface PostHogInterface { public fun getFeatureFlag( key: String, defaultValue: Any? = null, + distinctId: String? = null, + groups: Map? = null, ): Any? /** @@ -82,8 +86,20 @@ public interface PostHogInterface { public fun getFeatureFlagPayload( key: String, defaultValue: Any? = null, + distinctId: String? = null, + groups: Map? = null, ): Any? + public fun getAllFeatureFlags( + distinctId: String? = null, + groups: Map? = null, + ): Map? + + public fun getAllFeatureFlagsAndPayloads( + distinctId: String? = null, + groups: Map? = null, + ): Pair?, Map?> + /** * Flushes all the events in the Queue right away */ @@ -116,6 +132,7 @@ public interface PostHogInterface { type: String, key: String, groupProperties: Map? = null, + distinctId: String? = null, ) /** @@ -126,6 +143,7 @@ public interface PostHogInterface { public fun screen( screenTitle: String, properties: Map? = null, + distinctId: String? = null, ) /** @@ -133,7 +151,10 @@ public interface PostHogInterface { * Docs https://posthog.com/docs/product-analytics/identify#alias-assigning-multiple-distinct-ids-to-the-same-user * @param alias the alias */ - public fun alias(alias: String) + public fun alias( + alias: String, + distinctId: String? = null, + ) /** * Checks if the [optOut] mode is enabled or disabled @@ -171,8 +192,8 @@ public interface PostHogInterface { /** * Starts a session - * The SDK will automatically start a session when you call [setup] - * On Android, the SDK will automatically start a session when the app is in the foreground + * The SDK will automatically start a session when you call [setup] on Android + * On Android, the SDK will also automatically start a session when the app is in the foreground */ public fun startSession() @@ -191,4 +212,6 @@ public interface PostHogInterface { @PostHogInternal public fun getConfig(): T? + + // TODO: local evaluation } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 0e3e8eca..dac6cd69 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -9,6 +9,7 @@ import okhttp3.RequestBody import okio.BufferedSink import java.io.IOException import java.io.OutputStream +import java.util.concurrent.TimeUnit /** * The class that calls the PostHog API @@ -29,8 +30,11 @@ internal class PostHogApi( private val client: OkHttpClient = OkHttpClient.Builder() .addInterceptor(GzipRequestInterceptor(config)) + .callTimeout(config.featureFlagsRequestTimeoutSeconds.toLong(), TimeUnit.SECONDS) .build() + private val decideClient = client.newBuilder().callTimeout(config.featureFlagsRequestTimeoutSeconds.toLong(), TimeUnit.SECONDS).build() + private val theHost: String get() { return if (config.host.endsWith("/")) config.host.substring(0, config.host.length - 1) else config.host @@ -57,11 +61,6 @@ internal class PostHogApi( it.apiKey = config.apiKey } -// // for easy debugging -// config.serializer.serializeObject(events)?.let { -// print("rrweb events: $it") -// } - // sent_at isn't supported by the snapshot endpoint val request = makeRequest("$theHost${config.snapshotEndpoint}") { @@ -101,14 +100,15 @@ internal class PostHogApi( anonymousId: String?, groups: Map?, ): PostHogDecideResponse? { - val decideRequest = PostHogDecideRequest(config.apiKey, distinctId, anonymousId = anonymousId, groups) + val decideRequest = + PostHogDecideRequest(config.apiKey, distinctId, config.disableGeoIP, anonymousId = anonymousId, groups = groups) val request = makeRequest("$theHost/decide/?v=3") { config.serializer.serialize(decideRequest, it.bufferedWriter()) } - client.newCall(request).execute().use { + decideClient.newCall(request).execute().use { if (!it.isSuccessful) throw PostHogApiError(it.code, it.message, it.body) it.body?.let { body -> diff --git a/posthog/src/main/java/com/posthog/internal/PostHogDecideRequest.kt b/posthog/src/main/java/com/posthog/internal/PostHogDecideRequest.kt index 4a672c5e..1f6607d7 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogDecideRequest.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogDecideRequest.kt @@ -6,6 +6,7 @@ package com.posthog.internal internal class PostHogDecideRequest( apiKey: String, distinctId: String, + disableGeoIP: Boolean, anonymousId: String?, groups: Map?, // add person_properties, group_properties @@ -13,6 +14,7 @@ internal class PostHogDecideRequest( init { this["api_key"] = apiKey this["distinct_id"] = distinctId + this["disable_geoip"] = disableGeoIP if (!anonymousId.isNullOrBlank()) { this["\$anon_distinct_id"] = anonymousId } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt index 13897e4a..4c9c3ced 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt @@ -17,7 +17,7 @@ internal class PostHogFeatureFlags( private val config: PostHogConfig, private val api: PostHogApi, private val executor: ExecutorService, -) { +) : PostHogFeatureFlagsInterface { private var isLoadingFeatureFlags = AtomicBoolean(false) private val featureFlagsLock = Any() @@ -28,7 +28,7 @@ internal class PostHogFeatureFlags( @Volatile private var isFeatureFlagsLoaded = false - fun loadFeatureFlags( + override fun loadFeatureFlags( distinctId: String, anonymousId: String?, groups: Map?, @@ -46,7 +46,7 @@ internal class PostHogFeatureFlags( } try { - val response = api.decide(distinctId, anonymousId = anonymousId, groups) + val response = api.decide(distinctId, anonymousId = anonymousId, groups = groups) response?.let { synchronized(featureFlagsLock) { @@ -55,13 +55,13 @@ internal class PostHogFeatureFlags( this.featureFlags = (this.featureFlags ?: mapOf()) + (response.featureFlags ?: mapOf()) - val normalizedPayloads = normalizePayloads(response.featureFlagPayloads) + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) ?: mapOf() this.featureFlagPayloads = (this.featureFlagPayloads ?: mapOf()) + normalizedPayloads } else { this.featureFlags = response.featureFlags - val normalizedPayloads = normalizePayloads(response.featureFlagPayloads) + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) this.featureFlagPayloads = normalizedPayloads } @@ -132,30 +132,12 @@ internal class PostHogFeatureFlags( } } - private fun normalizePayloads(featureFlagPayloads: Map?): Map { - val parsedPayloads = (featureFlagPayloads ?: mapOf()).toMutableMap() - - for (item in parsedPayloads) { - val value = item.value - - try { - // only try to parse if its a String, since the JSON values are stringified - if (value is String) { - // try to deserialize as Any? - config.serializer.deserializeString(value)?.let { - parsedPayloads[item.key] = it - } - } - } catch (ignored: Throwable) { - // if it fails, we keep the original value - } - } - return parsedPayloads - } - - fun isFeatureEnabled( + override fun isFeatureEnabled( key: String, defaultValue: Boolean, + distinctId: String, + anonymousId: String?, + groups: Map?, ): Boolean { if (!isFeatureFlagsLoaded) { loadFeatureFlagsFromCache() @@ -166,16 +148,7 @@ internal class PostHogFeatureFlags( value = featureFlags?.get(key) } - return if (value != null) { - if (value is Boolean) { - value - } else { - // if its multivariant flag, its enabled by default - true - } - } else { - defaultValue - } + return normalizeBoolean(value, defaultValue) } private fun readFeatureFlag( @@ -195,21 +168,35 @@ internal class PostHogFeatureFlags( return value ?: defaultValue } - fun getFeatureFlag( + override fun getFeatureFlag( key: String, defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, ): Any? { return readFeatureFlag(key, defaultValue, featureFlags) } - fun getFeatureFlagPayload( + override fun getFeatureFlagPayload( key: String, defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, ): Any? { return readFeatureFlag(key, defaultValue, featureFlagPayloads) } - fun getFeatureFlags(): Map? { + override fun getAllFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Map? { + if (!isFeatureFlagsLoaded) { + loadFeatureFlagsFromCache() + } + val flags: Map? synchronized(featureFlagsLock) { flags = featureFlags?.toMap() @@ -217,7 +204,22 @@ internal class PostHogFeatureFlags( return flags } - fun clear() { + override fun getAllFeatureFlagsAndPayloads( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Pair?, Map?> { + val flags = getAllFeatureFlags(distinctId, anonymousId = anonymousId, groups = groups) + + val payloads: Map? + synchronized(featureFlagsLock) { + payloads = featureFlagPayloads?.toMap() + } + + return Pair(flags, payloads) + } + + override fun clear() { synchronized(featureFlagsLock) { featureFlags = null featureFlagPayloads = null diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt new file mode 100644 index 00000000..8feb66f4 --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt @@ -0,0 +1,90 @@ +package com.posthog.internal + +import com.posthog.PostHogOnFeatureFlags + +internal interface PostHogFeatureFlagsInterface { + fun loadFeatureFlags( + distinctId: String, + anonymousId: String? = null, + groups: Map? = null, + onFeatureFlags: PostHogOnFeatureFlags?, + ) + + fun isFeatureEnabled( + key: String, + defaultValue: Boolean, + distinctId: String, + anonymousId: String? = null, + groups: Map? = null, + ): Boolean + + fun getFeatureFlagPayload( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String? = null, + groups: Map? = null, + ): Any? + + fun getFeatureFlag( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String? = null, + groups: Map? = null, + ): Any? + + fun getAllFeatureFlags( + distinctId: String, + anonymousId: String? = null, + groups: Map? = null, + ): Map? + + fun getAllFeatureFlagsAndPayloads( + distinctId: String, + anonymousId: String? = null, + groups: Map? = null, + ): Pair?, Map?> + + fun clear() + + fun normalizePayloads( + serializer: PostHogSerializer, + featureFlagPayloads: Map?, + ): Map? { + val parsedPayloads = featureFlagPayloads?.toMutableMap() ?: return null + + for (item in parsedPayloads) { + val value = item.value + + try { + // only try to parse if it's a String, since the JSON values are stringified + if (value is String) { + // try to deserialize as Any? + serializer.deserializeString(value)?.let { + parsedPayloads[item.key] = it + } + } + } catch (ignored: Throwable) { + // if it fails, we keep the original value + } + } + return parsedPayloads + } + + fun normalizeBoolean( + value: Any?, + defaultValue: Boolean, + ): Boolean { + return if (value != null) { + if (value is Boolean) { + value + } else { + // if its multivariant flag, its enabled by default + true + } + } else { + defaultValue + } + } +} diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt new file mode 100644 index 00000000..56ea1870 --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsSync.kt @@ -0,0 +1,100 @@ +package com.posthog.internal + +import com.posthog.PostHogConfig +import com.posthog.PostHogOnFeatureFlags + +internal class PostHogFeatureFlagsSync( + private val config: PostHogConfig, + private val api: PostHogApi, +) : PostHogFeatureFlagsInterface { + override fun loadFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + onFeatureFlags: PostHogOnFeatureFlags?, + ) { + // NoOp since its all sync - not cached + } + + private fun loadFeatureFlagsFromNetwork( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Pair?, Map?> { + try { + val response = api.decide(distinctId, anonymousId = anonymousId, groups = groups) + + response?.let { + val featureFlags = response.featureFlags + val normalizedPayloads = normalizePayloads(config.serializer, response.featureFlagPayloads) + + return Pair(featureFlags, normalizedPayloads) + } + } catch (e: Throwable) { + config.logger.log("Loading feature flags from network failed: $e") + } + return Pair(null, null) + } + + override fun isFeatureEnabled( + key: String, + defaultValue: Boolean, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Boolean { + val (flags, _) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + val value = flags?.get(key) + + return normalizeBoolean(value, defaultValue) + } + + override fun getFeatureFlagPayload( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Any? { + val (_, payloads) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return payloads?.get(key) ?: defaultValue + } + + override fun getFeatureFlag( + key: String, + defaultValue: Any?, + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Any? { + val (flags, _) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return flags?.get(key) ?: defaultValue + } + + override fun getAllFeatureFlags( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Map? { + val (flags, _) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return flags?.ifEmpty { null } + } + + override fun getAllFeatureFlagsAndPayloads( + distinctId: String, + anonymousId: String?, + groups: Map?, + ): Pair?, Map?> { + val (flags, payloads) = loadFeatureFlagsFromNetwork(distinctId, anonymousId = anonymousId, groups = groups) + + return Pair(flags, payloads) + } + + override fun clear() { + // NoOp since its all sync - not cached + } +} diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFile.kt b/posthog/src/main/java/com/posthog/internal/PostHogFile.kt new file mode 100644 index 00000000..3343d84b --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogFile.kt @@ -0,0 +1,39 @@ +package com.posthog.internal + +import com.posthog.PostHogConfig +import com.posthog.PostHogEvent +import java.io.File +import java.io.FileNotFoundException +import java.io.InputStream +import java.io.OutputStream + +internal class PostHogFile(private val file: File) : PostHogFileInterface { + @Throws(FileNotFoundException::class, SecurityException::class) + override fun outputStream(): OutputStream { + return file.outputStream() + } + + @Throws(FileNotFoundException::class, SecurityException::class) + override fun inputStream(): InputStream { + return file.inputStream() + } + + override fun deleteSafely(config: PostHogConfig) { + file.deleteSafely(config) + } + + override fun name(): String { + return file.name + } + + override fun isStreamable(): Boolean { + return true + } + + override fun event(config: PostHogConfig): PostHogEvent { + val inputStream = config.encryption?.decrypt(inputStream()) ?: inputStream() + inputStream.use { + return config.serializer.deserialize(it.reader().buffered()) + } + } +} diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFileInterface.kt b/posthog/src/main/java/com/posthog/internal/PostHogFileInterface.kt new file mode 100644 index 00000000..e684351e --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogFileInterface.kt @@ -0,0 +1,20 @@ +package com.posthog.internal + +import com.posthog.PostHogConfig +import com.posthog.PostHogEvent +import java.io.InputStream +import java.io.OutputStream + +internal interface PostHogFileInterface { + fun outputStream(): OutputStream + + fun inputStream(): InputStream + + fun deleteSafely(config: PostHogConfig) + + fun name(): String + + fun isStreamable(): Boolean + + fun event(config: PostHogConfig): PostHogEvent +} diff --git a/posthog/src/main/java/com/posthog/internal/PostHogMemoryFile.kt b/posthog/src/main/java/com/posthog/internal/PostHogMemoryFile.kt new file mode 100644 index 00000000..7877b6a4 --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogMemoryFile.kt @@ -0,0 +1,35 @@ +package com.posthog.internal + +import com.posthog.PostHogConfig +import com.posthog.PostHogEvent +import java.io.InputStream +import java.io.OutputStream + +internal class PostHogMemoryFile(private val event: PostHogEvent) : PostHogFileInterface { + override fun outputStream(): OutputStream { + // NoOp since its in-memory + TODO("Not yet implemented") + } + + override fun inputStream(): InputStream { + // NoOp since its in-memory + TODO("Not yet implemented") + } + + override fun deleteSafely(config: PostHogConfig) { + // NoOp since its in-memory + } + + override fun name(): String { + return event.uuid?.toString() ?: event.event + } + + override fun isStreamable(): Boolean { + // this is to avoid round trip serialization + return false + } + + override fun event(config: PostHogConfig): PostHogEvent { + return event + } +} diff --git a/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt b/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt index 94a80e11..524e9d73 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt @@ -27,7 +27,7 @@ internal class PostHogQueue( private val storagePrefix: String?, private val executor: ExecutorService, ) { - private val deque: ArrayDeque = ArrayDeque() + private val deque: ArrayDeque = ArrayDeque() private val dequeLock = Any() private val timerLock = Any() private var pausedUntil: Date? = null @@ -56,41 +56,52 @@ internal class PostHogQueue( if (removeFirst) { try { - val first: File + val first: PostHogFileInterface synchronized(dequeLock) { first = deque.removeFirst() } first.deleteSafely(config) - config.logger.log("Queue is full, the oldest event ${first.name} is dropped.") + config.logger.log("Queue is full, the oldest event ${first.name()} is dropped.") } catch (ignored: NoSuchElementException) { } } - storagePrefix?.let { - val dir = File(it, config.apiKey) + val file: PostHogFileInterface + if (!storagePrefix.isNullOrEmpty()) { + val dir = File(storagePrefix, config.apiKey) if (!dirCreated) { dir.mkdirs() dirCreated = true } - val file = File(dir, "${UUID.randomUUID()}.event") - synchronized(dequeLock) { - deque.add(file) - } + val theFile = File(dir, "${UUID.randomUUID()}.event") + file = PostHogFile(theFile) + addFile(file) try { val os = config.encryption?.encrypt(file.outputStream()) ?: file.outputStream() os.use { theOutputStream -> config.serializer.serialize(event, theOutputStream.writer().buffered()) } - config.logger.log("Queued event ${file.name}.") - - flushIfOverThreshold() + config.logger.log("Queued event ${file.name()}.") } catch (e: Throwable) { config.logger.log("Event ${event.event} failed to parse: $e.") } + } else { + file = PostHogMemoryFile(event) + addFile(file) + + config.logger.log("Queued event ${file.name()}.") } + + flushIfOverThreshold() + } + } + + private fun addFile(file: PostHogFileInterface) { + synchronized(dequeLock) { + deque.add(file) } } @@ -113,8 +124,8 @@ internal class PostHogQueue( return true } - private fun takeFiles(): List { - val events: List + private fun takeFiles(): List { + val events: List synchronized(dequeLock) { events = deque.take(config.maxBatchSize) } @@ -163,25 +174,24 @@ internal class PostHogQueue( val events = mutableListOf() for (file in files) { - try { - val inputStream = config.encryption?.decrypt(file.inputStream()) ?: file.inputStream() - inputStream.use { - val event = config.serializer.deserialize(it.reader().buffered()) - event?.let { theEvent -> - events.add(theEvent) + if (file.isStreamable()) { + try { + events.add(file.event(config)) + } catch (e: Throwable) { + synchronized(dequeLock) { + deque.remove(file) } + file.deleteSafely(config) + config.logger.log("File: ${file.name()} failed to parse: $e.") } - } catch (e: Throwable) { - synchronized(dequeLock) { - deque.remove(file) - } - file.deleteSafely(config) - config.logger.log("File: ${file.name} failed to parse: $e.") + } else { + events.add(file.event(config)) } } var deleteFiles = true try { + // TODO: python checks if the payload isnt bigger than 900kb if (events.isNotEmpty()) { when (endpoint) { PostHogApiEndpoint.BATCH -> api.batch(events) @@ -189,6 +199,8 @@ internal class PostHogQueue( } } } catch (e: PostHogApiError) { + // TODO: python retries up to 3 times with a 500ms interval + // if the API returns between 400 and 500 if (e.statusCode < 400) { deleteFiles = false } @@ -294,7 +306,7 @@ internal class PostHogQueue( fun clear() { executor.executeSafely { - val tempFiles: List + val tempFiles: List synchronized(dequeLock) { tempFiles = deque.toList() deque.clear() @@ -305,10 +317,10 @@ internal class PostHogQueue( } } - val dequeList: List + val dequeList: List @PostHogVisibleForTesting get() { - val tempFiles: List + val tempFiles: List synchronized(dequeLock) { tempFiles = deque.toList() } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt b/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt index 7c8392c7..b68fc1e7 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt @@ -8,6 +8,7 @@ import java.io.InterruptedIOException import java.net.SocketTimeoutException import java.net.UnknownHostException import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService private fun isRequestCanceled(throwable: Throwable): Boolean { return throwable is IOException && @@ -45,6 +46,14 @@ internal fun File.existsSafely(config: PostHogConfig): Boolean { } } +internal fun ExecutorService.shutdownSafely() { + try { + // can throw SecurityException + shutdown() + } catch (ignored: Throwable) { + } +} + internal fun Executor.executeSafely(run: Runnable) { try { // can throw RejectedExecutionException diff --git a/posthog/src/test/java/com/posthog/PostHogJavaTest.kt b/posthog/src/test/java/com/posthog/PostHogJavaTest.kt new file mode 100644 index 00000000..c564a1b3 --- /dev/null +++ b/posthog/src/test/java/com/posthog/PostHogJavaTest.kt @@ -0,0 +1,65 @@ +package com.posthog + +import com.posthog.internal.PostHogMemoryPreferences +import com.posthog.internal.PostHogThreadFactory +import java.util.concurrent.Executors +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +internal class PostHogJavaTest { + private val queueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestQueue")) + private val replayQueueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestReplayQueue")) + private val featureFlagsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestFeatureFlags")) + private val cachedEventsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestCachedEvents")) + private lateinit var config: PostHogConfig + + fun getSut( + host: String, + preloadFeatureFlags: Boolean = true, + ): PostHogInterface { + config = + PostHogConfig(API_KEY, host).apply { + // for testing + this.flushAt = 1 + this.preloadFeatureFlags = preloadFeatureFlags + this.sendFeatureFlagEvent = false + } + return PostHog.withInternal( + config, + queueExecutor, + replayQueueExecutor, + featureFlagsExecutor, + cachedEventsExecutor, + preloadFeatureFlags, + ) + } + + @Test + fun `setup sets in memory cached preferences if not given`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), preloadFeatureFlags = false) + + assertTrue(config.cachePreferences is PostHogMemoryPreferences) + + sut.close() + } + + @Test + fun `preload feature flags if enabled`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString()) + + featureFlagsExecutor.shutdownAndAwaitTermination() + + val request = http.takeRequest() + assertEquals(1, http.requestCount) + assertEquals("/decide/?v=3", request.path) + + sut.close() + } +} diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index 5e968f68..8e5d24e3 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -336,7 +336,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -361,7 +361,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.awaitExecution() @@ -374,7 +374,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -397,7 +397,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -436,7 +436,7 @@ internal class PostHogTest { properties = props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -568,7 +568,7 @@ internal class PostHogTest { sut.group("theType", "theKey", groupProps) - sut.capture("test", groupProperties = mutableMapOf("theType3" to "theKey3")) + sut.capture("test", groups = mutableMapOf("theType3" to "theKey3")) queueExecutor.shutdownAndAwaitTermination() @@ -603,7 +603,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -635,7 +635,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -669,7 +669,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -699,7 +699,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -741,7 +741,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -763,7 +763,7 @@ internal class PostHogTest { properties = props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -898,7 +898,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() @@ -931,7 +931,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.awaitExecution() @@ -953,7 +953,7 @@ internal class PostHogTest { props, userProperties = userProps, userPropertiesSetOnce = userPropsOnce, - groupProperties = groupProps, + groups = groupProps, ) queueExecutor.shutdownAndAwaitTermination() diff --git a/posthog/src/test/java/com/posthog/internal/PostHogDecideRequestTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogDecideRequestTest.kt index fa8b27f3..28a49e52 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogDecideRequestTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogDecideRequestTest.kt @@ -10,7 +10,7 @@ import kotlin.test.assertEquals internal class PostHogDecideRequestTest { @Test fun `sets the decide request content`() { - val request = PostHogDecideRequest(API_KEY, DISTINCT_ID, anonymousId = ANON_ID, groups) + val request = PostHogDecideRequest(API_KEY, DISTINCT_ID, disableGeoIP = true, anonymousId = ANON_ID, groups) assertEquals(API_KEY, request["api_key"]) assertEquals(DISTINCT_ID, request["distinct_id"]) diff --git a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt index 81038676..8fe16d65 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt @@ -28,7 +28,7 @@ internal class PostHogFeatureFlagsTest { private fun getSut( host: String, networkStatus: PostHogNetworkStatus? = null, - ): PostHogFeatureFlags { + ): PostHogFeatureFlagsInterface { val config = PostHogConfig(API_KEY, host).apply { this.networkStatus = networkStatus @@ -62,7 +62,7 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertNull(sut.getFeatureFlags()) + assertNull(sut.getAllFeatureFlags(distinctId = "distinctId")) } @Test @@ -84,8 +84,8 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false) as Boolean) - assertTrue(sut.getFeatureFlagPayload("thePayload", defaultValue = false) as Boolean) + assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false, distinctId = "distinctId") as Boolean) + assertTrue(sut.getFeatureFlagPayload("thePayload", defaultValue = false, distinctId = "distinctId") as Boolean) assertTrue(callback) } @@ -105,13 +105,13 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false) as Boolean) + assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false, distinctId = "distinctId") as Boolean) assertNotNull(preferences.getValue(FEATURE_FLAGS)) assertNotNull(preferences.getValue(FEATURE_FLAGS_PAYLOAD)) sut.clear() - assertNull(sut.getFeatureFlags()) + assertNull(sut.getAllFeatureFlags(distinctId = "distinctId")) assertNull(preferences.getValue(FEATURE_FLAGS)) assertNull(preferences.getValue(FEATURE_FLAGS_PAYLOAD)) } @@ -132,7 +132,7 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - val flags = sut.getFeatureFlags() + val flags = sut.getAllFeatureFlags(distinctId = "distinctId") assertEquals(1, flags!!.size) } @@ -152,7 +152,7 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertTrue(sut.getFeatureFlag("notFound", defaultValue = true) as Boolean) + assertTrue(sut.getFeatureFlag("notFound", defaultValue = true, distinctId = "distinctId") as Boolean) } @Test @@ -182,11 +182,11 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false) as Boolean) - assertTrue(sut.getFeatureFlag("foo", defaultValue = false) as Boolean) + assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz", defaultValue = false, distinctId = "distinctId") as Boolean) + assertTrue(sut.getFeatureFlag("foo", defaultValue = false, distinctId = "distinctId") as Boolean) - assertTrue(sut.getFeatureFlagPayload("thePayload", defaultValue = false) as Boolean) - assertTrue(sut.getFeatureFlagPayload("foo", defaultValue = false) as Boolean) + assertTrue(sut.getFeatureFlagPayload("thePayload", defaultValue = false, distinctId = "distinctId") as Boolean) + assertTrue(sut.getFeatureFlagPayload("foo", defaultValue = false, distinctId = "distinctId") as Boolean) } @Test @@ -207,10 +207,10 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertTrue(sut.isFeatureEnabled("4535-funnel-bar-viz", defaultValue = false)) - assertFalse(sut.isFeatureEnabled("IAmInactive", defaultValue = true)) - assertTrue(sut.isFeatureEnabled("splashScreenName", defaultValue = false)) - assertTrue(sut.isFeatureEnabled("IDontExist", defaultValue = true)) + assertTrue(sut.isFeatureEnabled("4535-funnel-bar-viz", defaultValue = false, distinctId = "distinctId")) + assertFalse(sut.isFeatureEnabled("IAmInactive", defaultValue = true, distinctId = "distinctId")) + assertTrue(sut.isFeatureEnabled("splashScreenName", defaultValue = false, distinctId = "distinctId")) + assertTrue(sut.isFeatureEnabled("IDontExist", defaultValue = true, distinctId = "distinctId")) } @Test @@ -231,21 +231,21 @@ internal class PostHogFeatureFlagsTest { executor.shutdownAndAwaitTermination() - assertEquals("theString", sut.getFeatureFlagPayload("theString", defaultValue = null) as String) - assertEquals(123, sut.getFeatureFlagPayload("theInteger", defaultValue = null) as Int) - assertEquals(123.5, sut.getFeatureFlagPayload("theDouble", defaultValue = null) as Double) + assertEquals("theString", sut.getFeatureFlagPayload("theString", defaultValue = null, distinctId = "distinctId") as String) + assertEquals(123, sut.getFeatureFlagPayload("theInteger", defaultValue = null, distinctId = "distinctId") as Int) + assertEquals(123.5, sut.getFeatureFlagPayload("theDouble", defaultValue = null, distinctId = "distinctId") as Double) val theObject = mapOf("key" to "value") @Suppress("UNCHECKED_CAST") - assertEquals(theObject, sut.getFeatureFlagPayload("theObject", defaultValue = null) as Map) + assertEquals(theObject, sut.getFeatureFlagPayload("theObject", defaultValue = null, distinctId = "distinctId") as Map) val theArray = listOf(1, "2", 3.5) @Suppress("UNCHECKED_CAST") - assertEquals(theArray, sut.getFeatureFlagPayload("theArray", defaultValue = null) as List) + assertEquals(theArray, sut.getFeatureFlagPayload("theArray", defaultValue = null, distinctId = "distinctId") as List) - assertTrue(sut.getFeatureFlagPayload("theBoolean", defaultValue = null) as Boolean) - assertNull(sut.getFeatureFlagPayload("theNull", defaultValue = null)) - assertEquals("[1, 2", sut.getFeatureFlagPayload("theBroken", defaultValue = null) as String) + assertTrue(sut.getFeatureFlagPayload("theBoolean", defaultValue = null, distinctId = "distinctId") as Boolean) + assertNull(sut.getFeatureFlagPayload("theNull", defaultValue = null, distinctId = "distinctId")) + assertEquals("[1, 2", sut.getFeatureFlagPayload("theBroken", defaultValue = null, distinctId = "distinctId") as String) } @Test @@ -264,8 +264,8 @@ internal class PostHogFeatureFlagsTest { val sut = getSut(host = url.toString()) - assertTrue(sut.isFeatureEnabled("foo", defaultValue = false)) - assertTrue(sut.getFeatureFlagPayload("foo", defaultValue = false) as Boolean) + assertTrue(sut.isFeatureEnabled("foo", defaultValue = false, distinctId = "distinctId")) + assertTrue(sut.getFeatureFlagPayload("foo", defaultValue = false, distinctId = "distinctId") as Boolean) } @Test diff --git a/settings.gradle.kts b/settings.gradle.kts index 950ab276..086d0275 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { @@ -20,3 +23,5 @@ include(":posthog-android") // samples include(":posthog-samples:posthog-android-sample") +include(":posthog-samples:posthog-console-sample") +findProject(":posthog-samples:posthog-console-sample")?.name = "posthog-console-sample"