Skip to content

Commit db1f199

Browse files
dustinbyrnehaacked
andauthored
feat(posthog-server): Add sendFeatureFlags option to capture (#347)
* feat(posthog-server): Add `sendFeatureFlags` option to capture * docs(posthog-server): Update CHANGELOG * docs(posthog-server): Add `sendFeatureFlags` usage to USAGE.md * chore(posthog-server): Update API dump * Update posthog-server/CHANGELOG.md Co-authored-by: Phil Haack <[email protected]> * Rename `sendFeatureFlags` -> `appendFeatureFlags` --------- Co-authored-by: Phil Haack <[email protected]>
1 parent ea03a27 commit db1f199

File tree

9 files changed

+474
-10
lines changed

9 files changed

+474
-10
lines changed

posthog-server/CHANGELOG.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
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))
3+
- feat: Include `evaluated_at` properties in `$feature_flag_called` events ([#321](https://github.com/PostHog/posthog-android/pull/321))
4+
- feat: Add `appendFeatureFlags` optional boolean to `capture` ([#347](https://github.com/PostHog/posthog-android/pull/347))
65

76
## 2.0.1 - 2025-11-24
87

posthog-server/USAGE.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,39 @@ postHog.capture(
175175
)
176176
```
177177

178+
### Enriching Events with Feature Flags
179+
180+
You can automatically enrich captured events with the user's current feature flag values by setting `appendFeatureFlags` to `true`. This adds `$feature/flag_name` properties for each flag and an `$active_feature_flags` array containing all enabled flags.
181+
182+
This is useful for analyzing how feature flags correlate with user behavior without manually tracking flag states.
183+
184+
#### Kotlin
185+
186+
```kotlin
187+
postHog.capture(
188+
distinctId = "user123",
189+
event = "purchase_completed",
190+
properties = mapOf("amount" to 99.99),
191+
appendFeatureFlags = true
192+
)
193+
```
194+
195+
#### Java
196+
197+
```java
198+
postHog.capture(
199+
"user123",
200+
"purchase_completed",
201+
PostHogCaptureOptions
202+
.builder()
203+
.property("amount", 99.99)
204+
.appendFeatureFlags(true)
205+
.build()
206+
);
207+
```
208+
209+
When `appendFeatureFlags` is `true`, the SDK will fetch feature flags for the user (or use locally evaluated flags if local evaluation is enabled) and include them in the event properties.
210+
178211
## Error Tracking
179212

180213
PostHog provides error tracking to help you monitor and debug errors in your application. Use the `captureException` API to log exceptions to PostHog.

posthog-server/api/posthog-server.api

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ public final class com/posthog/server/PostHog : com/posthog/PostHogStateless, co
44
public fun alias (Ljava/lang/String;Ljava/lang/String;)V
55
public fun capture (Ljava/lang/String;Ljava/lang/String;)V
66
public fun capture (Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
7-
public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;)V
7+
public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;Z)V
88
public fun captureException (Ljava/lang/Throwable;)V
99
public fun captureException (Ljava/lang/Throwable;Ljava/lang/String;)V
1010
public fun captureException (Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/Map;)V
@@ -40,8 +40,9 @@ public final class com/posthog/server/PostHog$Companion {
4040

4141
public final class com/posthog/server/PostHogCaptureOptions {
4242
public static final field Companion Lcom/posthog/server/PostHogCaptureOptions$Companion;
43-
public synthetic fun <init> (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
43+
public synthetic fun <init> (Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
4444
public static final fun builder ()Lcom/posthog/server/PostHogCaptureOptions$Builder;
45+
public final fun getAppendFeatureFlags ()Z
4546
public final fun getGroups ()Ljava/util/Map;
4647
public final fun getProperties ()Ljava/util/Map;
4748
public final fun getTimestamp ()Ljava/util/Date;
@@ -51,7 +52,9 @@ public final class com/posthog/server/PostHogCaptureOptions {
5152

5253
public final class com/posthog/server/PostHogCaptureOptions$Builder {
5354
public fun <init> ()V
55+
public final fun appendFeatureFlags (Z)Lcom/posthog/server/PostHogCaptureOptions$Builder;
5456
public final fun build ()Lcom/posthog/server/PostHogCaptureOptions;
57+
public final fun getAppendFeatureFlags ()Z
5558
public final fun getGroups ()Ljava/util/Map;
5659
public final fun getProperties ()Ljava/util/Map;
5760
public final fun getTimestamp ()Ljava/util/Date;
@@ -61,6 +64,7 @@ public final class com/posthog/server/PostHogCaptureOptions$Builder {
6164
public final fun groups (Ljava/util/Map;)Lcom/posthog/server/PostHogCaptureOptions$Builder;
6265
public final fun properties (Ljava/util/Map;)Lcom/posthog/server/PostHogCaptureOptions$Builder;
6366
public final fun property (Ljava/lang/String;Ljava/lang/Object;)Lcom/posthog/server/PostHogCaptureOptions$Builder;
67+
public final fun setAppendFeatureFlags (Z)V
6468
public final fun setGroups (Ljava/util/Map;)V
6569
public final fun setProperties (Ljava/util/Map;)V
6670
public final fun setTimestamp (Ljava/util/Date;)V
@@ -203,7 +207,7 @@ public abstract interface class com/posthog/server/PostHogInterface {
203207
public abstract fun alias (Ljava/lang/String;Ljava/lang/String;)V
204208
public abstract fun capture (Ljava/lang/String;Ljava/lang/String;)V
205209
public abstract fun capture (Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
206-
public abstract synthetic fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;)V
210+
public abstract synthetic fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;Z)V
207211
public abstract fun captureException (Ljava/lang/Throwable;)V
208212
public abstract fun captureException (Ljava/lang/Throwable;Ljava/lang/String;)V
209213
public abstract fun captureException (Ljava/lang/Throwable;Ljava/lang/String;Ljava/util/Map;)V
@@ -235,7 +239,7 @@ public abstract interface class com/posthog/server/PostHogInterface {
235239
public final class com/posthog/server/PostHogInterface$DefaultImpls {
236240
public static fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;)V
237241
public static fun capture (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Lcom/posthog/server/PostHogCaptureOptions;)V
238-
public static synthetic fun capture$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ILjava/lang/Object;)V
242+
public static synthetic fun capture$default (Lcom/posthog/server/PostHogInterface;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Date;ZILjava/lang/Object;)V
239243
public static fun captureException (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;)V
240244
public static fun captureException (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;Ljava/lang/String;)V
241245
public static fun captureException (Lcom/posthog/server/PostHogInterface;Ljava/lang/Throwable;Ljava/util/Map;)V

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,25 @@ public class PostHog : PostHogStateless(), PostHogInterface {
4141
userPropertiesSetOnce: Map<String, Any>?,
4242
groups: Map<String, String>?,
4343
timestamp: java.util.Date?,
44+
appendFeatureFlags: Boolean,
4445
) {
46+
val mergedProperties =
47+
if (appendFeatureFlags) {
48+
mergeFeatureFlagProperties(
49+
distinctId = distinctId,
50+
groups = groups,
51+
userProperties = userProperties,
52+
groupProperties = null,
53+
properties = properties,
54+
)
55+
} else {
56+
properties
57+
}
58+
4559
super.captureStateless(
4660
event,
4761
distinctId,
48-
properties,
62+
mergedProperties,
4963
userProperties,
5064
userPropertiesSetOnce,
5165
groups,
@@ -147,6 +161,45 @@ public class PostHog : PostHogStateless(), PostHogInterface {
147161
)
148162
}
149163

164+
private fun mergeFeatureFlagProperties(
165+
distinctId: String,
166+
groups: Map<String, String>?,
167+
userProperties: Map<String, Any>?,
168+
groupProperties: Map<String, Map<String, Any>>?,
169+
properties: Map<String, Any>?,
170+
): Map<String, Any> {
171+
val props = properties?.toMutableMap() ?: mutableMapOf()
172+
val flags =
173+
(featureFlags as? PostHogFeatureFlags)?.getFeatureFlags(
174+
distinctId = distinctId,
175+
groups = groups,
176+
groupProperties = groupProperties,
177+
personProperties = userProperties,
178+
)
179+
180+
if (flags != null && flags.isNotEmpty()) {
181+
val activeFlags = mutableListOf<String>()
182+
183+
for ((key, flag) in flags) {
184+
val flagValue: Any = flag.variant ?: flag.enabled
185+
props["\$feature/$key"] = flagValue
186+
val isActive =
187+
when (flagValue) {
188+
is Boolean -> flagValue
189+
is String -> flagValue.isNotEmpty()
190+
else -> true
191+
}
192+
if (isActive) {
193+
activeFlags.add(key)
194+
}
195+
}
196+
197+
props["\$active_feature_flags"] = activeFlags
198+
}
199+
200+
return props
201+
}
202+
150203
public companion object {
151204
/**
152205
* Set up the SDK and returns an instance that you can hold and pass it around

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ public class PostHogCaptureOptions private constructor(
1414
public val userPropertiesSetOnce: Map<String, Any>?,
1515
public val groups: Map<String, String>?,
1616
public val timestamp: Date? = null,
17+
public val appendFeatureFlags: Boolean = false,
1718
) {
1819
public class Builder {
1920
public var properties: MutableMap<String, Any>? = null
2021
public var userProperties: MutableMap<String, Any>? = null
2122
public var userPropertiesSetOnce: MutableMap<String, Any>? = null
2223
public var groups: MutableMap<String, String>? = null
2324
public var timestamp: Date? = null
25+
public var appendFeatureFlags: Boolean = false
2426

2527
/**
2628
* Add a single custom property to the capture options
@@ -155,13 +157,25 @@ public class PostHogCaptureOptions private constructor(
155157
return this
156158
}
157159

160+
/**
161+
* When true, enriches the event with all evaluated feature flags.
162+
* Adds `$feature/{flagName}` properties for each flag and
163+
* `$active_feature_flags` array containing names of all truthy flags.
164+
* @see <a href="https://posthog.com/docs/feature-flags">Documentation: Feature Flags</a>
165+
*/
166+
public fun appendFeatureFlags(appendFeatureFlags: Boolean): Builder {
167+
this.appendFeatureFlags = appendFeatureFlags
168+
return this
169+
}
170+
158171
public fun build(): PostHogCaptureOptions =
159172
PostHogCaptureOptions(
160173
properties,
161174
userProperties,
162175
userPropertiesSetOnce,
163176
groups,
164177
timestamp,
178+
appendFeatureFlags,
165179
)
166180
}
167181

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ public sealed interface PostHogInterface {
7070
/**
7171
* Captures events
7272
* @param distinctId the distinctId of the user performing the event
73+
* @param event the event name
7374
* @param properties the custom properties
7475
* @param userProperties the user properties, set as a "$set" property, Docs https://posthog.com/docs/product-analytics/user-properties
7576
* @param userPropertiesSetOnce the user properties to set only once, set as a "$set_once" property, Docs https://posthog.com/docs/product-analytics/user-properties
7677
* @param groups the groups, set as a "$groups" property, Docs https://posthog.com/docs/product-analytics/group-analytics
78+
* @param timestamp the timestamp for the event
79+
* @param appendFeatureFlags when true, enriches the event with feature flag properties
7780
*/
7881
@JvmSynthetic
7982
public fun capture(
@@ -84,13 +87,14 @@ public sealed interface PostHogInterface {
8487
userPropertiesSetOnce: Map<String, Any>? = null,
8588
groups: Map<String, String>? = null,
8689
timestamp: Date? = null,
90+
appendFeatureFlags: Boolean = false,
8791
)
8892

8993
/**
9094
* Captures events
91-
* @param event the event name
9295
* @param distinctId the distinctId
93-
* @param options the capture options containing properties, userProperties, userPropertiesSetOnce, and groups
96+
* @param event the event name
97+
* @param options the capture options containing properties, userProperties, userPropertiesSetOnce, groups, and appendFeatureFlags
9498
*/
9599
public fun capture(
96100
distinctId: String,
@@ -105,6 +109,7 @@ public sealed interface PostHogInterface {
105109
options.userPropertiesSetOnce,
106110
options.groups,
107111
options.timestamp,
112+
options.appendFeatureFlags,
108113
)
109114
}
110115

posthog-server/src/test/java/com/posthog/server/PostHogCaptureOptionsTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,4 +581,43 @@ internal class PostHogCaptureOptionsTest {
581581
assertEquals(date, optionsFromLong.timestamp)
582582
assertEquals(date, optionsFromInstant.timestamp)
583583
}
584+
585+
@Test
586+
fun `appendFeatureFlags defaults to false`() {
587+
val options = PostHogCaptureOptions.builder().build()
588+
589+
assertEquals(false, options.appendFeatureFlags)
590+
}
591+
592+
@Test
593+
fun `appendFeatureFlags can be set to true`() {
594+
val options =
595+
PostHogCaptureOptions.builder()
596+
.appendFeatureFlags(true)
597+
.build()
598+
599+
assertEquals(true, options.appendFeatureFlags)
600+
}
601+
602+
@Test
603+
fun `appendFeatureFlags returns builder for chaining`() {
604+
val builder = PostHogCaptureOptions.builder()
605+
val result = builder.appendFeatureFlags(true)
606+
607+
assertEquals(builder, result)
608+
}
609+
610+
@Test
611+
fun `appendFeatureFlags can be combined with other options`() {
612+
val options =
613+
PostHogCaptureOptions.builder()
614+
.property("key", "value")
615+
.appendFeatureFlags(true)
616+
.timestamp(Date(1234567890L))
617+
.build()
618+
619+
assertEquals(mapOf("key" to "value"), options.properties)
620+
assertEquals(true, options.appendFeatureFlags)
621+
assertEquals(Date(1234567890L), options.timestamp)
622+
}
584623
}

0 commit comments

Comments
 (0)