Skip to content

Commit 472cc44

Browse files
authored
included evaluated_at properties in $feature_flag_called events (#321)
* implementation * update the property * test * update API * added changelogs * modify test interface and function signatures
1 parent 35c1449 commit 472cc44

File tree

14 files changed

+178
-9
lines changed

14 files changed

+178
-9
lines changed

posthog-android/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
## Next
22

3+
## 3.28.0 - 2025-12-01
4+
5+
- feat: include `evaluated_at` properties in `$feature_flag_called` events ([#321](https://github.com/PostHog/posthog-android/pull/321))
6+
7+
## 3.27.0 - 2025-11-24
8+
39
- feat: proguard support ([#316](https://github.com/PostHog/posthog-android/pull/316))
410

511
## 3.26.0 - 2025-11-05

posthog-server/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Next
22

3+
## 2.2.0 - 2025-12-01
4+
5+
- feat: include `evaluated_at` properties in `$feature_flag_called` events ([#321](https://github.com/PostHog/posthog-android/pull/321))
6+
37
## 2.0.1 - 2025-11-24
48

59
- fix: Local evaluation properly handles cases when flag dependency should be false ([#320](https://github.com/PostHog/posthog-android/pull/320))

posthog-server/src/main/java/com/posthog/server/internal/FeatureFlagCacheEntry.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ internal data class FeatureFlagCacheEntry(
99
val flags: Map<String, FeatureFlag>?,
1010
val timestamp: Long,
1111
val expiresAt: Long,
12+
val requestId: String? = null,
13+
val evaluatedAt: Long? = null,
1214
) {
1315
/**
1416
* Check if this cache entry has expired
@@ -21,6 +23,8 @@ internal data class FeatureFlagCacheEntry(
2123
var result = flags?.hashCode() ?: 0
2224
result = 31 * result + (timestamp xor (timestamp ushr 32)).toInt()
2325
result = 31 * result + (expiresAt xor (expiresAt ushr 32)).toInt()
26+
result = 31 * result + (requestId?.hashCode() ?: 0)
27+
result = 31 * result + (evaluatedAt?.hashCode() ?: 0)
2428
return result
2529
}
2630

@@ -31,6 +35,8 @@ internal data class FeatureFlagCacheEntry(
3135
if (flags != other.flags) return false
3236
if (timestamp != other.timestamp) return false
3337
if (expiresAt != other.expiresAt) return false
38+
if (requestId != other.requestId) return false
39+
if (evaluatedAt != other.evaluatedAt) return false
3440

3541
return true
3642
}

posthog-server/src/main/java/com/posthog/server/internal/PostHogFeatureFlagCache.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,42 @@ internal class PostHogFeatureFlagCache(
3838
return entry.flags
3939
}
4040

41+
/**
42+
* Get full cache entry (including requestId and evaluatedAt) if present and not expired
43+
*/
44+
@Synchronized
45+
fun getEntry(key: FeatureFlagCacheKey): FeatureFlagCacheEntry? {
46+
val entry = cache[key]
47+
if (entry == null) {
48+
return null
49+
}
50+
51+
if (entry.isExpired()) {
52+
cache.remove(key)
53+
return null
54+
}
55+
56+
return entry
57+
}
58+
4159
/**
4260
* Put feature flags into cache with current timestamp
4361
*/
4462
@Synchronized
4563
fun put(
4664
key: FeatureFlagCacheKey,
4765
flags: Map<String, FeatureFlag>?,
66+
requestId: String? = null,
67+
evaluatedAt: Long? = null,
4868
) {
4969
val currentTime = System.currentTimeMillis()
5070
val entry =
5171
FeatureFlagCacheEntry(
5272
flags = flags,
5373
timestamp = currentTime,
5474
expiresAt = currentTime + maxAgeMs,
75+
requestId = requestId,
76+
evaluatedAt = evaluatedAt,
5577
)
5678

5779
cache[key] = entry

posthog-server/src/main/java/com/posthog/server/internal/PostHogFeatureFlags.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ internal class PostHogFeatureFlags(
259259
return try {
260260
val response = api.flags(distinctId, null, groups, personProperties, groupProperties)
261261
val flags = response?.flags
262-
cache.put(cacheKey, flags)
262+
cache.put(cacheKey, flags, response?.requestId, response?.evaluatedAt)
263263
flags
264264
} catch (e: Throwable) {
265265
config.logger.log("Loading remote feature flags failed: $e")
@@ -499,4 +499,48 @@ internal class PostHogFeatureFlags(
499499
private fun localEvaluationEnabled(): Boolean {
500500
return localEvaluation && !personalApiKey.isNullOrBlank()
501501
}
502+
503+
/**
504+
* Get the requestId from the cache for the given distinctId and groups
505+
*/
506+
override fun getRequestId(
507+
distinctId: String?,
508+
groups: Map<String, String>?,
509+
personProperties: Map<String, Any?>?,
510+
groupProperties: Map<String, Map<String, Any?>>?,
511+
): String? {
512+
if (distinctId == null) {
513+
return null
514+
}
515+
val cacheKey =
516+
FeatureFlagCacheKey(
517+
distinctId = distinctId,
518+
groups = groups,
519+
personProperties = personProperties,
520+
groupProperties = groupProperties,
521+
)
522+
return cache.getEntry(cacheKey)?.requestId
523+
}
524+
525+
/**
526+
* Get the evaluatedAt from the cache for the given distinctId and groups
527+
*/
528+
override fun getEvaluatedAt(
529+
distinctId: String?,
530+
groups: Map<String, String>?,
531+
personProperties: Map<String, Any?>?,
532+
groupProperties: Map<String, Map<String, Any?>>?,
533+
): Long? {
534+
if (distinctId == null) {
535+
return null
536+
}
537+
val cacheKey =
538+
FeatureFlagCacheKey(
539+
distinctId = distinctId,
540+
groups = groups,
541+
personProperties = personProperties,
542+
groupProperties = groupProperties,
543+
)
544+
return cache.getEntry(cacheKey)?.evaluatedAt
545+
}
502546
}

posthog/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Next
22

3+
## 5.2.0 - 2025-11-24
4+
5+
- feat: include `evaluated_at` properties in `$feature_flag_called` events ([#321](https://github.com/PostHog/posthog-android/pull/321))
6+
7+
38
## 5.1.0 - 2025-11-06
49

510
- feat: Add an optional shutdown override to `FeatureFlagInterface` ([#299](https://github.com/PostHog/posthog-android/pull/299))

posthog/api/posthog.api

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -611,32 +611,38 @@ public final class com/posthog/internal/PostHogDeviceDateProvider : com/posthog/
611611

612612
public abstract interface class com/posthog/internal/PostHogFeatureFlagsInterface {
613613
public abstract fun clear ()V
614+
public abstract fun getEvaluatedAt (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Long;
614615
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;
615616
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;
616617
public abstract fun getFeatureFlags (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
618+
public abstract fun getRequestId (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/String;
617619
public abstract fun shutDown ()V
618620
}
619621

620622
public final class com/posthog/internal/PostHogFeatureFlagsInterface$DefaultImpls {
623+
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;
621624
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;
622625
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;
623626
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;
627+
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;
624628
public static fun shutDown (Lcom/posthog/internal/PostHogFeatureFlagsInterface;)V
625629
}
626630

627631
public final class com/posthog/internal/PostHogFlagsResponse : com/posthog/internal/PostHogRemoteConfigResponse {
628-
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;)V
629-
public synthetic fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
632+
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;Ljava/lang/Long;)V
633+
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
630634
public final fun component1 ()Z
631635
public final fun component2 ()Ljava/util/Map;
632636
public final fun component3 ()Ljava/util/Map;
633637
public final fun component4 ()Ljava/util/Map;
634638
public final fun component5 ()Ljava/util/List;
635639
public final fun component6 ()Ljava/lang/String;
636-
public final fun copy (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/List;Ljava/lang/String;)Lcom/posthog/internal/PostHogFlagsResponse;
637-
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;
640+
public final fun component7 ()Ljava/lang/Long;
641+
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;
642+
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;
638643
public fun equals (Ljava/lang/Object;)Z
639644
public final fun getErrorsWhileComputingFlags ()Z
645+
public final fun getEvaluatedAt ()Ljava/lang/Long;
640646
public final fun getFeatureFlagPayloads ()Ljava/util/Map;
641647
public final fun getFeatureFlags ()Ljava/util/Map;
642648
public final fun getFlags ()Ljava/util/Map;
@@ -720,12 +726,13 @@ public final class com/posthog/internal/PostHogRemoteConfig : com/posthog/intern
720726
public fun <init> (Lcom/posthog/PostHogConfig;Lcom/posthog/internal/PostHogApi;Ljava/util/concurrent/ExecutorService;Lkotlin/jvm/functions/Function0;)V
721727
public synthetic fun <init> (Lcom/posthog/PostHogConfig;Lcom/posthog/internal/PostHogApi;Ljava/util/concurrent/ExecutorService;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
722728
public fun clear ()V
729+
public fun getEvaluatedAt (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Long;
723730
public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Object;
724731
public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/Object;
725732
public fun getFeatureFlags (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
726733
public final fun getFlagDetails (Ljava/lang/String;)Lcom/posthog/internal/FeatureFlag;
727734
public final fun getOnRemoteConfigLoaded ()Lkotlin/jvm/functions/Function0;
728-
public final fun getRequestId ()Ljava/lang/String;
735+
public fun getRequestId (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/lang/String;
729736
public final fun getSurveys ()Ljava/util/List;
730737
public final fun isSessionReplayFlagActive ()Z
731738
public final fun loadFeatureFlags (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lcom/posthog/PostHogOnFeatureFlags;Lcom/posthog/PostHogOnFeatureFlags;)V

posthog/src/main/java/com/posthog/PostHog.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -889,12 +889,14 @@ public class PostHog private constructor(
889889
remoteConfig?.let {
890890
val flagDetails = it.getFlagDetails(key)
891891
val requestId = it.getRequestId()
892+
val evaluatedAt = it.getEvaluatedAt()
892893

893894
val props = mutableMapOf<String, Any>()
894895
props["\$feature_flag"] = key
895896
// value should never be nullabe anyway
896897
props["\$feature_flag_response"] = value ?: ""
897-
props["\$feature_flag_request_id"] = requestId ?: ""
898+
requestId?.let { props["\$feature_flag_request_id"] = it }
899+
evaluatedAt?.let { props["\$feature_flag_evaluated_at"] = it }
898900
flagDetails?.let {
899901
props["\$feature_flag_id"] = it.metadata.id
900902
props["\$feature_flag_version"] = it.metadata.version

posthog/src/main/java/com/posthog/PostHogStateless.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ public open class PostHogStateless protected constructor(
432432
distinctId: String,
433433
key: String,
434434
value: Any?,
435+
requestId: String? = null,
436+
evaluatedAt: Long? = null,
435437
) {
436438
if (config?.sendFeatureFlagEvent == true) {
437439
val isNewlySeen = featureFlagsCalled?.add(distinctId, key, value) ?: false
@@ -440,6 +442,8 @@ public open class PostHogStateless protected constructor(
440442
props["\$feature_flag"] = key
441443
// value should never be nullable anyway
442444
props["\$feature_flag_response"] = value ?: ""
445+
requestId?.let { props["\$feature_flag_request_id"] = it }
446+
evaluatedAt?.let { props["\$feature_flag_evaluated_at"] = it }
443447

444448
captureStateless("\$feature_flag_called", distinctId, properties = props)
445449
}
@@ -467,7 +471,11 @@ public open class PostHogStateless protected constructor(
467471
groupProperties,
468472
) ?: defaultValue
469473

470-
sendFeatureFlagCalled(distinctId, key, value)
474+
// Get requestId and evaluatedAt from feature flags
475+
val requestId = featureFlags?.getRequestId(distinctId, groups, personProperties, groupProperties)
476+
val evaluatedAt = featureFlags?.getEvaluatedAt(distinctId, groups, personProperties, groupProperties)
477+
478+
sendFeatureFlagCalled(distinctId, key, value, requestId, evaluatedAt)
471479

472480
return value
473481
}

posthog/src/main/java/com/posthog/internal/PostHogFeatureFlagsInterface.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,18 @@ public interface PostHogFeatureFlagsInterface {
3434
public fun shutDown() {
3535
// no-op by default
3636
}
37+
38+
public fun getRequestId(
39+
distinctId: String? = null,
40+
groups: Map<String, String>? = null,
41+
personProperties: Map<String, Any?>? = null,
42+
groupProperties: Map<String, Map<String, Any?>>? = null,
43+
): String?
44+
45+
public fun getEvaluatedAt(
46+
distinctId: String? = null,
47+
groups: Map<String, String>? = null,
48+
personProperties: Map<String, Any?>? = null,
49+
groupProperties: Map<String, Map<String, Any?>>? = null,
50+
): Long?
3751
}

0 commit comments

Comments
 (0)