Skip to content

Commit 4c65dc3

Browse files
committed
wip: Local evaluation
1 parent f4fd7ac commit 4c65dc3

File tree

13 files changed

+2143
-48
lines changed

13 files changed

+2143
-48
lines changed
Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,78 @@
11
package com.posthog.java.sample;
22

3-
import com.posthog.server.PostHogCaptureOptions;
4-
import com.posthog.server.PostHogConfig;
53
import com.posthog.server.PostHog;
4+
import com.posthog.server.PostHogConfig;
65
import com.posthog.server.PostHogInterface;
7-
8-
import java.util.HashMap;
9-
import java.util.Map;
6+
import com.posthog.server.PostHogFeatureFlagOptions;
107

118
/**
129
* Simple Java 1.8 example demonstrating PostHog usage
1310
*/
1411
public class PostHogJavaExample {
12+
private static PostHogInterface postHog;
13+
1514
public static void main(String[] args) {
1615
PostHogConfig config = PostHogConfig
17-
.builder("phc_wz4KZkikEluCCdfY2B2h7MXYygNGdTqFgjbU7I1ZdVR")
18-
.build();
19-
20-
PostHogInterface postHog = PostHog.with(config);
16+
.builder("phc_qYXiHw5odMiVWF7Dwh2sHWS7Hj6FsutBNp2SEaMqS0A")
17+
.personalApiKey("phx_dQE4l5QTFXeQhgc0JNlMVndF8N713TYIfpNhdw0sftX6KG9")
18+
.host("http://localhost:8010")
19+
.localEvaluation(true)
20+
.debug(true)
21+
.onLocalEvaluationReady(() -> {
22+
if (postHog.isFeatureEnabled("distinct-id", "beta-feature", false)) {
23+
System.out.println("The feature is enabled.");
24+
}
2125

22-
postHog.group("distinct-id", "company", "some-company-id");
23-
postHog.capture(
24-
"distinct-id",
25-
"new-purchase",
26-
PostHogCaptureOptions
27-
.builder()
28-
.property("item", "SKU-0000")
29-
.property("sale", false)
30-
.build());
26+
Object flagValue = postHog.getFeatureFlag("distinct-id", "multi-variate-flag", "default");
27+
String flagVariate = flagValue instanceof String ? (String) flagValue : "default";
28+
Object flagPayload = postHog.getFeatureFlagPayload("distinct-id", "multi-variate-flag");
3129

32-
HashMap<String, Object> userProperties = new HashMap<>();
33-
userProperties.put("email", "[email protected]");
34-
postHog.identify("distinct-id", userProperties);
30+
System.out.println("The flag variant was: " + flagVariate);
31+
System.out.println("Received flag payload: " + flagPayload);
3532

36-
// AVOID - Anonymous inner class holds reference to outer class.
37-
// The following won't serialize properly.
38-
// postHog.identify("user-123", new HashMap<String, Object>() {{
39-
// put("key", "value");
40-
// }});
33+
Boolean hasFilePreview = postHog.isFeatureEnabled(
34+
"distinct-id",
35+
"file-previews",
36+
PostHogFeatureFlagOptions
37+
.builder()
38+
.defaultValue(false)
39+
.personProperty("email", "[email protected]")
40+
.build());
4141

42-
postHog.alias("distinct-id", "alias-id");
42+
System.out.println("File previews enabled: " + hasFilePreview);
4343

44+
postHog.flush();
45+
postHog.close();
4446

45-
if (postHog.isFeatureEnabled("distinct-id", "beta-feature", false)) {
46-
System.out.println("The feature is enabled.");
47-
}
47+
return null;
48+
})
49+
.build();
4850

49-
Object flagValue = postHog.getFeatureFlag("distinct-id", "multi-variate-flag", "default");
50-
String flagVariate = flagValue instanceof String ? (String) flagValue : "default";
51-
Object flagPayload = postHog.getFeatureFlagPayload("distinct-id", "multi-variate-flag");
51+
postHog = PostHog.with(config);
5252

53-
System.out.println("The flag variant was: " + flagVariate);
54-
System.out.println("Received flag payload: " + flagPayload);
53+
/*
54+
* postHog.group("distinct-id", "company", "some-company-id");
55+
* postHog.capture(
56+
* "distinct-id",
57+
* "new-purchase",
58+
* PostHogCaptureOptions
59+
* .builder()
60+
* .property("item", "SKU-0000")
61+
* .property("sale", false)
62+
* .build());
63+
*
64+
* HashMap<String, Object> userProperties = new HashMap<>();
65+
* userProperties.put("email", "[email protected]");
66+
* postHog.identify("distinct-id", userProperties);
67+
*
68+
* // AVOID - Anonymous inner class holds reference to outer class.
69+
* // The following won't serialize properly.
70+
* // postHog.identify("user-123", new HashMap<String, Object>() {{
71+
* // put("key", "value");
72+
* // }});
73+
*
74+
* postHog.alias("distinct-id", "alias-id");
75+
*/
5576

56-
postHog.flush();
57-
postHog.close();
5877
}
5978
}

