Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
34f849a
fix!: Correctly type user and group properties
dustinbyrne Oct 24, 2025
e10886d
docs: Update CHANGELOG
dustinbyrne Oct 28, 2025
dd70e0e
chore: lint and api dump
dustinbyrne Oct 28, 2025
e824ce6
docs: Update CHANGELOG
dustinbyrne Oct 28, 2025
73c397a
feat(core): Add `localEvaluation` API
dustinbyrne Oct 6, 2025
ec305f7
feat(core): Add an optional shutdown override to Feature Flags
dustinbyrne Oct 6, 2025
c9e101d
docs(core): Update CHANGELOG
dustinbyrne Oct 6, 2025
946514b
feat(server): Add feature flags local evaluation
dustinbyrne Oct 6, 2025
a153e03
docs(server): Update CHANGELOG
dustinbyrne Oct 6, 2025
59ef0f4
docs(server): Update USAGE
dustinbyrne Oct 6, 2025
cd61d23
chore: Update Java sample to include local eval
dustinbyrne Oct 6, 2025
5f271e5
Don't shadow reserved keyword identifiers
dustinbyrne Oct 8, 2025
e21558f
Drop a list allocation
dustinbyrne Oct 8, 2025
986bd30
Add enums for property operators and types
dustinbyrne Oct 8, 2025
8c9f04d
Regex patterns are static
dustinbyrne Oct 8, 2025
9bb1cf6
Separate duties in feature flags
dustinbyrne Oct 9, 2025
193cba7
refactor: Move local eval models to core
dustinbyrne Oct 21, 2025
b097195
refactor: Use deserialization from core
dustinbyrne Oct 21, 2025
cbf276b
fix: Flag definitions are loaded synchronously if missing
dustinbyrne Oct 21, 2025
e5022e8
fix: Use group properties when evaluating group flags
dustinbyrne Oct 21, 2025
3d4dfe5
test: Use JSON objects
dustinbyrne Oct 21, 2025
ede6f7d
test: Add flag dependency tests
dustinbyrne Oct 21, 2025
e798c95
fix: Mark the poller as daemon
dustinbyrne Oct 21, 2025
5644252
chore(java-sample): Move logic out of onFeatureFlags
dustinbyrne Oct 21, 2025
4655d7f
docs(core): Update CHANGELOG
dustinbyrne Oct 21, 2025
f55606f
chore(server): apply formatter
dustinbyrne Oct 21, 2025
9f37a04
fix: Properly type `userProperties`/`groupProperties`
dustinbyrne Oct 28, 2025
7489684
feat: Setting `personalApiKey` turns local eval on by default
dustinbyrne Oct 28, 2025
ca71eea
fix(server): Skip flags with ensure_experience_continuity
dustinbyrne Oct 31, 2025
8ae1d7a
refactor(server): Inherit `PostHogStateless`
dustinbyrne Oct 31, 2025
4853718
feat(server): Add `reloadFeatureFlags` method
dustinbyrne Oct 31, 2025
2865407
fix(server): Improve synchronization of flag definitions
dustinbyrne Oct 31, 2025
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
@@ -1,26 +1,31 @@
package com.posthog.java.sample;

import com.posthog.server.PostHog;
import com.posthog.server.PostHogCaptureOptions;
import com.posthog.server.PostHogConfig;
import com.posthog.server.PostHog;
import com.posthog.server.PostHogFeatureFlagOptions;
import com.posthog.server.PostHogInterface;

import java.util.HashMap;
import java.util.Map;

