Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remote config api #233

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- feat: use remote config API ([#233](https://github.com/PostHog/posthog-android/pull/233))

## 3.12.0 - 2025-03-10

- feat: support reuse of `anonymousId` between user changes ([#229](https://github.com/PostHog/posthog-android/pull/229))
Expand Down
22 changes: 22 additions & 0 deletions posthog-android/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,26 @@
column="17"/>
</issue>

<issue
id="GradleDependency"
message="A newer version of org.codehaus.mojo:animal-sniffer-annotations than 1.23 is available: 1.24"
errorLine1=" compileOnly(&quot;org.codehaus.mojo:animal-sniffer-annotations:${PosthogBuildConfig.Plugins.ANIMAL_SNIFFER_SDK_ANNOTATION}&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle.kts"
line="94"
column="17"/>
</issue>

<issue
id="GradleDependency"
message="A newer version of com.squareup.okhttp3:mockwebserver than 4.11.0 is available: 4.12.0"
errorLine1=" testImplementation(&quot;com.squareup.okhttp3:mockwebserver:${PosthogBuildConfig.Dependencies.OKHTTP}&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle.kts"
line="109"
column="24"/>
</issue>

</issues>
10 changes: 8 additions & 2 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface

public class com/posthog/PostHogConfig {
public static final field Companion Lcom/posthog/PostHogConfig$Companion;
public static final field DEFAULT_EU_ASSETS_HOST Ljava/lang/String;
public static final field DEFAULT_EU_HOST Ljava/lang/String;
public static final field DEFAULT_HOST Ljava/lang/String;
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public 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;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V
public final fun getApiKey ()Ljava/lang/String;
public final fun getCachePreferences ()Lcom/posthog/internal/PostHogPreferences;
Expand All @@ -96,6 +100,7 @@ public class com/posthog/PostHogConfig {
public final fun getPersonProfiles ()Lcom/posthog/PersonProfiles;
public final fun getPreloadFeatureFlags ()Z
public final fun getPropertiesSanitizer ()Lcom/posthog/PostHogPropertiesSanitizer;
public final fun getRemoteConfig ()Z
public final fun getReplayStoragePrefix ()Ljava/lang/String;
public final fun getReuseAnonymousId ()Z
public final fun getSdkName ()Ljava/lang/String;
Expand Down Expand Up @@ -124,6 +129,7 @@ public class com/posthog/PostHogConfig {
public final fun setPersonProfiles (Lcom/posthog/PersonProfiles;)V
public final fun setPreloadFeatureFlags (Z)V
public final fun setPropertiesSanitizer (Lcom/posthog/PostHogPropertiesSanitizer;)V
public final fun setRemoteConfig (Z)V
public final fun setReplayStoragePrefix (Ljava/lang/String;)V
public final fun setReuseAnonymousId (Z)V
public final fun setSdkName (Ljava/lang/String;)V
Expand Down
52 changes: 38 additions & 14 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.posthog

import com.posthog.internal.PostHogApi
import com.posthog.internal.PostHogApiEndpoint
import com.posthog.internal.PostHogFeatureFlags
import com.posthog.internal.PostHogMemoryPreferences
import com.posthog.internal.PostHogNoOpLogger
import com.posthog.internal.PostHogPreferences
Expand All @@ -17,6 +16,7 @@ import com.posthog.internal.PostHogPreferences.Companion.PERSON_PROCESSING
import com.posthog.internal.PostHogPreferences.Companion.VERSION
import com.posthog.internal.PostHogPrintLogger
import com.posthog.internal.PostHogQueue
import com.posthog.internal.PostHogRemoteConfig
import com.posthog.internal.PostHogSendCachedEventsIntegration
import com.posthog.internal.PostHogSerializer
import com.posthog.internal.PostHogSessionManager
Expand All @@ -35,9 +35,9 @@ public class PostHog private constructor(
Executors.newSingleThreadScheduledExecutor(
PostHogThreadFactory("PostHogReplayQueueThread"),
),
private val featureFlagsExecutor: ExecutorService =
private val remoteConfigExecutor: ExecutorService =
Executors.newSingleThreadScheduledExecutor(
PostHogThreadFactory("PostHogFeatureFlagsThread"),
PostHogThreadFactory("PostHogRemoteConfigThread"),
),
private val cachedEventsExecutor: ExecutorService =
Executors.newSingleThreadScheduledExecutor(
Expand All @@ -59,7 +59,7 @@ public class PostHog private constructor(

private var config: PostHogConfig? = null

private var featureFlags: PostHogFeatureFlags? = null
private var remoteConfig: PostHogRemoteConfig? = null
private var queue: PostHogQueue? = null
private var replayQueue: PostHogQueue? = null
private var memoryPreferences = PostHogMemoryPreferences()
Expand All @@ -86,7 +86,7 @@ public class PostHog private constructor(
val api = PostHogApi(config)
val queue = PostHogQueue(config, api, PostHogApiEndpoint.BATCH, config.storagePrefix, queueExecutor)
val replayQueue = PostHogQueue(config, api, PostHogApiEndpoint.SNAPSHOT, config.replayStoragePrefix, replayExecutor)
val featureFlags = PostHogFeatureFlags(config, api, featureFlagsExecutor)
val featureFlags = PostHogRemoteConfig(config, api, remoteConfigExecutor)

// no need to lock optOut here since the setup is locked already
val optOut =
Expand All @@ -110,7 +110,7 @@ public class PostHog private constructor(
this.config = config
this.queue = queue
this.replayQueue = replayQueue
this.featureFlags = featureFlags
this.remoteConfig = featureFlags

config.addIntegration(sendCachedEventsIntegration)

Expand All @@ -131,7 +131,9 @@ public class PostHog private constructor(
}

// only because of testing in isolation, this flag is always enabled
if (reloadFeatureFlags && config.preloadFeatureFlags) {
if (reloadFeatureFlags && config.remoteConfig) {
loadRemoteConfigRequest(config.onFeatureFlags)
} else if (reloadFeatureFlags && config.preloadFeatureFlags) {
loadFeatureFlagsRequest(config.onFeatureFlags)
}
} catch (e: Throwable) {
Expand Down Expand Up @@ -299,7 +301,7 @@ public class PostHog private constructor(
}

if (config?.sendFeatureFlagEvent == true) {
featureFlags?.getFeatureFlags()?.let {
remoteConfig?.getFeatureFlags()?.let {
if (it.isNotEmpty()) {
val keys = mutableListOf<String>()
for (entry in it.entries) {
Expand Down Expand Up @@ -702,7 +704,29 @@ public class PostHog private constructor(
return
}

featureFlags?.loadFeatureFlags(distinctId, anonymousId = anonymousId, groups, onFeatureFlags)
val calledFromRemoteConfig = config?.remoteConfig ?: false
// only process remote config if the remote config API is disabled
remoteConfig?.loadFeatureFlags(
distinctId,
anonymousId = anonymousId,
groups,
calledFromRemoteConfig = calledFromRemoteConfig,
onFeatureFlags,
)
}

private fun loadRemoteConfigRequest(onFeatureFlags: PostHogOnFeatureFlags?) {
@Suppress("UNCHECKED_CAST")
val groups = getPreferences().getValue(GROUPS) as? Map<String, String>

val distinctId = this.distinctId
var anonymousId: String? = null

if (config?.reuseAnonymousId != true) {
anonymousId = this.anonymousId
}

remoteConfig?.loadRemoteConfig(distinctId, anonymousId = anonymousId, groups, onFeatureFlags)
}

public override fun isFeatureEnabled(
Expand All @@ -712,7 +736,7 @@ public class PostHog private constructor(
if (!isEnabled()) {
return defaultValue
}
val value = featureFlags?.isFeatureEnabled(key, defaultValue) ?: defaultValue
val value = remoteConfig?.isFeatureEnabled(key, defaultValue) ?: defaultValue

sendFeatureFlagCalled(key, value)

Expand Down Expand Up @@ -751,7 +775,7 @@ public class PostHog private constructor(
if (!isEnabled()) {
return defaultValue
}
val value = featureFlags?.getFeatureFlag(key, defaultValue) ?: defaultValue
val value = remoteConfig?.getFeatureFlag(key, defaultValue) ?: defaultValue

sendFeatureFlagCalled(key, value)

Expand All @@ -765,7 +789,7 @@ public class PostHog private constructor(
if (!isEnabled()) {
return defaultValue
}
return featureFlags?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue
return remoteConfig?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue
}

public override fun flush() {
Expand All @@ -790,7 +814,7 @@ public class PostHog private constructor(
except.add(ANONYMOUS_ID)
}
getPreferences().clear(except = except.toList())
featureFlags?.clear()
remoteConfig?.clear()
featureFlagsCalled.clear()
synchronized(identifiedLock) {
isIdentifiedLoaded = false
Expand Down Expand Up @@ -878,7 +902,7 @@ public class PostHog private constructor(
// this is used in cases where we know the session is already active
// so we spare another locker
private fun isSessionReplayFlagActive(): Boolean {
return config?.sessionReplay == true && featureFlags?.isSessionReplayFlagActive() == true
return config?.sessionReplay == true && remoteConfig?.isSessionReplayFlagActive() == true
}

override fun isSessionReplayActive(): Boolean {
Expand Down
15 changes: 14 additions & 1 deletion posthog/src/main/java/com/posthog/PostHogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public open class PostHogConfig(
* Defaults to true
*/
public var preloadFeatureFlags: Boolean = true,
/**
* Preload PostHog remote config automatically
* Defaults to true
*/
@PostHogExperimental
public var remoteConfig: Boolean = true,
/**
* Number of minimum events before they are sent over the wire
* Defaults to 20
Expand Down Expand Up @@ -212,6 +218,13 @@ public open class PostHogConfig(
}

public companion object {
public const val DEFAULT_HOST: String = "https://us.i.posthog.com"
public const val DEFAULT_US_HOST: String = "https://us.i.posthog.com"
public const val DEFAULT_US_ASSETS_HOST: String = "https://us-assets.i.posthog.com"

// flutter uses it
public const val DEFAULT_HOST: String = DEFAULT_US_HOST

public const val DEFAULT_EU_HOST: String = "https://eu.i.posthog.com"
public const val DEFAULT_EU_ASSETS_HOST: String = "https://eu-assets.i.posthog.com"
}
}
44 changes: 43 additions & 1 deletion posthog/src/main/java/com/posthog/internal/PostHogApi.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.posthog.internal

import com.posthog.PostHogConfig
import com.posthog.PostHogConfig.Companion.DEFAULT_EU_ASSETS_HOST
import com.posthog.PostHogConfig.Companion.DEFAULT_EU_HOST
import com.posthog.PostHogConfig.Companion.DEFAULT_US_ASSETS_HOST
import com.posthog.PostHogConfig.Companion.DEFAULT_US_HOST
import com.posthog.PostHogEvent
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
Expand All @@ -17,10 +21,14 @@ import java.io.OutputStream
internal class PostHogApi(
private val config: PostHogConfig,
) {
private companion object {
private const val APP_JSON_UTF_8 = "application/json; charset=utf-8"
}

private val mediaType by lazy {
try {
// can throw IllegalArgumentException
"application/json; charset=utf-8".toMediaType()
APP_JSON_UTF_8.toMediaType()
} catch (ignored: Throwable) {
null
}
Expand Down Expand Up @@ -117,4 +125,38 @@ internal class PostHogApi(
return null
}
}

@Throws(PostHogApiError::class, IOException::class)
fun remoteConfig(): PostHogRemoteConfigResponse? {
var host = theHost
host =
when (host) {
DEFAULT_US_HOST -> {
DEFAULT_US_ASSETS_HOST
}
DEFAULT_EU_HOST -> {
DEFAULT_EU_ASSETS_HOST
}
else -> {
host
}
}

val request =
Request.Builder()
.url("$host/array/${config.apiKey}/config")
.header("User-Agent", config.userAgent)
.header("Content-Type", APP_JSON_UTF_8)
.get()
.build()

client.newCall(request).execute().use {
if (!it.isSuccessful) throw PostHogApiError(it.code, it.message, it.body)

it.body?.let { body ->
return config.serializer.deserialize(body.charStream().buffered())
}
return null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ internal data class PostHogDecideResponse(
val errorsWhileComputingFlags: Boolean = false,
val featureFlags: Map<String, Any>?,
val featureFlagPayloads: Map<String, Any?>?,
// its either a boolean or a map, see https://github.com/PostHog/posthog-js/blob/10fd7f4fa083f997d31a4a4c7be7d311d0a95e74/src/types.ts#L235-L243
val sessionRecording: Any? = false,
val quotaLimited: List<String>? = null,
)
) : PostHogRemoteConfigResponse()
Loading
Loading