Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
2 changes: 2 additions & 0 deletions posthog-server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- fix!: Restructured `groupProperties` and `userProperties` types to match the API and other SDKs ([#312](https://github.com/PostHog/posthog-android/pull/312))

## 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
4 changes: 2 additions & 2 deletions posthog-server/api/posthog-server.api
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,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
12 changes: 6 additions & 6 deletions posthog-server/src/main/java/com/posthog/server/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public class PostHog : PostHogInterface {
key: String,
defaultValue: Boolean,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this break the public API if someone declare and pass a Map<String, String>? should we raise a major?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is technically a major version bump in both packages. Hopefully this should be the last for a while.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we prob should normalize all params with Any since non-serialized values can break serialization (confirm if GSON does this automatically?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GSON should be able to do this automatically but I'll include a test to verify

Copy link
Contributor Author

@dustinbyrne dustinbyrne Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

886d3a2

Does this address your concerns? I'd started looking at normalizing into primitive JSON types, but it seems GSON's default behavior may suffice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably yes, we do this on iOS https://github.com/PostHog/posthog-ios/blob/ba5dd65d2341ab8ea1949f072594ef67c490af3a/PostHog/Utils/DictUtils.swift#L10
if GSON behaves the same way, we are good.
It's important that GSON either stringifies any non-serializable object or just does not set it, rather than failing the serialization.
I'd suggest using a different type of object, which is not serializable eg a Context, Activity, View classes in Android, I suspect it might break the serialization but i am not 100% sure

groupProperties: Map<String, Map<String, Any?>>?,
): Boolean {
return instance?.isFeatureEnabledStateless(
distinctId,
Expand All @@ -77,8 +77,8 @@ 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?.getFeatureFlagStateless(
distinctId,
Expand All @@ -95,8 +95,8 @@ 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(
distinctId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ package com.posthog.server
public class PostHogFeatureFlagOptions private constructor(
public val defaultValue: Any?,
public val groups: Map<String, String>?,
public val personProperties: Map<String, String>?,
public val groupProperties: Map<String, String>?,
public val personProperties: Map<String, Any?>?,
public val groupProperties: Map<String, Map<String, Any?>>?,
) {
public class Builder {
public var defaultValue: Any? = null
public var groups: MutableMap<String, String>? = null
public var personProperties: MutableMap<String, String>? = null
public var groupProperties: MutableMap<String, String>? = null
public var personProperties: MutableMap<String, Any?>? = null
public var groupProperties: MutableMap<String, MutableMap<String, Any?>>? = null

/**
* Sets the default value to return if the feature flag is not found or not enabled
Expand All @@ -29,11 +29,11 @@ public class PostHogFeatureFlagOptions private constructor(
*/
public fun group(
key: String,
value: String,
propValue: String,
): Builder {
groups =
(groups ?: mutableMapOf()).apply {
put(key, value)
put(key, propValue)
}
return this
}
Expand All @@ -55,11 +55,11 @@ public class PostHogFeatureFlagOptions private constructor(
*/
public fun personProperty(
key: String,
value: String,
propValue: Any?,
): Builder {
personProperties =
(personProperties ?: mutableMapOf()).apply {
put(key, value)
put(key, propValue)
}
return this
}
Expand All @@ -68,7 +68,7 @@ public class PostHogFeatureFlagOptions private constructor(
* Appends multiple user properties to the capture options.
* @see <a href="https://posthog.com/docs/product-analytics/user-properties">Documentation: User Properties</a>
*/
public fun personProperties(userProperties: Map<String, String>): Builder {
public fun personProperties(userProperties: Map<String, Any?>): Builder {
this.personProperties =
(this.personProperties ?: mutableMapOf()).apply {
putAll(userProperties)
Expand All @@ -81,12 +81,13 @@ public class PostHogFeatureFlagOptions private constructor(
* @see <a href="https://posthog.com/docs/product-analytics/user-properties">Documentation: User Properties</a>
*/
public fun groupProperty(
group: String,
key: String,
value: String,
propValue: Any?,
): Builder {
groupProperties =
(groupProperties ?: mutableMapOf()).apply {
put(key, value)
getOrPut(group) { mutableMapOf() }[key] = propValue
}
return this
}
Expand All @@ -95,10 +96,12 @@ public class PostHogFeatureFlagOptions private constructor(
* Appends multiple user properties (set once) to the capture options.
* @see <a href="https://posthog.com/docs/product-analytics/user-properties">Documentation: User Properties</a>
*/
public fun groupProperties(groupProperties: Map<String, String>): Builder {
public fun groupProperties(groupProperties: Map<String, Map<String, Any?>>): Builder {
this.groupProperties =
(this.groupProperties ?: mutableMapOf()).apply {
putAll(groupProperties)
groupProperties.forEach { (group, properties) ->
getOrPut(group) { mutableMapOf() }.putAll(properties)
}
}
return this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ public sealed interface PostHogInterface {
key: String,
defaultValue: Boolean = false,
groups: Map<String, String>? = null,
personProperties: Map<String, String>? = null,
groupProperties: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
): Boolean

/**
Expand Down Expand Up @@ -219,8 +219,8 @@ public sealed interface PostHogInterface {
key: String,
defaultValue: Any? = null,
groups: Map<String, String>? = null,
personProperties: Map<String, String>? = null,
groupProperties: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
): Any?

/**
Expand Down Expand Up @@ -300,8 +300,8 @@ public sealed interface PostHogInterface {
key: String,
defaultValue: Any? = null,
groups: Map<String, String>? = null,
personProperties: Map<String, String>? = null,
groupProperties: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
): Any?

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ package com.posthog.server.internal
internal data class FeatureFlagCacheKey(
val distinctId: String?,
val groups: Map<String, String>?,
val personProperties: Map<String, String>?,
val groupProperties: Map<String, String>?,
val personProperties: Map<String, Any?>?,
val groupProperties: Map<String, Map<String, Any?>>?,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ internal class PostHogFeatureFlags(
defaultValue: Any?,
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Any? {
val flag =
getFeatureFlags(
Expand All @@ -40,8 +40,8 @@ internal class PostHogFeatureFlags(
defaultValue: Any?,
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Any? {
return getFeatureFlags(
distinctId,
Expand All @@ -55,8 +55,8 @@ internal class PostHogFeatureFlags(
override fun getFeatureFlags(
distinctId: String?,
groups: Map<String, String>?,
personProperties: Map<String, String>?,
groupProperties: Map<String, String>?,
personProperties: Map<String, Any?>?,
groupProperties: Map<String, Map<String, Any?>>?,
): Map<String, FeatureFlag>? {
if (distinctId == null) {
config.logger.log("getFeatureFlags called but no distinctId available for API call")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ internal class PostHogFeatureFlagOptionsTest {

builder.personProperty("plan", "premium")

assertEquals(mutableMapOf<String, String>("plan" to "premium"), builder.personProperties)
assertEquals(mutableMapOf<String, Any?>("plan" to "premium"), builder.personProperties)
}

@Test
Expand Down Expand Up @@ -199,7 +199,7 @@ internal class PostHogFeatureFlagOptionsTest {
val propertiesToAdd = mapOf("plan" to "premium")
builder.personProperties(propertiesToAdd)

assertEquals(mutableMapOf<String, String>("plan" to "premium"), builder.personProperties)
assertEquals(mutableMapOf<String, Any?>("plan" to "premium"), builder.personProperties)
}

@Test
Expand Down Expand Up @@ -231,44 +231,44 @@ internal class PostHogFeatureFlagOptionsTest {
fun `groupProperty method adds single group property`() {
val options =
PostHogFeatureFlagOptions.builder()
.groupProperty("industry", "tech")
.groupProperty("my-org", "industry", "tech")
.build()

assertEquals(mapOf("industry" to "tech"), options.groupProperties)
assertEquals(mapOf("my-org" to mapOf("industry" to "tech")), options.groupProperties)
}

@Test
fun `groupProperty method creates groupProperties map when null`() {
val builder = PostHogFeatureFlagOptions.builder()
assertNull(builder.groupProperties)

builder.groupProperty("industry", "tech")
builder.groupProperty("my-org", "industry", "tech")

assertEquals(mutableMapOf<String, String>("industry" to "tech"), builder.groupProperties)
assertEquals(mutableMapOf("my-org" to mutableMapOf<String, Any?>("industry" to "tech")), builder.groupProperties)
}

@Test
fun `groupProperty method adds to existing groupProperties map`() {
val options =
PostHogFeatureFlagOptions.builder()
.groupProperty("industry", "tech")
.groupProperty("size", "large")
.groupProperty("my-org", "industry", "tech")
.groupProperty("my-org", "size", "large")
.build()

assertEquals(mapOf("industry" to "tech", "size" to "large"), options.groupProperties)
assertEquals(mapOf("my-org" to mapOf("industry" to "tech", "size" to "large")), options.groupProperties)
}

@Test
fun `groupProperty method returns builder for chaining`() {
val builder = PostHogFeatureFlagOptions.builder()
val result = builder.groupProperty("industry", "tech")
val result = builder.groupProperty("my-org", "industry", "tech")

assertEquals(builder, result)
}

@Test
fun `groupProperties method adds multiple group properties`() {
val propertiesToAdd = mapOf("industry" to "tech", "size" to "large")
val propertiesToAdd = mapOf("my-org" to mapOf("industry" to "tech", "size" to "large"))
val options =
PostHogFeatureFlagOptions.builder()
.groupProperties(propertiesToAdd)
Expand All @@ -282,33 +282,36 @@ internal class PostHogFeatureFlagOptionsTest {
val builder = PostHogFeatureFlagOptions.builder()
assertNull(builder.groupProperties)

val propertiesToAdd = mapOf("industry" to "tech")
val propertiesToAdd = mapOf("my-org" to mapOf("industry" to "tech"))
builder.groupProperties(propertiesToAdd)

assertEquals(mutableMapOf<String, String>("industry" to "tech"), builder.groupProperties)
assertEquals(mutableMapOf("my-org" to mutableMapOf<String, Any?>("industry" to "tech")), builder.groupProperties)
}

@Test
fun `groupProperties method appends to existing groupProperties`() {
val options =
PostHogFeatureFlagOptions.builder()
.groupProperty("existing_key", "existing_value")
.groupProperties(mapOf("new_key1" to "new_value1", "new_key2" to "new_value2"))
.groupProperty("my-org", "existing_key", "existing_value")
.groupProperties(mapOf("my-org" to mapOf("new_key1" to "new_value1", "new_key2" to "new_value2")))
.build()

val expected =
mapOf(
"existing_key" to "existing_value",
"new_key1" to "new_value1",
"new_key2" to "new_value2",
"my-org" to
mapOf(
"existing_key" to "existing_value",
"new_key1" to "new_value1",
"new_key2" to "new_value2",
),
)
assertEquals(expected, options.groupProperties)
}

@Test
fun `groupProperties method returns builder for chaining`() {
val builder = PostHogFeatureFlagOptions.builder()
val result = builder.groupProperties(mapOf("industry" to "tech"))
val result = builder.groupProperties(mapOf("my-org" to mapOf("industry" to "tech")))

assertEquals(builder, result)
}
Expand All @@ -322,14 +325,14 @@ internal class PostHogFeatureFlagOptionsTest {
.groups(mapOf("team" to "team_456"))
.personProperty("plan", "premium")
.personProperties(mapOf("role" to "admin"))
.groupProperty("industry", "tech")
.groupProperties(mapOf("size" to "large"))
.groupProperty("my-org", "industry", "tech")
.groupProperties(mapOf("my-org" to mapOf("size" to "large")))
.build()

assertEquals("default", options.defaultValue)
assertEquals(mapOf("organization" to "org_123", "team" to "team_456"), options.groups)
assertEquals(mapOf("plan" to "premium", "role" to "admin"), options.personProperties)
assertEquals(mapOf("industry" to "tech", "size" to "large"), options.groupProperties)
assertEquals(mapOf("my-org" to mapOf("industry" to "tech", "size" to "large")), options.groupProperties)
}

@Test
Expand Down Expand Up @@ -358,11 +361,11 @@ internal class PostHogFeatureFlagOptionsTest {
fun `overwriting same key in groupProperties replaces value`() {
val options =
PostHogFeatureFlagOptions.builder()
.groupProperty("size", "small")
.groupProperty("size", "large")
.groupProperty("my-org", "size", "small")
.groupProperty("my-org", "size", "large")
.build()

assertEquals(mapOf("size" to "large"), options.groupProperties)
assertEquals(mapOf("my-org" to mapOf("size" to "large")), options.groupProperties)
}

@Test
Expand Down
Loading
Loading