Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ internal data class FeatureFlagCacheEntry(
val flags: Map<String, FeatureFlag>?,
val timestamp: Long,
val expiresAt: Long,
val requestId: String? = null,
val evaluatedAt: Long? = null,
) {
/**
* Check if this cache entry has expired
Expand All @@ -21,6 +23,8 @@ internal data class FeatureFlagCacheEntry(
var result = flags?.hashCode() ?: 0
result = 31 * result + (timestamp xor (timestamp ushr 32)).toInt()
result = 31 * result + (expiresAt xor (expiresAt ushr 32)).toInt()
result = 31 * result + (requestId?.hashCode() ?: 0)
result = 31 * result + (evaluatedAt?.hashCode() ?: 0)
return result
}

Expand All @@ -31,6 +35,8 @@ internal data class FeatureFlagCacheEntry(
if (flags != other.flags) return false
if (timestamp != other.timestamp) return false
if (expiresAt != other.expiresAt) return false
if (requestId != other.requestId) return false
if (evaluatedAt != other.evaluatedAt) return false

return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,42 @@ internal class PostHogFeatureFlagCache(
return entry.flags
}

/**
* Get full cache entry (including requestId and evaluatedAt) if present and not expired
*/
@Synchronized
fun getEntry(key: FeatureFlagCacheKey): FeatureFlagCacheEntry? {
val entry = cache[key]
if (entry == null) {
return null
}

if (entry.isExpired()) {
cache.remove(key)
return null
}

return entry
}

/**
* Put feature flags into cache with current timestamp
*/
@Synchronized
fun put(
key: FeatureFlagCacheKey,
flags: Map<String, FeatureFlag>?,
requestId: String? = null,
evaluatedAt: Long? = null,
) {
val currentTime = System.currentTimeMillis()
val entry =
FeatureFlagCacheEntry(
flags = flags,
timestamp = currentTime,
expiresAt = currentTime + maxAgeMs,
requestId = requestId,
evaluatedAt = evaluatedAt,
)

cache[key] = entry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ internal class PostHogFeatureFlags(
return try {
val response = api.flags(distinctId, null, groups, personProperties, groupProperties)
val flags = response?.flags
cache.put(cacheKey, flags)
cache.put(cacheKey, flags, response?.requestId, response?.evaluatedAt)
flags
} catch (e: Throwable) {
config.logger.log("Loading remote feature flags failed: $e")
Expand Down Expand Up @@ -499,4 +499,48 @@ internal class PostHogFeatureFlags(
private fun localEvaluationEnabled(): Boolean {
return localEvaluation && !personalApiKey.isNullOrBlank()
}

/**
* Get the requestId from the cache for the given distinctId and groups
*/
override fun getRequestId(
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): String? {
if (distinctId == null) {
return null
}
val cacheKey =
FeatureFlagCacheKey(
distinctId = distinctId,
groups = groups,
personProperties = personProperties,
groupProperties = groupProperties,
)
return cache.getEntry(cacheKey)?.requestId
}

/**
* Get the evaluatedAt from the cache for the given distinctId and groups
*/
override fun getEvaluatedAt(
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Long? {
if (distinctId == null) {
return null
}
val cacheKey =
FeatureFlagCacheKey(
distinctId = distinctId,
groups = groups,
personProperties = personProperties,
groupProperties = groupProperties,
)
return cache.getEntry(cacheKey)?.evaluatedAt
}
}
17 changes: 12 additions & 5 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -609,32 +609,38 @@ public final class com/posthog/internal/PostHogDeviceDateProvider : com/posthog/

public abstract interface class com/posthog/internal/PostHogFeatureFlagsInterface {
public abstract fun clear ()V
public abstract fun getEvaluatedAt (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Long;
public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Object;
public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Object;
public abstract fun getFeatureFlags (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
public abstract fun getRequestId (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/String;
public abstract fun shutDown ()V
}

public final class com/posthog/internal/PostHogFeatureFlagsInterface$DefaultImpls {
public static synthetic fun getEvaluatedAt$default (Lcom/posthog/internal/PostHogFeatureFlagsInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Long;
public static synthetic fun getFeatureFlag$default (Lcom/posthog/internal/PostHogFeatureFlagsInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Object;
public static synthetic fun getFeatureFlagPayload$default (Lcom/posthog/internal/PostHogFeatureFlagsInterface;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/Object;
public static synthetic fun getFeatureFlags$default (Lcom/posthog/internal/PostHogFeatureFlagsInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ljava/util/Map;
public static synthetic fun getRequestId$default (Lcom/posthog/internal/PostHogFeatureFlagsInterface;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Ljava/lang/String;
public static fun shutDown (Lcom/posthog/internal/PostHogFeatureFlagsInterface;)V
}

public final class com/posthog/internal/PostHogFlagsResponse : com/posthog/internal/PostHogRemoteConfigResponse {
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;)V
public synthetic fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;)V
public synthetic fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Z
public final fun component2 ()Ljava/util/Map;
public final fun component3 ()Ljava/util/Map;
public final fun component4 ()Ljava/util/Map;
public final fun component5 ()Ljava/util/List;
public final fun component6 ()Ljava/lang/String;
public final fun copy (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;)Lcom/posthog/internal/PostHogFlagsResponse;
public static synthetic fun copy$default (Lcom/posthog/internal/PostHogFlagsResponse;ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public final fun component7 ()Ljava/lang/Long;
public final fun copy (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;)Lcom/posthog/internal/PostHogFlagsResponse;
public static synthetic fun copy$default (Lcom/posthog/internal/PostHogFlagsResponse;ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public fun equals (Ljava/lang/Object;)Z
public final fun getErrorsWhileComputingFlags ()Z
public final fun getEvaluatedAt ()Ljava/lang/Long;
public final fun getFeatureFlagPayloads ()Ljava/util/Map;
public final fun getFeatureFlags ()Ljava/util/Map;
public final fun getFlags ()Ljava/util/Map;
Expand Down Expand Up @@ -718,12 +724,13 @@ public final class com/posthog/internal/PostHogRemoteConfig : com/posthog/intern
public fun <init> (Lcom/posthog/PostHogConfig;Lcom/posthog/internal/PostHogApi;Ljava/util/concurrent/ExecutorService;Lkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (Lcom/posthog/PostHogConfig;Lcom/posthog/internal/PostHogApi;Ljava/util/concurrent/ExecutorService;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun clear ()V
public fun getEvaluatedAt (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Long;
public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Object;
public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Object;
public fun getFeatureFlags (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
public final fun getFlagDetails (Ljava/lang/String;)Lcom/posthog/internal/FeatureFlag;
public final fun getOnRemoteConfigLoaded ()Lkotlin/jvm/functions/Function0;
public final fun getRequestId ()Ljava/lang/String;
public fun getRequestId (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/String;
public final fun getSurveys ()Ljava/util/List;
public final fun isSessionReplayFlagActive ()Z
public final fun loadFeatureFlags (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/posthog/PostHogOnFeatureFlags;Lcom/posthog/PostHogOnFeatureFlags;)V
Expand Down
2 changes: 2 additions & 0 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -888,12 +888,14 @@ public class PostHog private constructor(
remoteConfig?.let {
val flagDetails = it.getFlagDetails(key)
val requestId = it.getRequestId()
val evaluatedAt = it.getEvaluatedAt()

val props = mutableMapOf<String, Any>()
props["\$feature_flag"] = key
// value should never be nullabe anyway
props["\$feature_flag_response"] = value ?: ""
props["\$feature_flag_request_id"] = requestId ?: ""
props["\$feature_flag_evaluated_at"] = evaluatedAt ?: ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is either a long or a string depending on whether or not evaluatedAt is null. Does this introduce a chance of creating the $feature_flag_evaluated_at property definition with the wrong type? If so, maybe we only write it when it's non-null

Suggested change
props["\$feature_flag_evaluated_at"] = evaluatedAt ?: ""
evaluatedAt?.let { props["\$feature_flag_evaluated_at"] = it }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d'oh; you're so right

flagDetails?.let {
props["\$feature_flag_id"] = it.metadata.id
props["\$feature_flag_version"] = it.metadata.version
Expand Down
10 changes: 9 additions & 1 deletion posthog/src/main/java/com/posthog/PostHogStateless.kt
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ public open class PostHogStateless protected constructor(
distinctId: String,
key: String,
value: Any?,
requestId: String? = null,
evaluatedAt: Long? = null,
) {
if (config?.sendFeatureFlagEvent == true) {
val isNewlySeen = featureFlagsCalled?.add(distinctId, key, value) ?: false
Expand All @@ -440,6 +442,8 @@ public open class PostHogStateless protected constructor(
props["\$feature_flag"] = key
// value should never be nullable anyway
props["\$feature_flag_response"] = value ?: ""
props["\$feature_flag_request_id"] = requestId ?: ""
props["\$feature_flag_evaluated_at"] = evaluatedAt ?: ""

captureStateless("\$feature_flag_called", distinctId, properties = props)
}
Expand Down Expand Up @@ -467,7 +471,11 @@ public open class PostHogStateless protected constructor(
groupProperties,
) ?: defaultValue

sendFeatureFlagCalled(distinctId, key, value)
// Get requestId and evaluatedAt from feature flags
val requestId = featureFlags?.getRequestId(distinctId, groups, personProperties, groupProperties)
val evaluatedAt = featureFlags?.getEvaluatedAt(distinctId, groups, personProperties, groupProperties)

sendFeatureFlagCalled(distinctId, key, value, requestId, evaluatedAt)

return value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,18 @@ public interface PostHogFeatureFlagsInterface {
public fun shutDown() {
// no-op by default
}

public fun getRequestId(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a PostHogFeatureFlagsInterface implemented somewhere in the tests which will need the new methods.

distinctId: String? = null,
groups: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
): String?

public fun getEvaluatedAt(
distinctId: String? = null,
groups: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
): Long?
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
* @property featureFlagPayloads the feature flag payloads
* @property flags the feature flags.
* @property quotaLimited array of quota limited features
* @property requestId the request id generated by the flags server on evaluation
* @property evaluatedAt the evaluated at timestamp generated by the flags server on evaluation
*/
@IgnoreJRERequirement
@PostHogInternal
Expand All @@ -21,4 +23,5 @@ public data class PostHogFlagsResponse(
val flags: Map<String, FeatureFlag>? = null,
val quotaLimited: List<String>? = null,
val requestId: String?,
val evaluatedAt: Long?,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the @property comment docs above, also for requestId just noticed its missing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do!

) : PostHogRemoteConfigResponse()
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public interface PostHogPreferences {
internal const val FEATURE_FLAGS = "featureFlags"
internal const val FEATURE_FLAGS_PAYLOAD = "featureFlagsPayload"
internal const val FEATURE_FLAG_REQUEST_ID = "feature_flag_request_id"
internal const val FEATURE_FLAG_EVALUATED_AT = "feature_flag_evaluated_at"
internal const val SESSION_REPLAY = "sessionReplay"
internal const val SURVEYS = "surveys"
internal const val PERSON_PROPERTIES_FOR_FLAGS = "personPropertiesForFlags"
Expand All @@ -61,6 +62,7 @@ public interface PostHogPreferences {
BUILD,
STRINGIFIED_KEYS,
FEATURE_FLAG_REQUEST_ID,
FEATURE_FLAG_EVALUATED_AT,
FLAGS,
PERSON_PROPERTIES_FOR_FLAGS,
GROUP_PROPERTIES_FOR_FLAGS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.posthog.PostHogInternal
import com.posthog.PostHogOnFeatureFlags
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS_PAYLOAD
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAG_EVALUATED_AT
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAG_REQUEST_ID
import com.posthog.internal.PostHogPreferences.Companion.FLAGS
import com.posthog.internal.PostHogPreferences.Companion.SESSION_REPLAY
Expand Down Expand Up @@ -46,6 +47,7 @@ public class PostHogRemoteConfig(
// But for now, we need to support both for back compatibility
private var flags: Map<String, Any>? = null
private var requestId: String? = null
private var evaluatedAt: Long? = null

private var surveys: List<Survey>? = null

Expand Down Expand Up @@ -501,12 +503,14 @@ public class PostHogRemoteConfig(
) as? Map<String, Any?> ?: mapOf()

val cachedRequestId = preferences.getValue(FEATURE_FLAG_REQUEST_ID) as? String
val cachedEvaluatedAt = preferences.getValue(FEATURE_FLAG_EVALUATED_AT) as? Long

synchronized(featureFlagsLock) {
this.flags = flags
this.featureFlags = featureFlags
this.featureFlagPayloads = payloads
this.requestId = cachedRequestId
this.evaluatedAt = cachedEvaluatedAt
isFeatureFlagsLoaded = true
}
}
Expand Down Expand Up @@ -549,6 +553,11 @@ public class PostHogRemoteConfig(
this.requestId?.let { requestId ->
config.cachePreferences?.setValue(FEATURE_FLAG_REQUEST_ID, requestId as Any)
}
// Store the evaluatedAt in the cache.
this.evaluatedAt = newResponse.evaluatedAt
this.evaluatedAt?.let { evaluatedAt ->
config.cachePreferences?.setValue(FEATURE_FLAG_EVALUATED_AT, evaluatedAt as Any)
}
}
return newResponse
}
Expand Down Expand Up @@ -618,13 +627,30 @@ public class PostHogRemoteConfig(

public fun isSessionReplayFlagActive(): Boolean = sessionReplayFlagActive

public fun getRequestId(): String? {
override fun getRequestId(
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): String? {
loadFeatureFlagsFromCacheIfNeeded()
synchronized(featureFlagsLock) {
return requestId
}
}

override fun getEvaluatedAt(
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Long? {
loadFeatureFlagsFromCacheIfNeeded()
synchronized(featureFlagsLock) {
return evaluatedAt
}
}

public fun getFlagDetails(key: String): FeatureFlag? {
loadFeatureFlagsFromCacheIfNeeded()

Expand All @@ -645,12 +671,14 @@ public class PostHogRemoteConfig(
this.featureFlagPayloads = null
this.flags = null
this.requestId = null
this.evaluatedAt = null

config.cachePreferences?.let { preferences ->
preferences.remove(FLAGS)
preferences.remove(FEATURE_FLAGS)
preferences.remove(FEATURE_FLAGS_PAYLOAD)
preferences.remove(FEATURE_FLAG_REQUEST_ID)
preferences.remove(FEATURE_FLAG_EVALUATED_AT)
}
}

Expand Down
Loading