posthog-server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- feat: `timestamp` can now be overridden when capturing an event ([#297](https://github.com/PostHog/posthog-android/issues/297))
66
- feat: Add `groups`, `groupProperties`, `personProperties` overrides to feature flag methods ([#298](https://github.com/PostHog/posthog-android/issues/298))
7+
- feat: Add local evaluation for feature flags
78

89
## 1.0.3 - 2025-10-01
910

posthog-server/USAGE.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ PostHogConfig config = PostHogConfig.builder("phc_your_api_key_here")
9292
- `flushIntervalSeconds`: Interval between automatic flushes (default: `30`)
9393
- `featureFlagCacheSize`: The maximum number of feature flags results to cache (default: `1000`)
9494
- `featureFlagCacheMaxAgeMs`: The maximum age of a feature flag cache record in memory in milliseconds (default: `300000` or five minutes)
95+
- `enableLocalEvaluation`: Enable local evaluation of feature flags (default: `false`) **(Experimental)**
96+
- `personalApiKey`: Personal API key required for local evaluation (default: `null`) **(Experimental)**
97+
- `pollIntervalSeconds`: Interval for polling flag definitions for local evaluation (default: `30`) **(Experimental)**
9598

9699
## Capturing Events
97100

@@ -202,6 +205,52 @@ postHog.identify("user123", userProperties, userPropertiesSetOnce);
202205

203206
## Feature Flags
204207

208+
### Local Evaluation (Experimental)
209+
210+
Local evaluation allows the SDK to evaluate feature flags locally without making API calls for each flag check. This reduces latency and API costs.
211+
212+
**How it works:**
213+
1. The SDK periodically polls for flag definitions from PostHog (every 30 seconds by default)
214+
2. Flags are evaluated locally using cached definitions
215+
3. If evaluation is inconclusive (missing properties, etc.), the SDK falls back to the API
216+
217+
**Requirements:**
218+
- A Personal API Key (get one from PostHog → Settings → User → Personal API Keys)
219+
- The `enableLocalEvaluation` config option set to `true`
220+
221+
#### Kotlin
222+
223+
```kotlin
224+
val config = PostHogConfig(
225+
apiKey = "phc_your_api_key_here",
226+
host = "https://your-posthog-instance.com",
227+
enableLocalEvaluation = true,
228+
personalApiKey = "phx_your_personal_api_key_here",
229+
pollIntervalSeconds = 30 // Optional: customize polling interval
230+
)
231+
```
232+
233+
#### Java
234+
235+
```java
236+
PostHogConfig config = PostHogConfig.builder("phc_your_api_key_here")
237+
.host("https://your-posthog-instance.com")
238+
.enableLocalEvaluation(true)
239+
.personalApiKey("phx_your_personal_api_key_here")
240+
.pollIntervalSeconds(30) // Optional: customize polling interval
241+
.build();
242+
```
243+
244+
**Benefits:**
245+
- **Reduced latency**: No API call needed for most flag evaluations
246+
- **Lower costs**: Fewer API requests
247+
- **Offline support**: Flags continue to work with cached definitions
248+
249+
**Limitations:**
250+
- Requires person/group properties to be provided with each call
251+
- Falls back to API for cohort-based flags without local cohort data
252+
- May not reflect real-time flag changes (respects polling interval)
253+
205254
### Check if Feature is Enabled
206255

207256
#### Kotlin

posthog-server/src/main/java/com/posthog/server/PostHogConfig.kt

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.posthog.server.internal.PostHogMemoryQueue
1111
import com.posthog.server.internal.PostHogServerContext
1212
import java.net.Proxy
1313

14+
internal typealias NullaryCallback = () -> Unit
15+
1416
/**
1517
* The SDK Config
1618
*/
@@ -106,6 +108,31 @@ public open class PostHogConfig constructor(
106108
* Defaults to 1000
107109
*/
108110
public var featureFlagCalledCacheSize: Int = DEFAULT_FEATURE_FLAG_CALLED_CACHE_SIZE,
111+
/**
112+
* Enable local evaluation of feature flags
113+
* When enabled, the SDK periodically fetches flag definitions and evaluates flags locally
114+
* without making API calls for each flag check. Falls back to API if evaluation is inconclusive.
115+
* Requires personalApiKey to be set.
116+
* Defaults to false
117+
*/
118+
public var enableLocalEvaluation: Boolean = false,
119+
/**
120+
* Personal API key for local evaluation
121+
* Required when enableLocalEvaluation is true.
122+
* Get your personal API key from PostHog -> Settings -> User -> Personal API Keys
123+
* Defaults to null
124+
*/
125+
public var personalApiKey: String? = null,
126+
/**
127+
* Interval in seconds for polling feature flag definitions for local evaluation
128+
* Defaults to 30 seconds
129+
*/
130+
public var pollIntervalSeconds: Int = DEFAULT_POLL_INTERVAL_SECONDS,
131+
/**
132+
* Callback when local evaluation is ready
133+
*/
134+
public var onLocalEvaluationReady: NullaryCallback? = null
135+
109136
) {
110137
private val beforeSendCallbacks = mutableListOf<PostHogBeforeSend>()
111138
private val integrations = mutableListOf<PostHogIntegration>()
@@ -145,6 +172,10 @@ public open class PostHogConfig constructor(
145172
api,
146173
cacheMaxAgeMs = featureFlagCacheMaxAgeMs,
147174
cacheMaxSize = featureFlagCacheSize,
175+
enableLocalEvaluation = enableLocalEvaluation,
176+
personalApiKey = personalApiKey,
177+
pollIntervalSeconds = pollIntervalSeconds,
178+
onLocalEvaluationReady = onLocalEvaluationReady,
148179
)
149180
},
150181
queueProvider = { config, api, endpoint, _, executor ->
@@ -181,6 +212,7 @@ public open class PostHogConfig constructor(
181212
public const val DEFAULT_FEATURE_FLAG_CACHE_SIZE: Int = 1000
182213
public const val DEFAULT_FEATURE_FLAG_CACHE_MAX_AGE_MS: Int = 5 * 60 * 1000 // 5 minutes
183214
public const val DEFAULT_FEATURE_FLAG_CALLED_CACHE_SIZE: Int = 1000
215+
public const val DEFAULT_POLL_INTERVAL_SECONDS: Int = 30
184216

185217
@JvmStatic
186218
public fun builder(apiKey: String): Builder = Builder(apiKey)
@@ -202,39 +234,64 @@ public open class PostHogConfig constructor(
202234
private var featureFlagCacheSize: Int = DEFAULT_FEATURE_FLAG_CACHE_SIZE
203235
private var featureFlagCacheMaxAgeMs: Int = DEFAULT_FEATURE_FLAG_CACHE_MAX_AGE_MS
204236
private var featureFlagCalledCacheSize: Int = DEFAULT_FEATURE_FLAG_CALLED_CACHE_SIZE
237+
private var enableLocalEvaluation: Boolean = false
238+
private var personalApiKey: String? = null
239+
private var pollIntervalSeconds: Int = DEFAULT_POLL_INTERVAL_SECONDS
240+
private var onLocalEvaluationReady: NullaryCallback? = null
205241

206242
public fun host(host: String): Builder = apply { this.host = host }
207243

208244
public fun debug(debug: Boolean): Builder = apply { this.debug = debug }
209245

210-
public fun sendFeatureFlagEvent(sendFeatureFlagEvent: Boolean): Builder = apply { this.sendFeatureFlagEvent = sendFeatureFlagEvent }
246+
public fun sendFeatureFlagEvent(sendFeatureFlagEvent: Boolean): Builder =
247+
apply { this.sendFeatureFlagEvent = sendFeatureFlagEvent }
211248

212-
public fun preloadFeatureFlags(preloadFeatureFlags: Boolean): Builder = apply { this.preloadFeatureFlags = preloadFeatureFlags }
249+
public fun preloadFeatureFlags(preloadFeatureFlags: Boolean): Builder =
250+
apply { this.preloadFeatureFlags = preloadFeatureFlags }
213251

214-
public fun remoteConfig(remoteConfig: Boolean): Builder = apply { this.remoteConfig = remoteConfig }
252+
public fun remoteConfig(remoteConfig: Boolean): Builder =
253+
apply { this.remoteConfig = remoteConfig }
215254

216255
public fun flushAt(flushAt: Int): Builder = apply { this.flushAt = flushAt }
217256

218-
public fun maxQueueSize(maxQueueSize: Int): Builder = apply { this.maxQueueSize = maxQueueSize }
257+
public fun maxQueueSize(maxQueueSize: Int): Builder =
258+
apply { this.maxQueueSize = maxQueueSize }
219259

220-
public fun maxBatchSize(maxBatchSize: Int): Builder = apply { this.maxBatchSize = maxBatchSize }
260+
public fun maxBatchSize(maxBatchSize: Int): Builder =
261+
apply { this.maxBatchSize = maxBatchSize }
221262

222-
public fun flushIntervalSeconds(flushIntervalSeconds: Int): Builder = apply { this.flushIntervalSeconds = flushIntervalSeconds }
263+
public fun flushIntervalSeconds(flushIntervalSeconds: Int): Builder =
264+
apply { this.flushIntervalSeconds = flushIntervalSeconds }
223265

224-
public fun encryption(encryption: PostHogEncryption?): Builder = apply { this.encryption = encryption }
266+
public fun encryption(encryption: PostHogEncryption?): Builder =
267+
apply { this.encryption = encryption }
225268

226-
public fun onFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags?): Builder = apply { this.onFeatureFlags = onFeatureFlags }
269+
public fun onFeatureFlags(onFeatureFlags: PostHogOnFeatureFlags?): Builder =
270+
apply { this.onFeatureFlags = onFeatureFlags }
227271

228272
public fun proxy(proxy: Proxy?): Builder = apply { this.proxy = proxy }
229273

230-
public fun featureFlagCacheSize(featureFlagCacheSize: Int): Builder = apply { this.featureFlagCacheSize = featureFlagCacheSize }
274+
public fun featureFlagCacheSize(featureFlagCacheSize: Int): Builder =
275+
apply { this.featureFlagCacheSize = featureFlagCacheSize }
231276

232277
public fun featureFlagCacheMaxAgeMs(featureFlagCacheMaxAgeMs: Int): Builder =
233278
apply { this.featureFlagCacheMaxAgeMs = featureFlagCacheMaxAgeMs }
234279

235280
public fun featureFlagCalledCacheSize(featureFlagCalledCacheSize: Int): Builder =
236281
apply { this.featureFlagCalledCacheSize = featureFlagCalledCacheSize }
237282

283+
public fun localEvaluation(enableLocalEvaluation: Boolean): Builder =
284+
apply { this.enableLocalEvaluation = enableLocalEvaluation }
285+
286+
public fun personalApiKey(personalApiKey: String?): Builder =
287+
apply { this.personalApiKey = personalApiKey }
288+
289+
public fun pollIntervalSeconds(pollIntervalSeconds: Int): Builder =
290+
apply { this.pollIntervalSeconds = pollIntervalSeconds }
291+
292+
public fun onLocalEvaluationReady(onLocalEvaluationReady: NullaryCallback?): Builder =
293+
apply { this.onLocalEvaluationReady = onLocalEvaluationReady }
294+
238295
public fun build(): PostHogConfig =
239296
PostHogConfig(
240297
apiKey = apiKey,
@@ -253,6 +310,10 @@ public open class PostHogConfig constructor(
253310
featureFlagCacheSize = featureFlagCacheSize,
254311
featureFlagCacheMaxAgeMs = featureFlagCacheMaxAgeMs,
255312
featureFlagCalledCacheSize = featureFlagCalledCacheSize,
313+
enableLocalEvaluation = enableLocalEvaluation,
314+
personalApiKey = personalApiKey,
315+
pollIntervalSeconds = pollIntervalSeconds,
316+
onLocalEvaluationReady = onLocalEvaluationReady,
256317
)
257318
}
258319
}

0 commit comments

Comments
 (0)