Skip to content

Commit 06dd8ae

Browse files
authored
fix!: Correctly type user and group properties (#312)
* fix!: Correctly type user and group properties * docs: Update CHANGELOG * chore: lint and api dump * docs: Update CHANGELOG * drop PostHogLocalEvaluationModels.kt * docs: Update CHANGELOG * test: Validate `Any?` serialization * feat: Safely deserialize Map<String, Any> This allows properties to be safely deserialized without raising exceptions. If an exception is raised while deserializing a property, the property will simply be ignored and an error logged.
1 parent 3b565ba commit 06dd8ae

22 files changed

+351
-121
lines changed

posthog-server/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Next
22

3+
- fix: Restructured `groupProperties` and `userProperties` types to match the API and other SDKs ([#312](https://github.com/PostHog/posthog-android/pull/312))
4+
35
## 1.1.0 - 2025-10-03
46

57
- feat: `timestamp` can now be overridden when capturing an event ([#297](https://github.com/PostHog/posthog-android/issues/297))

posthog-server/api/posthog-server.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,10 @@ public final class com/posthog/server/PostHogFeatureFlagOptions$Builder {
170170
public final fun getPersonProperties ()Ljava/util/Map;
171171
public final fun group (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
172172
public final fun groupProperties (Ljava/util/Map;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
173-
public final fun groupProperty (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
173+
public final fun groupProperty (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
174174
public final fun groups (Ljava/util/Map;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
175175
public final fun personProperties (Ljava/util/Map;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
176-
public final fun personProperty (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
176+
public final fun personProperty (Ljava/lang/String;Ljava/lang/Object;)Lcom/posthog/server/PostHogFeatureFlagOptions$Builder;
177177
public final fun setDefaultValue (Ljava/lang/Object;)V
178178
public final fun setGroupProperties (Ljava/util/Map;)V
179179
public final fun setGroups (Ljava/util/Map;)V

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ public class PostHog : PostHogInterface {
5959
key: String,
6060
defaultValue: Boolean,
6161
groups: Map<String, String>?,
62-
personProperties: Map<String, String>?,
63-
groupProperties: Map<String, String>?,
62+
personProperties: Map<String, Any?>?,
63+
groupProperties: Map<String, Map<String, Any?>>?,
6464
): Boolean {
6565
return instance?.isFeatureEnabledStateless(
6666
distinctId,
@@ -77,8 +77,8 @@ public class PostHog : PostHogInterface {
7777
key: String,
7878
defaultValue: Any?,
7979
groups: Map<String, String>?,
80-
personProperties: Map<String, String>?,
81-
groupProperties: Map<String, String>?,
80+
personProperties: Map<String, Any?>?,
81+
groupProperties: Map<String, Map<String, Any?>>?,
8282
): Any? {
8383
return instance?.getFeatureFlagStateless(
8484
distinctId,
@@ -95,8 +95,8 @@ public class PostHog : PostHogInterface {
9595
key: String,
9696
defaultValue: Any?,
9797
groups: Map<String, String>?,
98-
personProperties: Map<String, String>?,
99-
groupProperties: Map<String, String>?,
98+
personProperties: Map<String, Any?>?,
99+
groupProperties: Map<String, Map<String, Any?>>?,
100100
): Any? {
101101
return instance?.getFeatureFlagPayloadStateless(
102102
distinctId,

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

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ package com.posthog.server
77
public class PostHogFeatureFlagOptions private constructor(
88
public val defaultValue: Any?,
99
public val groups: Map<String, String>?,
10-
public val personProperties: Map<String, String>?,
11-
public val groupProperties: Map<String, String>?,
10+
public val personProperties: Map<String, Any?>?,
11+
public val groupProperties: Map<String, Map<String, Any?>>?,
1212
) {
1313
public class Builder {
1414
public var defaultValue: Any? = null
1515
public var groups: MutableMap<String, String>? = null
16-
public var personProperties: MutableMap<String, String>? = null
17-
public var groupProperties: MutableMap<String, String>? = null
16+
public var personProperties: MutableMap<String, Any?>? = null
17+
public var groupProperties: MutableMap<String, MutableMap<String, Any?>>? = null
1818

1919
/**
2020
* Sets the default value to return if the feature flag is not found or not enabled
@@ -29,11 +29,11 @@ public class PostHogFeatureFlagOptions private constructor(
2929
*/
3030
public fun group(
3131
key: String,
32-
value: String,
32+
propValue: String,
3333
): Builder {
3434
groups =
3535
(groups ?: mutableMapOf()).apply {
36-
put(key, value)
36+
put(key, propValue)
3737
}
3838
return this
3939
}
@@ -55,11 +55,11 @@ public class PostHogFeatureFlagOptions private constructor(
5555
*/
5656
public fun personProperty(
5757
key: String,
58-
value: String,
58+
propValue: Any?,
5959
): Builder {
6060
personProperties =
6161
(personProperties ?: mutableMapOf()).apply {
62-
put(key, value)
62+
put(key, propValue)
6363
}
6464
return this
6565
}
@@ -68,7 +68,7 @@ public class PostHogFeatureFlagOptions private constructor(
6868
* Appends multiple user properties to the capture options.
6969
* @see <a href="https://posthog.com/docs/product-analytics/user-properties">Documentation: User Properties</a>
7070
*/
71-
public fun personProperties(userProperties: Map<String, String>): Builder {
71+
public fun personProperties(userProperties: Map<String, Any?>): Builder {
7272
this.personProperties =
7373
(this.personProperties ?: mutableMapOf()).apply {
7474
putAll(userProperties)
@@ -81,12 +81,13 @@ public class PostHogFeatureFlagOptions private constructor(
8181
* @see <a href="https://posthog.com/docs/product-analytics/user-properties">Documentation: User Properties</a>
8282
*/
8383
public fun groupProperty(
84+
group: String,
8485
key: String,
85-
value: String,
86+
propValue: Any?,
8687
): Builder {
8788
groupProperties =
8889
(groupProperties ?: mutableMapOf()).apply {
89-
put(key, value)
90+
getOrPut(group) { mutableMapOf() }[key] = propValue
9091
}
9192
return this
9293
}
@@ -95,10 +96,12 @@ public class PostHogFeatureFlagOptions private constructor(
9596
* Appends multiple user properties (set once) to the capture options.
9697
* @see <a href="https://posthog.com/docs/product-analytics/user-properties">Documentation: User Properties</a>
9798
*/
98-
public fun groupProperties(groupProperties: Map<String, String>): Builder {
99+
public fun groupProperties(groupProperties: Map<String, Map<String, Any?>>): Builder {
99100
this.groupProperties =
100101
(this.groupProperties ?: mutableMapOf()).apply {
101-
putAll(groupProperties)
102+
groupProperties.forEach { (group, properties) ->
103+
getOrPut(group) { mutableMapOf() }.putAll(properties)
104+
}
102105
}
103106
return this
104107
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ public sealed interface PostHogInterface {
139139
key: String,
140140
defaultValue: Boolean = false,
141141
groups: Map<String, String>? = null,
142-
personProperties: Map<String, String>? = null,
143-
groupProperties: Map<String, String>? = null,
142+
personProperties: Map<String, Any?>? = null,
143+
groupProperties: Map<String, Map<String, Any?>>? = null,
144144
): Boolean
145145

146146
/**
@@ -219,8 +219,8 @@ public sealed interface PostHogInterface {
219219
key: String,
220220
defaultValue: Any? = null,
221221
groups: Map<String, String>? = null,
222-
personProperties: Map<String, String>? = null,
223-
groupProperties: Map<String, String>? = null,
222+
personProperties: Map<String, Any?>? = null,
223+
groupProperties: Map<String, Map<String, Any?>>? = null,
224224
): Any?
225225

226226
/**
@@ -300,8 +300,8 @@ public sealed interface PostHogInterface {
300300
key: String,
301301
defaultValue: Any? = null,
302302
groups: Map<String, String>? = null,
303-
personProperties: Map<String, String>? = null,
304-
groupProperties: Map<String, String>? = null,
303+
personProperties: Map<String, Any?>? = null,
304+
groupProperties: Map<String, Map<String, Any?>>? = null,
305305
): Any?
306306

307307
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ package com.posthog.server.internal
66
internal data class FeatureFlagCacheKey(
77
val distinctId: String?,
88
val groups: Map<String, String>?,
9-
val personProperties: Map<String, String>?,
10-
val groupProperties: Map<String, String>?,
9+
val personProperties: Map<String, Any?>?,
10+
val groupProperties: Map<String, Map<String, Any?>>?,
1111
) {
1212
override fun equals(other: Any?): Boolean {
1313
if (this === other) return true

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ internal class PostHogFeatureFlags(
2222
defaultValue: Any?,
2323
distinctId: String?,
2424
groups: Map<String, String>?,
25-
personProperties: Map<String, String>?,
26-
groupProperties: Map<String, String>?,
25+
personProperties: Map<String, Any?>?,
26+
groupProperties: Map<String, Map<String, Any?>>?,
2727
): Any? {
2828
val flag =
2929
getFeatureFlags(
@@ -40,8 +40,8 @@ internal class PostHogFeatureFlags(
4040
defaultValue: Any?,
4141
distinctId: String?,
4242
groups: Map<String, String>?,
43-
personProperties: Map<String, String>?,
44-
groupProperties: Map<String, String>?,
43+
personProperties: Map<String, Any?>?,
44+
groupProperties: Map<String, Map<String, Any?>>?,
4545
): Any? {
4646
return getFeatureFlags(
4747
distinctId,
@@ -55,8 +55,8 @@ internal class PostHogFeatureFlags(
5555
override fun getFeatureFlags(
5656
distinctId: String?,
5757
groups: Map<String, String>?,
58-
personProperties: Map<String, String>?,
59-
groupProperties: Map<String, String>?,
58+
personProperties: Map<String, Any?>?,
59+
groupProperties: Map<String, Map<String, Any?>>?,
6060
): Map<String, FeatureFlag>? {
6161
if (distinctId == null) {
6262
config.logger.log("getFeatureFlags called but no distinctId available for API call")

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

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ internal class PostHogFeatureFlagOptionsTest {
158158

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

161-
assertEquals(mutableMapOf<String, String>("plan" to "premium"), builder.personProperties)
161+
assertEquals(mutableMapOf<String, Any?>("plan" to "premium"), builder.personProperties)
162162
}
163163

164164
@Test
@@ -199,7 +199,7 @@ internal class PostHogFeatureFlagOptionsTest {
199199
val propertiesToAdd = mapOf("plan" to "premium")
200200
builder.personProperties(propertiesToAdd)
201201

202-
assertEquals(mutableMapOf<String, String>("plan" to "premium"), builder.personProperties)
202+
assertEquals(mutableMapOf<String, Any?>("plan" to "premium"), builder.personProperties)
203203
}
204204

205205
@Test
@@ -231,44 +231,44 @@ internal class PostHogFeatureFlagOptionsTest {
231231
fun `groupProperty method adds single group property`() {
232232
val options =
233233
PostHogFeatureFlagOptions.builder()
234-
.groupProperty("industry", "tech")
234+
.groupProperty("my-org", "industry", "tech")
235235
.build()
236236

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

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

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

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

250250
@Test
251251
fun `groupProperty method adds to existing groupProperties map`() {
252252
val options =
253253
PostHogFeatureFlagOptions.builder()
254-
.groupProperty("industry", "tech")
255-
.groupProperty("size", "large")
254+
.groupProperty("my-org", "industry", "tech")
255+
.groupProperty("my-org", "size", "large")
256256
.build()
257257

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

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

266266
assertEquals(builder, result)
267267
}
268268

269269
@Test
270270
fun `groupProperties method adds multiple group properties`() {
271-
val propertiesToAdd = mapOf("industry" to "tech", "size" to "large")
271+
val propertiesToAdd = mapOf("my-org" to mapOf("industry" to "tech", "size" to "large"))
272272
val options =
273273
PostHogFeatureFlagOptions.builder()
274274
.groupProperties(propertiesToAdd)
@@ -282,33 +282,36 @@ internal class PostHogFeatureFlagOptionsTest {
282282
val builder = PostHogFeatureFlagOptions.builder()
283283
assertNull(builder.groupProperties)
284284

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

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

291291
@Test
292292
fun `groupProperties method appends to existing groupProperties`() {
293293
val options =
294294
PostHogFeatureFlagOptions.builder()
295-
.groupProperty("existing_key", "existing_value")
296-
.groupProperties(mapOf("new_key1" to "new_value1", "new_key2" to "new_value2"))
295+
.groupProperty("my-org", "existing_key", "existing_value")
296+
.groupProperties(mapOf("my-org" to mapOf("new_key1" to "new_value1", "new_key2" to "new_value2")))
297297
.build()
298298

299299
val expected =
300300
mapOf(
301-
"existing_key" to "existing_value",
302-
"new_key1" to "new_value1",
303-
"new_key2" to "new_value2",
301+
"my-org" to
302+
mapOf(
303+
"existing_key" to "existing_value",
304+
"new_key1" to "new_value1",
305+
"new_key2" to "new_value2",
306+
),
304307
)
305308
assertEquals(expected, options.groupProperties)
306309
}
307310

308311
@Test
309312
fun `groupProperties method returns builder for chaining`() {
310313
val builder = PostHogFeatureFlagOptions.builder()
311-
val result = builder.groupProperties(mapOf("industry" to "tech"))
314+
val result = builder.groupProperties(mapOf("my-org" to mapOf("industry" to "tech")))
312315

313316
assertEquals(builder, result)
314317
}
@@ -322,14 +325,14 @@ internal class PostHogFeatureFlagOptionsTest {
322325
.groups(mapOf("team" to "team_456"))
323326
.personProperty("plan", "premium")
324327
.personProperties(mapOf("role" to "admin"))
325-
.groupProperty("industry", "tech")
326-
.groupProperties(mapOf("size" to "large"))
328+
.groupProperty("my-org", "industry", "tech")
329+
.groupProperties(mapOf("my-org" to mapOf("size" to "large")))
327330
.build()
328331

329332
assertEquals("default", options.defaultValue)
330333
assertEquals(mapOf("organization" to "org_123", "team" to "team_456"), options.groups)
331334
assertEquals(mapOf("plan" to "premium", "role" to "admin"), options.personProperties)
332-
assertEquals(mapOf("industry" to "tech", "size" to "large"), options.groupProperties)
335+
assertEquals(mapOf("my-org" to mapOf("industry" to "tech", "size" to "large")), options.groupProperties)
333336
}
334337

335338
@Test
@@ -358,11 +361,11 @@ internal class PostHogFeatureFlagOptionsTest {
358361
fun `overwriting same key in groupProperties replaces value`() {
359362
val options =
360363
PostHogFeatureFlagOptions.builder()
361-
.groupProperty("size", "small")
362-
.groupProperty("size", "large")
364+
.groupProperty("my-org", "size", "small")
365+
.groupProperty("my-org", "size", "large")
363366
.build()
364367

365-
assertEquals(mapOf("size" to "large"), options.groupProperties)
368+
assertEquals(mapOf("my-org" to mapOf("size" to "large")), options.groupProperties)
366369
}
367370

368371
@Test

0 commit comments

Comments
 (0)