Skip to content

Commit 2a45532

Browse files
Igor Macedo QuintanilhaIgor Macedo Quintanilha
authored andcommitted
fix(server): align FeatureFlagCacheKey shape across put and get paths
The 2.5.0 release added flagKeys and disableGeoip to FeatureFlagCacheKey and threaded them through getFeatureFlagsFromRemote (put-side), but the legacy read paths (getFeatureFlagsFromCache, getFeatureFlagError, getFeatureFlagDetails, getRequestId, getEvaluatedAt) still constructed the cache key with only the original four fields. Because the put-side writes under a 6-field key while these read-side helpers look up under a 4-field key, every cache read from the legacy flow misses — even after a successful remote evaluation. The visible symptoms were getFeatureFlagError returning UNKNOWN_ERROR for flags that actually resolved successfully (it hits the ?: return FeatureFlagError.UNKNOWN_ERROR fallback when cache.getEntry misses), $feature_flag_called events on legacy isFeatureEnabled / getFeatureFlag calls carrying a spurious $feature_flag_error: 'unknown_error' even though the SDK reached PostHog and got an answer, and getFeatureFlagDetails / getRequestId / getEvaluatedAt returning null for flags that were just evaluated. This change inlines flagKeys = null, disableGeoip = false at each of the five legacy read sites so their keys match the keys getFeatureFlagsFromRemote writes when called from the legacy flow (which always passes those parameters as their defaults). The evaluateFlags() path is unaffected — it already builds the 6-field key consistently on both sides. Adds LegacyCacheKeyRegressionTest with two cases that fail without this change and pass with it.
1 parent cb2b4ed commit 2a45532

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"posthog-server": patch
3+
---
4+
5+
Fix the legacy `isFeatureEnabled` / `getFeatureFlag` flow so it stops emitting `$feature_flag_called` events with a spurious `$feature_flag_error: "unknown_error"` after a successful evaluation. `FeatureFlagCacheKey` is now built with the same shape (`flagKeys` and `disableGeoip` included) on every read path, matching what `getFeatureFlagsFromRemote` writes when called from the legacy flow. Affects `getFeatureFlagsFromCache`, `getFeatureFlagError`, `getFeatureFlagDetails`, `getRequestId`, and `getEvaluatedAt`.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ internal class PostHogFeatureFlags(
220220
groups = groups,
221221
personProperties = personProperties,
222222
groupProperties = groupProperties,
223+
flagKeys = null,
224+
disableGeoip = false,
223225
)
224226

225227
return cache.get(cacheKey)
@@ -648,6 +650,8 @@ internal class PostHogFeatureFlags(
648650
groups = groups,
649651
personProperties = personProperties,
650652
groupProperties = groupProperties,
653+
flagKeys = null,
654+
disableGeoip = false,
651655
)
652656
return cache.getEntry(cacheKey)?.requestId
653657
}
@@ -670,6 +674,8 @@ internal class PostHogFeatureFlags(
670674
groups = groups,
671675
personProperties = personProperties,
672676
groupProperties = groupProperties,
677+
flagKeys = null,
678+
disableGeoip = false,
673679
)
674680
return cache.getEntry(cacheKey)?.evaluatedAt
675681
}
@@ -705,6 +711,8 @@ internal class PostHogFeatureFlags(
705711
groups = groups,
706712
personProperties = personProperties,
707713
groupProperties = groupProperties,
714+
flagKeys = null,
715+
disableGeoip = false,
708716
)
709717
return cache.getEntry(cacheKey)?.flags?.get(key)
710718
}
@@ -824,6 +832,8 @@ internal class PostHogFeatureFlags(
824832
groups = groups,
825833
personProperties = personProperties,
826834
groupProperties = groupProperties,
835+
flagKeys = null,
836+
disableGeoip = false,
827837
)
828838

829839
val entry = cache.getEntry(cacheKey) ?: return FeatureFlagError.UNKNOWN_ERROR
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.posthog.server.internal
2+
3+
import com.posthog.internal.PostHogApi
4+
import com.posthog.server.createFlagsResponse
5+
import com.posthog.server.createMockHttp
6+
import com.posthog.server.createTestConfig
7+
import com.posthog.server.jsonResponse
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertNotNull
11+
import kotlin.test.assertNull
12+
13+
/**
14+
* Regression: the cache key written by `getFeatureFlagsFromRemote` includes the
15+
* `flagKeys` and `disableGeoip` fields introduced in 2.5.0, but several read
16+
* paths used by the legacy `isFeatureEnabled` / `getFeatureFlag` flow
17+
* (`getFeatureFlagsFromCache`, `getFeatureFlagError`, `getFeatureFlagDetails`,
18+
* `getRequestId`, `getEvaluatedAt`) constructed the cache key with only the
19+
* original four fields. The shape mismatch caused every legacy lookup to miss,
20+
* so `getFeatureFlagError` returned `UNKNOWN_ERROR` as its fallback even after
21+
* a successful evaluation, and `getFeatureFlagDetails` / `getRequestId` /
22+
* `getEvaluatedAt` returned null.
23+
*/
24+
internal class LegacyCacheKeyRegressionTest {
25+
@Test
26+
fun `getFeatureFlagError returns null after a successful legacy getFeatureFlag`() {
27+
val flagsResponse = createFlagsResponse("test-flag", enabled = true)
28+
val mockServer = createMockHttp(jsonResponse(flagsResponse))
29+
val url = mockServer.url("/")
30+
31+
val config = createTestConfig(host = url.toString())
32+
val api = PostHogApi(config)
33+
val remoteConfig = PostHogFeatureFlags(config, api, 60000, 100)
34+
35+
val flagValue =
36+
remoteConfig.getFeatureFlag(
37+
key = "test-flag",
38+
defaultValue = false,
39+
distinctId = "test-user",
40+
)
41+
42+
val error =
43+
remoteConfig.getFeatureFlagError(
44+
key = "test-flag",
45+
distinctId = "test-user",
46+
)
47+
48+
assertEquals(true, flagValue, "Legacy getFeatureFlag should resolve the flag")
49+
assertNull(error, "Expected no error after a successful legacy evaluation")
50+
mockServer.shutdown()
51+
}
52+
53+
@Test
54+
fun `getFeatureFlagDetails returns the flag after a successful legacy getFeatureFlag`() {
55+
val flagsResponse = createFlagsResponse("test-flag", enabled = true)
56+
val mockServer = createMockHttp(jsonResponse(flagsResponse))
57+
val url = mockServer.url("/")
58+
59+
val config = createTestConfig(host = url.toString())
60+
val api = PostHogApi(config)
61+
val remoteConfig = PostHogFeatureFlags(config, api, 60000, 100)
62+
63+
remoteConfig.getFeatureFlag(
64+
key = "test-flag",
65+
defaultValue = false,
66+
distinctId = "test-user",
67+
)
68+
69+
val details =
70+
remoteConfig.getFeatureFlagDetails(
71+
key = "test-flag",
72+
distinctId = "test-user",
73+
)
74+
75+
assertNotNull(details, "Expected details for a flag that was just resolved")
76+
assertEquals(true, details.enabled)
77+
mockServer.shutdown()
78+
}
79+
}

0 commit comments

Comments
 (0)