/**
* Simple Java 1.8 example demonstrating PostHog usage
*/
public class PostHogJavaExample {

public static void main(String[] args) {
PostHogConfig config = PostHogConfig
.builder("phc_wz4KZkikEluCCdfY2B2h7MXYygNGdTqFgjbU7I1ZdVR")
.builder("phc_wxtaSxv9yC8UYxUAxNojluoAf41L8p6SJZmiTMtS8jA")
.personalApiKey("phs_DuaFTmUtxQNj5R2W03emB1jMLIX5XwDvrt3DKfi5uYNcxzd")
.host("http://localhost:8010")
.localEvaluation(true)
.debug(true)
.build();

PostHogInterface postHog = PostHog.with(config);
PostHogInterface posthog = PostHog.with(config);

postHog.group("distinct-id", "company", "some-company-id");
postHog.capture(
posthog.group("distinct-id", "company", "some-company-id");
posthog.capture(
"distinct-id",
"new-purchase",
PostHogCaptureOptions
Expand All @@ -31,29 +36,40 @@ public static void main(String[] args) {

HashMap<String, Object> userProperties = new HashMap<>();
userProperties.put("email", "[email protected]");
postHog.identify("distinct-id", userProperties);
posthog.identify("distinct-id", userProperties);

// AVOID - Anonymous inner class holds reference to outer class.
// The following won't serialize properly.
// postHog.identify("user-123", new HashMap<String, Object>() {{
// put("key", "value");
// posthog.identify("user-123", new HashMap<String, Object>() {{
// put("key", "value");
// }});

postHog.alias("distinct-id", "alias-id");
posthog.alias("distinct-id", "alias-id");


if (postHog.isFeatureEnabled("distinct-id", "beta-feature", false)) {
// Feature flag examples with local evaluation
if (posthog.isFeatureEnabled("distinct-id", "beta-feature", false)) {
System.out.println("The feature is enabled.");
}

Object flagValue = postHog.getFeatureFlag("distinct-id", "multi-variate-flag", "default");
Object flagValue = posthog.getFeatureFlag("distinct-id", "multi-variate-flag", "default");
String flagVariate = flagValue instanceof String ? (String) flagValue : "default";
Object flagPayload = postHog.getFeatureFlagPayload("distinct-id", "multi-variate-flag");
Object flagPayload = posthog.getFeatureFlagPayload("distinct-id", "multi-variate-flag");

System.out.println("The flag variant was: " + flagVariate);
System.out.println("Received flag payload: " + flagPayload);

postHog.flush();
postHog.close();
Boolean hasFilePreview = posthog.isFeatureEnabled(
"distinct-id",
"file-previews",
PostHogFeatureFlagOptions
.builder()
.defaultValue(false)
.personProperty("email", "[email protected]")
.build());

System.out.println("File previews enabled: " + hasFilePreview);

posthog.flush();
posthog.close();
}
}
3 changes: 3 additions & 0 deletions posthog-server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Next

- fix!: Restructured `groupProperties` and `userProperties` types to match the API and other SDKs ([#312](https://github.com/PostHog/posthog-android/pull/312))
- feat: Add local evaluation for feature flags ([#299](https://github.com/PostHog/posthog-android/issues/299))

## 1.1.0 - 2025-10-03

- feat: `timestamp` can now be overridden when capturing an event ([#297](https://github.com/PostHog/posthog-android/issues/297))
Expand Down
55 changes: 55 additions & 0 deletions posthog-server/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ PostHogConfig config = PostHogConfig.builder("phc_your_api_key_here")
- `flushIntervalSeconds`: Interval between automatic flushes (default: `30`)
- `featureFlagCacheSize`: The maximum number of feature flags results to cache (default: `1000`)
- `featureFlagCacheMaxAgeMs`: The maximum age of a feature flag cache record in memory in milliseconds (default: `300000` or five minutes)
- `localEvaluation`: Enable local evaluation of feature flags (default: `false`)
- `personalApiKey`: Personal API key required for local evaluation (default: `null`)
- `pollIntervalSeconds`: Interval for polling flag definitions for local evaluation (default: `30`)

## Capturing Events

Expand Down Expand Up @@ -202,6 +205,58 @@ postHog.identify("user123", userProperties, userPropertiesSetOnce);

## Feature Flags

### Local Evaluation (Experimental)

Local evaluation allows the SDK to evaluate feature flags locally without making API calls for each flag check. This reduces latency and API costs.

**How it works:**

1. The SDK periodically polls for flag definitions from PostHog (every 30 seconds by default)
2. Flags are evaluated locally using cached definitions and properties provided by the caller
3. If evaluation is inconclusive (missing properties, etc.), the SDK falls back to the API

**Requirements:**

- A feature flags secure API key _or_ a personal API key
- A feature flags secure API key can be obtained via PostHog → Settings → Project → Feature Flags → Feature Flags Secure API key
- A personal API key can be generated via PostHog → Settings → Account → Personal API Keys
- The `localEvaluation` config option set to `true`

#### Kotlin

```kotlin
val config = PostHogConfig(
apiKey = "phc_your_api_key_here",
host = "https://your-posthog-instance.com",
localEvaluation = true,
personalApiKey = "phx_your_personal_api_key_here",
pollIntervalSeconds = 30 // Optional: customize polling interval
)
```

#### Java

```java
PostHogConfig config = PostHogConfig.builder("phc_your_api_key_here")
.host("https://your-posthog-instance.com")
.localEvaluation(true)
.personalApiKey("phx_your_personal_api_key_here")
.pollIntervalSeconds(30) // Optional: customize polling interval
.build();
```

**Benefits:**

- **Reduced latency**: No API call needed for most flag evaluations
- **Lower costs**: Fewer API requests in most cases
- **Offline support**: Flags continue to work with cached definitions

**Limitations:**

- Requires person/group properties to be provided with each call
- Falls back to API for cohort-based flags without local cohort data
- May not reflect real-time flag changes (respects polling interval)

### Check if Feature is Enabled

#### Kotlin
Expand Down
18 changes: 14 additions & 4 deletions posthog-server/api/posthog-server.api
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,11 @@ public class com/posthog/server/PostHogConfig {
public static final field DEFAULT_HOST Ljava/lang/String;
public static final field DEFAULT_MAX_BATCH_SIZE I
public static final field DEFAULT_MAX_QUEUE_SIZE I
public static final field DEFAULT_POLL_INTERVAL_SECONDS I
public static final field DEFAULT_US_ASSETS_HOST Ljava/lang/String;
public static final field DEFAULT_US_HOST Ljava/lang/String;
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;Ljava/net/Proxy;III)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;Ljava/net/Proxy;IIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;Ljava/net/Proxy;IIIZLjava/lang/String;I)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;Ljava/net/Proxy;IIIZLjava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addBeforeSend (Lcom/posthog/PostHogBeforeSend;)V
public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V
public static final fun builder (Ljava/lang/String;)Lcom/posthog/server/PostHogConfig$Builder;
Expand All @@ -102,9 +103,12 @@ public class com/posthog/server/PostHogConfig {
public final fun getFlushAt ()I
public final fun getFlushIntervalSeconds ()I
public final fun getHost ()Ljava/lang/String;
public final fun getLocalEvaluation ()Z
public final fun getMaxBatchSize ()I
public final fun getMaxQueueSize ()I
public final fun getOnFeatureFlags ()Lcom/posthog/PostHogOnFeatureFlags;
public final fun getPersonalApiKey ()Ljava/lang/String;
public final fun getPollIntervalSeconds ()I
public final fun getPreloadFeatureFlags ()Z
public final fun getProxy ()Ljava/net/Proxy;
public final fun getRemoteConfig ()Z
Expand All @@ -117,9 +121,12 @@ public class com/posthog/server/PostHogConfig {
public final fun setFeatureFlagCalledCacheSize (I)V
public final fun setFlushAt (I)V
public final fun setFlushIntervalSeconds (I)V
public final fun setLocalEvaluation (Z)V
public final fun setMaxBatchSize (I)V
public final fun setMaxQueueSize (I)V
public final fun setOnFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V
public final fun setPersonalApiKey (Ljava/lang/String;)V
public final fun setPollIntervalSeconds (I)V
public final fun setPreloadFeatureFlags (Z)V
public final fun setProxy (Ljava/net/Proxy;)V
public final fun setRemoteConfig (Z)V
Expand All @@ -137,9 +144,12 @@ public final class com/posthog/server/PostHogConfig$Builder {
public final fun flushAt (I)Lcom/posthog/server/PostHogConfig$Builder;
public final fun flushIntervalSeconds (I)Lcom/posthog/server/PostHogConfig$Builder;
public final fun host (Ljava/lang/String;)Lcom/posthog/server/PostHogConfig$Builder;
public final fun localEvaluation (Z)Lcom/posthog/server/PostHogConfig$Builder;
public final fun maxBatchSize (I)Lcom/posthog/server/PostHogConfig$Builder;
public final fun maxQueueSize (I)Lcom/posthog/server/PostHogConfig$Builder;
public final fun onFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)Lcom/posthog/server/PostHogConfig$Builder;
public final fun personalApiKey (Ljava/lang/String;)Lcom/posthog/server/PostHogConfig$Builder;
public final fun pollIntervalSeconds (I)Lcom/posthog/server/PostHogConfig$Builder;
public final fun preloadFeatureFlags (Z)Lcom/posthog/server/PostHogConfig$Builder;
public final fun proxy (Ljava/net/Proxy;)Lcom/posthog/server/PostHogConfig$Builder;
public final fun remoteConfig (Z)Lcom/posthog/server/PostHogConfig$Builder;
Expand Down Expand Up @@ -170,10 +180,10 @@ public final class com/posthog/server/PostHogFeatureFlagOptions$Builder {
public final fun getPersonProperties ()Ljava/util/Map;
public final fun group (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun groupProperties (Ljava/util/Map;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun groupProperty (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun groupProperty (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun groups (Ljava/util/Map;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun personProperties (Ljava/util/Map;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun personProperty (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun personProperty (Ljava/lang/String;Ljava/lang/Object;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
public final fun setDefaultValue (Ljava/lang/Object;)V
public final fun setGroupProperties (Ljava/util/Map;)V
public final fun setGroups (Ljava/util/Map;)V
Expand Down
47 changes: 25 additions & 22 deletions posthog-server/src/main/java/com/posthog/server/PostHog.kt
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
package com.posthog.server

import com.posthog.PostHog
import com.posthog.PostHogStateless
import com.posthog.PostHogStatelessInterface

public class PostHog : PostHogInterface {
private var instance: PostHogStatelessInterface? = null
import com.posthog.server.internal.PostHogFeatureFlags

public class PostHog : PostHogStateless(), PostHogInterface {
override fun <T : PostHogConfig> setup(config: T) {
instance = PostHogStateless.with(config.asCoreConfig())
super.setup(config.asCoreConfig())
}

override fun close() {
instance?.close()
super.close()
}

override fun identify(
distinctId: String,
userProperties: Map<String, Any>?,
userPropertiesSetOnce: Map<String, Any>?,
) {
instance?.identify(
super<PostHogStateless>.identify(
distinctId,
userProperties,
userPropertiesSetOnce,
)
}

override fun flush() {
instance?.flush()
super.flush()
}

override fun debug(enable: Boolean) {
instance?.debug(enable)
super.debug(enable)
}

override fun capture(
Expand All @@ -43,7 +42,7 @@ public class PostHog : PostHogInterface {
groups: Map<String, String>?,
timestamp: java.util.Date?,
) {
instance?.captureStateless(
super.captureStateless(
event,
distinctId,
properties,
Expand All @@ -59,28 +58,28 @@ public class PostHog : PostHogInterface {
key: String,
defaultValue: Boolean,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Boolean {
return instance?.isFeatureEnabledStateless(
return super.isFeatureEnabledStateless(
distinctId,
key,
defaultValue,
groups,
personProperties,
groupProperties,
) ?: false
)
}

override fun getFeatureFlag(
distinctId: String,
key: String,
defaultValue: Any?,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Any? {
return instance?.getFeatureFlagStateless(
return super.getFeatureFlagStateless(
distinctId,
key,
defaultValue,
Expand All @@ -95,10 +94,10 @@ public class PostHog : PostHogInterface {
key: String,
defaultValue: Any?,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Any? {
return instance?.getFeatureFlagPayloadStateless(
return super.getFeatureFlagPayloadStateless(
distinctId,
key,
defaultValue,
Expand All @@ -114,7 +113,7 @@ public class PostHog : PostHogInterface {
key: String,
groupProperties: Map<String, Any>?,
) {
instance?.groupStateless(
super.groupStateless(
distinctId,
type,
key,
Expand All @@ -126,12 +125,16 @@ public class PostHog : PostHogInterface {
distinctId: String,
alias: String,
) {
instance?.aliasStateless(
super.aliasStateless(
distinctId,
alias,
)
}

override fun reloadFeatureFlags() {
(featureFlags as? PostHogFeatureFlags)?.loadFeatureFlagDefinitions()
}

public companion object {
/**
* Set up the SDK and returns an instance that you can hold and pass it around
Expand Down
Loading
Loading