diff --git a/CHANGELOG.md b/CHANGELOG.md
index edf1730b..785d151d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## Next
+- feat: use remote config API ([#233](https://github.com/PostHog/posthog-android/pull/233))
+
## 3.12.0 - 2025-03-10
- feat: support reuse of `anonymousId` between user changes ([#229](https://github.com/PostHog/posthog-android/pull/229))
diff --git a/posthog-android/lint-baseline.xml b/posthog-android/lint-baseline.xml
index 1abed766..f0af1a85 100644
--- a/posthog-android/lint-baseline.xml
+++ b/posthog-android/lint-baseline.xml
@@ -45,4 +45,26 @@
column="17"/>
+
+
+
+
+
+
+
+
diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api
index b8e8f004..910e21e5 100644
--- a/posthog/api/posthog.api
+++ b/posthog/api/posthog.api
@@ -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 (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V
- public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public static final field DEFAULT_US_ASSETS_HOST Ljava/lang/String;
+ public static final field DEFAULT_US_HOST Ljava/lang/String;
+ public fun (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V
+ public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V
public final fun getApiKey ()Ljava/lang/String;
public final fun getCachePreferences ()Lcom/posthog/internal/PostHogPreferences;
@@ -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;
@@ -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
diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt
index eb322259..c312a7bf 100644
--- a/posthog/src/main/java/com/posthog/PostHog.kt
+++ b/posthog/src/main/java/com/posthog/PostHog.kt
@@ -2,7 +2,6 @@ package com.posthog
import com.posthog.internal.PostHogApi
import com.posthog.internal.PostHogApiEndpoint
-import com.posthog.internal.PostHogFeatureFlags
import com.posthog.internal.PostHogMemoryPreferences
import com.posthog.internal.PostHogNoOpLogger
import com.posthog.internal.PostHogPreferences
@@ -17,6 +16,7 @@ import com.posthog.internal.PostHogPreferences.Companion.PERSON_PROCESSING
import com.posthog.internal.PostHogPreferences.Companion.VERSION
import com.posthog.internal.PostHogPrintLogger
import com.posthog.internal.PostHogQueue
+import com.posthog.internal.PostHogRemoteConfig
import com.posthog.internal.PostHogSendCachedEventsIntegration
import com.posthog.internal.PostHogSerializer
import com.posthog.internal.PostHogSessionManager
@@ -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(
@@ -59,7 +59,7 @@ public class PostHog private constructor(
private var config: PostHogConfig? = null
- private var featureFlags: PostHogFeatureFlags? = null
+ private var remoteConfig: PostHogRemoteConfig? = null
private var queue: PostHogQueue? = null
private var replayQueue: PostHogQueue? = null
private var memoryPreferences = PostHogMemoryPreferences()
@@ -86,7 +86,7 @@ public class PostHog private constructor(
val api = PostHogApi(config)
val queue = PostHogQueue(config, api, PostHogApiEndpoint.BATCH, config.storagePrefix, queueExecutor)
val replayQueue = PostHogQueue(config, api, PostHogApiEndpoint.SNAPSHOT, config.replayStoragePrefix, replayExecutor)
- val featureFlags = PostHogFeatureFlags(config, api, featureFlagsExecutor)
+ val featureFlags = PostHogRemoteConfig(config, api, remoteConfigExecutor)
// no need to lock optOut here since the setup is locked already
val optOut =
@@ -110,7 +110,7 @@ public class PostHog private constructor(
this.config = config
this.queue = queue
this.replayQueue = replayQueue
- this.featureFlags = featureFlags
+ this.remoteConfig = featureFlags
config.addIntegration(sendCachedEventsIntegration)
@@ -131,8 +131,11 @@ public class PostHog private constructor(
}
// only because of testing in isolation, this flag is always enabled
- if (reloadFeatureFlags && config.preloadFeatureFlags) {
- loadFeatureFlagsRequest(config.onFeatureFlags)
+ if (reloadFeatureFlags) {
+ when {
+ config.remoteConfig -> loadRemoteConfigRequest(config.onFeatureFlags)
+ config.preloadFeatureFlags -> loadFeatureFlagsRequest(config.onFeatureFlags)
+ }
}
} catch (e: Throwable) {
config.logger.log("Setup failed: $e.")
@@ -299,7 +302,7 @@ public class PostHog private constructor(
}
if (config?.sendFeatureFlagEvent == true) {
- featureFlags?.getFeatureFlags()?.let {
+ remoteConfig?.getFeatureFlags()?.let {
if (it.isNotEmpty()) {
val keys = mutableListOf()
for (entry in it.entries) {
@@ -702,7 +705,26 @@ public class PostHog private constructor(
return
}
- featureFlags?.loadFeatureFlags(distinctId, anonymousId = anonymousId, groups, onFeatureFlags)
+ remoteConfig?.loadFeatureFlags(
+ distinctId,
+ anonymousId = anonymousId,
+ groups,
+ onFeatureFlags = onFeatureFlags,
+ )
+ }
+
+ private fun loadRemoteConfigRequest(onFeatureFlags: PostHogOnFeatureFlags?) {
+ @Suppress("UNCHECKED_CAST")
+ val groups = getPreferences().getValue(GROUPS) as? Map
+
+ val distinctId = this.distinctId
+ var anonymousId: String? = null
+
+ if (config?.reuseAnonymousId != true) {
+ anonymousId = this.anonymousId
+ }
+
+ remoteConfig?.loadRemoteConfig(distinctId, anonymousId = anonymousId, groups, onFeatureFlags)
}
public override fun isFeatureEnabled(
@@ -712,7 +734,7 @@ public class PostHog private constructor(
if (!isEnabled()) {
return defaultValue
}
- val value = featureFlags?.isFeatureEnabled(key, defaultValue) ?: defaultValue
+ val value = remoteConfig?.isFeatureEnabled(key, defaultValue) ?: defaultValue
sendFeatureFlagCalled(key, value)
@@ -751,7 +773,7 @@ public class PostHog private constructor(
if (!isEnabled()) {
return defaultValue
}
- val value = featureFlags?.getFeatureFlag(key, defaultValue) ?: defaultValue
+ val value = remoteConfig?.getFeatureFlag(key, defaultValue) ?: defaultValue
sendFeatureFlagCalled(key, value)
@@ -765,7 +787,7 @@ public class PostHog private constructor(
if (!isEnabled()) {
return defaultValue
}
- return featureFlags?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue
+ return remoteConfig?.getFeatureFlagPayload(key, defaultValue) ?: defaultValue
}
public override fun flush() {
@@ -790,7 +812,7 @@ public class PostHog private constructor(
except.add(ANONYMOUS_ID)
}
getPreferences().clear(except = except.toList())
- featureFlags?.clear()
+ remoteConfig?.clear()
featureFlagsCalled.clear()
synchronized(identifiedLock) {
isIdentifiedLoaded = false
@@ -878,7 +900,7 @@ public class PostHog private constructor(
// this is used in cases where we know the session is already active
// so we spare another locker
private fun isSessionReplayFlagActive(): Boolean {
- return config?.sessionReplay == true && featureFlags?.isSessionReplayFlagActive() == true
+ return config?.sessionReplay == true && remoteConfig?.isSessionReplayFlagActive() == true
}
override fun isSessionReplayActive(): Boolean {
diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt
index bfc9b07a..4e72eb1f 100644
--- a/posthog/src/main/java/com/posthog/PostHogConfig.kt
+++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt
@@ -50,6 +50,12 @@ public open class PostHogConfig(
* Defaults to true
*/
public var preloadFeatureFlags: Boolean = true,
+ /**
+ * Preload PostHog remote config automatically
+ * Defaults to true
+ */
+ @PostHogExperimental
+ public var remoteConfig: Boolean = true,
/**
* Number of minimum events before they are sent over the wire
* Defaults to 20
@@ -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"
}
}
diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt
index 075b2532..baa23ae2 100644
--- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt
+++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt
@@ -1,6 +1,10 @@
package com.posthog.internal
import com.posthog.PostHogConfig
+import com.posthog.PostHogConfig.Companion.DEFAULT_EU_ASSETS_HOST
+import com.posthog.PostHogConfig.Companion.DEFAULT_EU_HOST
+import com.posthog.PostHogConfig.Companion.DEFAULT_US_ASSETS_HOST
+import com.posthog.PostHogConfig.Companion.DEFAULT_US_HOST
import com.posthog.PostHogEvent
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@@ -17,10 +21,14 @@ import java.io.OutputStream
internal class PostHogApi(
private val config: PostHogConfig,
) {
+ private companion object {
+ private const val APP_JSON_UTF_8 = "application/json; charset=utf-8"
+ }
+
private val mediaType by lazy {
try {
// can throw IllegalArgumentException
- "application/json; charset=utf-8".toMediaType()
+ APP_JSON_UTF_8.toMediaType()
} catch (ignored: Throwable) {
null
}
@@ -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
+ }
+ }
}
diff --git a/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt b/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt
index 83a770e2..073be3d9 100644
--- a/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt
+++ b/posthog/src/main/java/com/posthog/internal/PostHogDecideResponse.kt
@@ -15,7 +15,5 @@ internal data class PostHogDecideResponse(
val errorsWhileComputingFlags: Boolean = false,
val featureFlags: Map?,
val featureFlagPayloads: Map?,
- // its either a boolean or a map, see https://github.com/PostHog/posthog-js/blob/10fd7f4fa083f997d31a4a4c7be7d311d0a95e74/src/types.ts#L235-L243
- val sessionRecording: Any? = false,
val quotaLimited: List? = null,
-)
+) : PostHogRemoteConfigResponse()
diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt
similarity index 54%
rename from posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
rename to posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt
index aae2c570..9bc13f35 100644
--- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt
+++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt
@@ -14,14 +14,16 @@ import java.util.concurrent.atomic.AtomicBoolean
* @property api the API
* @property executor the Executor
*/
-internal class PostHogFeatureFlags(
+internal class PostHogRemoteConfig(
private val config: PostHogConfig,
private val api: PostHogApi,
private val executor: ExecutorService,
) {
private var isLoadingFeatureFlags = AtomicBoolean(false)
+ private var isLoadingRemoteConfig = AtomicBoolean(false)
private val featureFlagsLock = Any()
+ private val remoteConfigLock = Any()
private var featureFlags: Map? = null
private var featureFlagPayloads: Map? = null
@@ -29,6 +31,9 @@ internal class PostHogFeatureFlags(
@Volatile
private var isFeatureFlagsLoaded = false
+ @Volatile
+ private var isRemoteConfigLoaded = false
+
@Volatile
private var sessionReplayFlagActive = false
@@ -81,7 +86,7 @@ internal class PostHogFeatureFlags(
return recordingActive
}
- fun loadFeatureFlags(
+ fun loadRemoteConfig(
distinctId: String,
anonymousId: String?,
groups: Map?,
@@ -93,105 +98,174 @@ internal class PostHogFeatureFlags(
return@executeSafely
}
- if (isLoadingFeatureFlags.getAndSet(true)) {
- config.logger.log("Feature flags are being loaded already.")
+ if (isLoadingRemoteConfig.getAndSet(true)) {
+ config.logger.log("Remote Config is being loaded already.")
return@executeSafely
}
try {
- val response = api.decide(distinctId, anonymousId = anonymousId, groups)
+ val response = api.remoteConfig()
response?.let {
- synchronized(featureFlagsLock) {
- if (response.quotaLimited?.contains("feature_flags") == true) {
- config.logger.log(
- """Feature flags are quota limited, clearing existing flags.
- Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts""",
- )
- this.featureFlags = null
- this.featureFlagPayloads = null
- config.cachePreferences?.let { preferences ->
- preferences.remove(FEATURE_FLAGS)
- preferences.remove(FEATURE_FLAGS_PAYLOAD)
+ synchronized(remoteConfigLock) {
+ processSessionRecordingConfig(it.sessionRecording)
+
+ val hasFlags = it.hasFeatureFlags ?: false
+
+ if (hasFlags && config.preloadFeatureFlags) {
+ if (distinctId.isNotBlank()) {
+ // do not process session recording from decide API
+ // since its already cached via the remote config API
+ executeFeatureFlags(distinctId, anonymousId, groups, onFeatureFlags, calledFromRemoteConfig = true)
+ } else {
+ config.logger.log("Feature flags not loaded, distinctId is invalid: $distinctId")
}
- return@let
}
- if (response.errorsWhileComputingFlags) {
- // if not all flags were computed, we upsert flags instead of replacing them
- this.featureFlags =
- (this.featureFlags ?: mapOf()) + (response.featureFlags ?: mapOf())
+ isRemoteConfigLoaded = true
+ }
+ } ?: run {
+ isRemoteConfigLoaded = false
+ }
+ } catch (e: Throwable) {
+ config.logger.log("Loading remote config failed: $e")
+ } finally {
+ isLoadingRemoteConfig.set(false)
+ }
+ }
+ }
- val normalizedPayloads = normalizePayloads(response.featureFlagPayloads)
+ private fun processSessionRecordingConfig(sessionRecording: Any?) {
+ when (sessionRecording) {
+ is Boolean -> {
+ // if sessionRecording is a Boolean, its always disabled
+ // so we don't enable sessionReplayFlagActive here
+ sessionReplayFlagActive = sessionRecording
+
+ if (!sessionRecording) {
+ config.cachePreferences?.remove(SESSION_REPLAY)
+ } else {
+ // do nothing
+ }
+ }
- this.featureFlagPayloads =
- (this.featureFlagPayloads ?: mapOf()) + normalizedPayloads
- } else {
- this.featureFlags = response.featureFlags
+ is Map<*, *> -> {
+ @Suppress("UNCHECKED_CAST")
+ (sessionRecording as? Map)?.let {
+ // keeps the value from config.sessionReplay since having sessionRecording
+ // means its enabled on the project settings, but its only enabled
+ // when local config.sessionReplay is also enabled
+ config.snapshotEndpoint = it["endpoint"] as? String
+ ?: config.snapshotEndpoint
- val normalizedPayloads = normalizePayloads(response.featureFlagPayloads)
- this.featureFlagPayloads = normalizedPayloads
- }
+ sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it)
+ config.cachePreferences?.setValue(SESSION_REPLAY, it)
- when (val sessionRecording = response.sessionRecording) {
- is Boolean -> {
- // if sessionRecording is a Boolean, its always disabled
- // so we don't enable sessionReplayFlagActive here
- sessionReplayFlagActive = sessionRecording
-
- if (!sessionRecording) {
- config.cachePreferences?.remove(SESSION_REPLAY)
- } else {
- // do nothing
- }
- }
+ // TODO:
+ // consoleLogRecordingEnabled -> Boolean or null
+ // networkPayloadCapture -> Boolean or null, can also be networkPayloadCapture={recordBody=true, recordHeaders=true}
+ // sampleRate, etc
+ }
+ }
+ else -> {
+ // do nothing
+ }
+ }
+ }
- is Map<*, *> -> {
- @Suppress("UNCHECKED_CAST")
- (sessionRecording as? Map)?.let {
- // keeps the value from config.sessionReplay since having sessionRecording
- // means its enabled on the project settings, but its only enabled
- // when local config.sessionReplay is also enabled
- config.snapshotEndpoint = it["endpoint"] as? String
- ?: config.snapshotEndpoint
-
- sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it)
- config.cachePreferences?.setValue(SESSION_REPLAY, it)
-
- // TODO:
- // consoleLogRecordingEnabled -> Boolean or null
- // networkPayloadCapture -> Boolean or null, can also be networkPayloadCapture={recordBody=true, recordHeaders=true}
- // sampleRate, etc
- }
- }
- else -> {
- // do nothing
- }
+ private fun executeFeatureFlags(
+ distinctId: String,
+ anonymousId: String?,
+ groups: Map?,
+ onFeatureFlags: PostHogOnFeatureFlags?,
+ calledFromRemoteConfig: Boolean,
+ ) {
+ if (config.networkStatus?.isConnected() == false) {
+ config.logger.log("Network isn't connected.")
+ return
+ }
+
+ if (isLoadingFeatureFlags.getAndSet(true)) {
+ config.logger.log("Feature flags are being loaded already.")
+ return
+ }
+
+ try {
+ val response = api.decide(distinctId, anonymousId = anonymousId, groups)
+
+ response?.let {
+ synchronized(featureFlagsLock) {
+ if (it.quotaLimited?.contains("feature_flags") == true) {
+ config.logger.log(
+ """Feature flags are quota limited, clearing existing flags.
+ Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts""",
+ )
+ this.featureFlags = null
+ this.featureFlagPayloads = null
+ config.cachePreferences?.let { preferences ->
+ preferences.remove(FEATURE_FLAGS)
+ preferences.remove(FEATURE_FLAGS_PAYLOAD)
}
+ return@let
+ }
+
+ if (it.errorsWhileComputingFlags) {
+ // if not all flags were computed, we upsert flags instead of replacing them
+ this.featureFlags =
+ (this.featureFlags ?: mapOf()) + (it.featureFlags ?: mapOf())
+
+ val normalizedPayloads = normalizePayloads(it.featureFlagPayloads)
+
+ this.featureFlagPayloads =
+ (this.featureFlagPayloads ?: mapOf()) + normalizedPayloads
+ } else {
+ this.featureFlags = it.featureFlags
+
+ val normalizedPayloads = normalizePayloads(it.featureFlagPayloads)
+ this.featureFlagPayloads = normalizedPayloads
}
- config.cachePreferences?.let { preferences ->
- val flags = this.featureFlags ?: mapOf()
- preferences.setValue(FEATURE_FLAGS, flags)
- val payloads = this.featureFlagPayloads ?: mapOf()
- preferences.setValue(FEATURE_FLAGS_PAYLOAD, payloads)
+ // only process and cache session recording config from decide API
+ // if not yet done by the remote config API
+ if (!calledFromRemoteConfig) {
+ processSessionRecordingConfig(it.sessionRecording)
}
- isFeatureFlagsLoaded = true
}
+ config.cachePreferences?.let { preferences ->
+ val flags = this.featureFlags ?: mapOf()
+ preferences.setValue(FEATURE_FLAGS, flags)
+
+ val payloads = this.featureFlagPayloads ?: mapOf()
+ preferences.setValue(FEATURE_FLAGS_PAYLOAD, payloads)
+ }
+ isFeatureFlagsLoaded = true
+ } ?: run {
+ isFeatureFlagsLoaded = false
+ }
+ } catch (e: Throwable) {
+ config.logger.log("Loading feature flags failed: $e")
+ } finally {
+ try {
+ onFeatureFlags?.loaded()
} catch (e: Throwable) {
- config.logger.log("Loading feature flags failed: $e")
+ config.logger.log("Executing the feature flags callback failed: $e")
} finally {
- try {
- onFeatureFlags?.loaded()
- } catch (e: Throwable) {
- config.logger.log("Executing the feature flags callback failed: $e")
- } finally {
- isLoadingFeatureFlags.set(false)
- }
+ isLoadingFeatureFlags.set(false)
}
}
}
+ fun loadFeatureFlags(
+ distinctId: String,
+ anonymousId: String?,
+ groups: Map?,
+ onFeatureFlags: PostHogOnFeatureFlags? = null,
+ ) {
+ executor.executeSafely {
+ executeFeatureFlags(distinctId, anonymousId, groups, onFeatureFlags, false)
+ }
+ }
+
private fun preloadSessionReplayFlag() {
synchronized(featureFlagsLock) {
config.cachePreferences?.let { preferences ->
@@ -328,6 +402,7 @@ internal class PostHogFeatureFlags(
featureFlags = null
featureFlagPayloads = null
sessionReplayFlagActive = false
+ isFeatureFlagsLoaded = false
config.cachePreferences?.let { preferences ->
preferences.remove(FEATURE_FLAGS)
@@ -335,5 +410,9 @@ internal class PostHogFeatureFlags(
preferences.remove(SESSION_REPLAY)
}
}
+
+ synchronized(remoteConfigLock) {
+ isRemoteConfigLoaded = false
+ }
}
}
diff --git a/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt
new file mode 100644
index 00000000..0b390823
--- /dev/null
+++ b/posthog/src/main/java/com/posthog/internal/PostHogRemoteConfigResponse.kt
@@ -0,0 +1,13 @@
+package com.posthog.internal
+
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+
+@IgnoreJRERequirement
+internal open class PostHogRemoteConfigResponse(
+ // its either a boolean or a map, see https://github.com/PostHog/posthog-js/blob/10fd7f4fa083f997d31a4a4c7be7d311d0a95e74/src/types.ts#L235-L243
+ val sessionRecording: Any? = false,
+ // its either a boolean or a map
+ val surveys: Any? = false,
+ // Indicates if the team has any flags enabled (if not we don't need to load them)
+ val hasFeatureFlags: Boolean? = false,
+)
diff --git a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt
index c14be5b1..385a65d3 100644
--- a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt
+++ b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt
@@ -18,7 +18,7 @@ internal class PostHogPersonProfilesTest {
private val queueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestQueue"))
private val replayQueueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestReplayQueue"))
- private val featureFlagsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestFeatureFlags"))
+ private val remoteConfigExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestRemoteConfig"))
private val cachedEventsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestCachedEvents"))
private val serializer = PostHogSerializer(PostHogConfig(API_KEY))
private lateinit var config: PostHogConfig
@@ -46,7 +46,7 @@ internal class PostHogPersonProfilesTest {
config,
queueExecutor,
replayQueueExecutor,
- featureFlagsExecutor,
+ remoteConfigExecutor,
cachedEventsExecutor,
false,
)
diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt
index 5ea57897..d24cc33b 100644
--- a/posthog/src/test/java/com/posthog/PostHogTest.kt
+++ b/posthog/src/test/java/com/posthog/PostHogTest.kt
@@ -28,7 +28,7 @@ internal class PostHogTest {
private val queueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestQueue"))
private val replayQueueExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestReplayQueue"))
- private val featureFlagsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestFeatureFlags"))
+ private val remoteConfigExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestRemoteConfig"))
private val cachedEventsExecutor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("TestCachedEvents"))
private val serializer = PostHogSerializer(PostHogConfig(API_KEY))
private lateinit var config: PostHogConfig
@@ -46,6 +46,7 @@ internal class PostHogTest {
sendFeatureFlagEvent: Boolean = true,
reuseAnonymousId: Boolean = false,
integration: PostHogIntegration? = null,
+ remoteConfig: Boolean = false,
cachePreferences: PostHogMemoryPreferences = PostHogMemoryPreferences(),
propertiesSanitizer: PostHogPropertiesSanitizer? = null,
): PostHogInterface {
@@ -63,12 +64,13 @@ internal class PostHogTest {
this.reuseAnonymousId = reuseAnonymousId
this.cachePreferences = cachePreferences
this.propertiesSanitizer = propertiesSanitizer
+ this.remoteConfig = remoteConfig
}
return PostHog.withInternal(
config,
queueExecutor,
replayQueueExecutor,
- featureFlagsExecutor,
+ remoteConfigExecutor,
cachedEventsExecutor,
reloadFeatureFlags,
)
@@ -162,7 +164,7 @@ internal class PostHogTest {
val sut = getSut(url.toString())
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
val request = http.takeRequest()
assertEquals(1, http.requestCount)
@@ -184,13 +186,90 @@ internal class PostHogTest {
reloaded = true
}
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
assertTrue(reloaded)
sut.close()
}
+ @Test
+ fun `preload remote config if enabled`() {
+ val http = mockHttp()
+ val url = http.url("/")
+
+ val sut = getSut(url.toString(), remoteConfig = true, preloadFeatureFlags = false)
+
+ remoteConfigExecutor.shutdownAndAwaitTermination()
+
+ val request = http.takeRequest()
+ assertEquals(1, http.requestCount)
+ assertEquals("/array/${API_KEY}/config", request.path)
+
+ sut.close()
+ }
+
+ @Test
+ fun `preload remote config and flags if enabled`() {
+ val file = File("src/test/resources/json/basic-remote-config.json")
+ val responseText = file.readText()
+
+ val http =
+ mockHttp(
+ response =
+ MockResponse()
+ .setBody(responseText),
+ )
+ http.enqueue(
+ MockResponse()
+ .setBody(responseDecideApi),
+ )
+ val url = http.url("/")
+
+ val sut = getSut(url.toString(), remoteConfig = true)
+
+ remoteConfigExecutor.shutdownAndAwaitTermination()
+
+ val remoteConfigRequest = http.takeRequest()
+
+ assertEquals(2, http.requestCount)
+ assertEquals("/array/${API_KEY}/config", remoteConfigRequest.path)
+
+ val decideApiRequest = http.takeRequest()
+ assertEquals("/decide/?v=3", decideApiRequest.path)
+
+ sut.close()
+ }
+
+ @Test
+ fun `preload remote config but no flags`() {
+ val file = File("src/test/resources/json/basic-remote-config-no-flags.json")
+ val responseText = file.readText()
+
+ val http =
+ mockHttp(
+ response =
+ MockResponse()
+ .setBody(responseText),
+ )
+ http.enqueue(
+ MockResponse()
+ .setBody(responseDecideApi),
+ )
+ val url = http.url("/")
+
+ val sut = getSut(url.toString(), remoteConfig = true)
+
+ remoteConfigExecutor.shutdownAndAwaitTermination()
+
+ val remoteConfigRequest = http.takeRequest()
+
+ assertEquals(1, http.requestCount)
+ assertEquals("/array/${API_KEY}/config", remoteConfigRequest.path)
+
+ sut.close()
+ }
+
@Test
fun `isFeatureEnabled returns value after reloaded`() {
val http =
@@ -205,7 +284,7 @@ internal class PostHogTest {
sut.reloadFeatureFlags()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
assertTrue(sut.isFeatureEnabled("4535-funnel-bar-viz"))
@@ -226,7 +305,7 @@ internal class PostHogTest {
sut.reloadFeatureFlags()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
assertTrue(sut.getFeatureFlag("4535-funnel-bar-viz") as Boolean)
@@ -254,7 +333,7 @@ internal class PostHogTest {
sut.reloadFeatureFlags()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
// remove from the http queue
http.takeRequest()
@@ -312,7 +391,7 @@ internal class PostHogTest {
sut.reloadFeatureFlags()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
// remove from the http queue
http.takeRequest()
@@ -357,7 +436,7 @@ internal class PostHogTest {
sut.reloadFeatureFlags()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
assertTrue(sut.getFeatureFlagPayload("thePayload") as Boolean)
@@ -371,7 +450,7 @@ internal class PostHogTest {
val sut = getSut(url.toString(), preloadFeatureFlags = false)
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
assertEquals(0, http.requestCount)
@@ -1113,7 +1192,7 @@ internal class PostHogTest {
sut.reloadFeatureFlags()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
// remove from the http queue
http.takeRequest()
@@ -1292,7 +1371,7 @@ internal class PostHogTest {
sut.reset()
- featureFlagsExecutor.shutdownAndAwaitTermination()
+ remoteConfigExecutor.shutdownAndAwaitTermination()
val request = http.takeRequest()
assertEquals(1, http.requestCount)
diff --git a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt
index f501b41d..efe93180 100644
--- a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt
+++ b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt
@@ -102,4 +102,47 @@ internal class PostHogApiTest {
assertEquals("Client Error", exc.message)
assertNotNull(exc.body)
}
+
+ @Test
+ fun `remote config returns successful response`() {
+ val file = File("src/test/resources/json/basic-remote-config.json")
+ val responseApi = file.readText()
+
+ val http =
+ mockHttp(
+ response =
+ MockResponse()
+ .setBody(responseApi),
+ )
+ val url = http.url("/")
+
+ val sut = getSut(host = url.toString())
+
+ val response = sut.remoteConfig()
+
+ val request = http.takeRequest()
+
+ assertNotNull(response)
+ assertEquals("posthog-java/${BuildConfig.VERSION_NAME}", request.headers["User-Agent"])
+ assertEquals("GET", request.method)
+ assertEquals("/array/${API_KEY}/config", request.path)
+ assertEquals("gzip", request.headers["Accept-Encoding"])
+ assertEquals("application/json; charset=utf-8", request.headers["Content-Type"])
+ }
+
+ @Test
+ fun `remote config throws if not successful`() {
+ val http = mockHttp(response = MockResponse().setResponseCode(400).setBody("error"))
+ val url = http.url("/")
+
+ val sut = getSut(host = url.toString())
+
+ val exc =
+ assertThrows(PostHogApiError::class.java) {
+ sut.remoteConfig()
+ }
+ assertEquals(400, exc.statusCode)
+ assertEquals("Client Error", exc.message)
+ assertNotNull(exc.body)
+ }
}
diff --git a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt
similarity index 88%
rename from posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt
rename to posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt
index 7f33791f..a7b4da1e 100644
--- a/posthog/src/test/java/com/posthog/internal/PostHogFeatureFlagsTest.kt
+++ b/posthog/src/test/java/com/posthog/internal/PostHogRemoteConfigTest.kt
@@ -19,7 +19,7 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
-internal class PostHogFeatureFlagsTest {
+internal class PostHogRemoteConfigTest {
private val executor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("Test"))
private val file = File("src/test/resources/json/basic-decide-no-errors.json")
@@ -31,14 +31,14 @@ internal class PostHogFeatureFlagsTest {
private fun getSut(
host: String,
networkStatus: PostHogNetworkStatus? = null,
- ): PostHogFeatureFlags {
+ ): PostHogRemoteConfig {
config =
PostHogConfig(API_KEY, host).apply {
this.networkStatus = networkStatus
cachePreferences = preferences
}
val api = PostHogApi(config!!)
- return PostHogFeatureFlags(config!!, api, executor = executor)
+ return PostHogRemoteConfig(config!!, api, executor = executor)
}
@BeforeTest
@@ -61,7 +61,7 @@ internal class PostHogFeatureFlagsTest {
false
})
- sut.loadFeatureFlags("distinctId", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("distinctId", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -105,7 +105,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -132,7 +132,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -152,7 +152,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -171,7 +171,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.awaitExecution()
@@ -182,7 +182,7 @@ internal class PostHogFeatureFlagsTest {
.setBody(file.readText())
http.enqueue(response)
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -207,7 +207,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -231,7 +231,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -284,7 +284,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -312,7 +312,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -357,7 +357,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -380,7 +380,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -403,7 +403,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -426,7 +426,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
- sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap(), null)
+ sut.loadFeatureFlags("my_identify", anonymousId = "anonId", emptyMap())
executor.shutdownAndAwaitTermination()
@@ -453,7 +453,7 @@ internal class PostHogFeatureFlagsTest {
val sut = getSut(host = url.toString())
// Load initial flags
- sut.loadFeatureFlags("test_id", null, null, null)
+ sut.loadFeatureFlags("test_id", null, null)
executor.awaitExecution()
// Verify flags are loaded
@@ -472,11 +472,61 @@ internal class PostHogFeatureFlagsTest {
)
// Reload flags
- sut.loadFeatureFlags("test_id", null, null, null)
+ sut.loadFeatureFlags("test_id", null, null)
executor.awaitExecution()
// Verify flags are cleared
assertNull(sut.getFeatureFlags())
assertNull(preferences.getValue(FEATURE_FLAGS))
}
+
+ @Test
+ fun `returns session replay enabled after remote config API call`() {
+ val file = File("src/test/resources/json/basic-remote-config-no-flags.json")
+
+ val http =
+ mockHttp(
+ response =
+ MockResponse()
+ .setBody(file.readText()),
+ )
+ val url = http.url("/")
+
+ val sut = getSut(host = url.toString())
+
+ sut.loadRemoteConfig("my_identify", anonymousId = "anonId", emptyMap(), null)
+
+ executor.shutdownAndAwaitTermination()
+
+ assertTrue(sut.isSessionReplayFlagActive())
+ assertEquals("/s/", config?.snapshotEndpoint)
+ assertEquals(1, http.requestCount)
+
+ sut.clear()
+
+ assertFalse(sut.isSessionReplayFlagActive())
+ }
+
+ @Test
+ fun `do not preload flags if distinct id is blank`() {
+ val file = File("src/test/resources/json/basic-remote-config.json")
+
+ val http =
+ mockHttp(
+ response =
+ MockResponse()
+ .setBody(file.readText()),
+ )
+ val url = http.url("/")
+
+ val sut = getSut(host = url.toString())
+
+ sut.loadRemoteConfig(" ", anonymousId = "anonId", emptyMap(), null)
+
+ executor.shutdownAndAwaitTermination()
+
+ assertEquals(1, http.requestCount)
+
+ sut.clear()
+ }
}
diff --git a/posthog/src/test/resources/json/basic-remote-config-no-flags.json b/posthog/src/test/resources/json/basic-remote-config-no-flags.json
new file mode 100644
index 00000000..e043f862
--- /dev/null
+++ b/posthog/src/test/resources/json/basic-remote-config-no-flags.json
@@ -0,0 +1,44 @@
+{
+ "token": "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D",
+ "supportedCompression": [
+ "gzip",
+ "gzip-js"
+ ],
+ "hasFeatureFlags": false,
+ "captureDeadClicks": false,
+ "capturePerformance": {
+ "network_timing": true,
+ "web_vitals": false,
+ "web_vitals_allowed_metrics": null
+ },
+ "autocapture_opt_out": true,
+ "autocaptureExceptions": false,
+ "analytics": {
+ "endpoint": "/i/v0/e/"
+ },
+ "elementsChainAsString": true,
+ "sessionRecording": {
+ "endpoint": "/s/",
+ "consoleLogRecordingEnabled": true,
+ "recorderVersion": "v2",
+ "sampleRate": null,
+ "minimumDurationMilliseconds": 1000,
+ "linkedFlag": null,
+ "networkPayloadCapture": {
+ "recordBody": true,
+ "recordHeaders": true
+ },
+ "masking": null,
+ "urlTriggers": [],
+ "urlBlocklist": [],
+ "eventTriggers": [],
+ "scriptConfig": null,
+ "recordCanvas": true,
+ "canvasFps": 3,
+ "canvasQuality": "0.4"
+ },
+ "heatmaps": false,
+ "surveys": false,
+ "defaultIdentifiedOnly": true,
+ "siteApps": []
+}
\ No newline at end of file
diff --git a/posthog/src/test/resources/json/basic-remote-config.json b/posthog/src/test/resources/json/basic-remote-config.json
new file mode 100644
index 00000000..5622d615
--- /dev/null
+++ b/posthog/src/test/resources/json/basic-remote-config.json
@@ -0,0 +1,44 @@
+{
+ "token": "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D",
+ "supportedCompression": [
+ "gzip",
+ "gzip-js"
+ ],
+ "hasFeatureFlags": true,
+ "captureDeadClicks": false,
+ "capturePerformance": {
+ "network_timing": true,
+ "web_vitals": false,
+ "web_vitals_allowed_metrics": null
+ },
+ "autocapture_opt_out": true,
+ "autocaptureExceptions": false,
+ "analytics": {
+ "endpoint": "/i/v0/e/"
+ },
+ "elementsChainAsString": true,
+ "sessionRecording": {
+ "endpoint": "/s/",
+ "consoleLogRecordingEnabled": true,
+ "recorderVersion": "v2",
+ "sampleRate": null,
+ "minimumDurationMilliseconds": 1000,
+ "linkedFlag": null,
+ "networkPayloadCapture": {
+ "recordBody": true,
+ "recordHeaders": true
+ },
+ "masking": null,
+ "urlTriggers": [],
+ "urlBlocklist": [],
+ "eventTriggers": [],
+ "scriptConfig": null,
+ "recordCanvas": true,
+ "canvasFps": 3,
+ "canvasQuality": "0.4"
+ },
+ "heatmaps": false,
+ "surveys": false,
+ "defaultIdentifiedOnly": true,
+ "siteApps": []
+}
\ No newline at end of file