From 3120e877643bfb136f9d53a42864d38ab81e07aa Mon Sep 17 00:00:00 2001 From: Igor Macedo Quintanilha Date: Thu, 22 Jan 2026 12:22:16 -0300 Subject: [PATCH] feat: propagate personProperties and groups in feature flag events When `getFeatureFlagStateless` calls `captureStateless` for the `$feature_flag_called` event, it now propagates the `personProperties` (as userProperties) and `groups` to the event. Previously these properties were available but not passed through, meaning the feature flag evaluation context was lost in the captured event. This ensures that when analyzing feature flag events in PostHog, the user properties and group context are properly attached for accurate targeting and analysis. Fixes part of the issue by ensuring proper event context propagation. --- .../main/java/com/posthog/PostHogStateless.kt | 11 +- .../java/com/posthog/PostHogStatelessTest.kt | 116 ++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/posthog/src/main/java/com/posthog/PostHogStateless.kt b/posthog/src/main/java/com/posthog/PostHogStateless.kt index 5628b166..ae2cba28 100644 --- a/posthog/src/main/java/com/posthog/PostHogStateless.kt +++ b/posthog/src/main/java/com/posthog/PostHogStateless.kt @@ -454,7 +454,16 @@ public open class PostHogStateless protected constructor( groupProperties, )?.let { props["\$feature_flag_error"] = it } - captureStateless(PostHogEventName.FEATURE_FLAG_CALLED.event, distinctId, properties = props) + val userProps = personProperties + ?.filterValues { it != null } + ?.mapValues { it.value!! } + captureStateless( + PostHogEventName.FEATURE_FLAG_CALLED.event, + distinctId, + properties = props, + userProperties = userProps, + groups = groups, + ) } } } diff --git a/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt b/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt index dea8871c..b3561792 100644 --- a/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogStatelessTest.kt @@ -811,6 +811,122 @@ internal class PostHogStatelessTest { assertEquals(true, event.properties!!["\$feature_flag_response"]) } + @Test + fun `feature flag called events propagate userProperties and groups`() { + val mockQueue = MockQueue() + val mockFeatureFlags = MockFeatureFlags() + mockFeatureFlags.setFlag("test_flag", "variant_a") + + sut = createStatelessInstance() + config = createConfig(sendFeatureFlagEvent = true) + + sut.setup(config) + sut.setMockQueue(mockQueue) + sut.setMockFeatureFlags(mockFeatureFlags) + + val groups = mapOf("organization" to "org_123") + val personProperties = mapOf("plan" to "premium", "role" to "admin") + + // Access feature flag with groups and person properties + sut.getFeatureFlagStateless( + "user123", + "test_flag", + null, + groups, + personProperties, + null, + ) + + // Should generate feature flag called event with propagated properties + assertEquals(1, mockQueue.events.size) + val event = mockQueue.events.first() + assertEquals("\$feature_flag_called", event.event) + assertEquals("user123", event.distinctId) + assertEquals("test_flag", event.properties!!["\$feature_flag"]) + assertEquals("variant_a", event.properties!!["\$feature_flag_response"]) + + // Check that groups are propagated + assertEquals(mapOf("organization" to "org_123"), event.properties!!["\$groups"]) + + // Check that userProperties are propagated (as $set) + @Suppress("UNCHECKED_CAST") + val setProps = event.properties!!["\$set"] as? Map + assertNotNull(setProps) + assertEquals("premium", setProps["plan"]) + assertEquals("admin", setProps["role"]) + } + + @Test + fun `feature flag called events filter out null values from personProperties`() { + val mockQueue = MockQueue() + val mockFeatureFlags = MockFeatureFlags() + mockFeatureFlags.setFlag("test_flag", true) + + sut = createStatelessInstance() + config = createConfig(sendFeatureFlagEvent = true) + + sut.setup(config) + sut.setMockQueue(mockQueue) + sut.setMockFeatureFlags(mockFeatureFlags) + + val personProperties = mapOf("plan" to "premium", "nullable" to null) + + // Access feature flag with person properties containing null + sut.isFeatureEnabledStateless( + "user123", + "test_flag", + false, + null, + personProperties, + null, + ) + + assertEquals(1, mockQueue.events.size) + val event = mockQueue.events.first() + + // Check that userProperties are propagated without null values + @Suppress("UNCHECKED_CAST") + val setProps = event.properties!!["\$set"] as? Map + assertNotNull(setProps) + assertEquals("premium", setProps["plan"]) + assertFalse(setProps.containsKey("nullable")) + } + + @Test + fun `feature flag called events handle null personProperties gracefully`() { + val mockQueue = MockQueue() + val mockFeatureFlags = MockFeatureFlags() + mockFeatureFlags.setFlag("test_flag", true) + + sut = createStatelessInstance() + config = createConfig(sendFeatureFlagEvent = true) + + sut.setup(config) + sut.setMockQueue(mockQueue) + sut.setMockFeatureFlags(mockFeatureFlags) + + val groups = mapOf("organization" to "org_123") + + // Access feature flag with null person properties + sut.isFeatureEnabledStateless( + "user123", + "test_flag", + false, + groups, + null, + null, + ) + + assertEquals(1, mockQueue.events.size) + val event = mockQueue.events.first() + + // Check that groups are still propagated + assertEquals(mapOf("organization" to "org_123"), event.properties!!["\$groups"]) + + // Check that $set is not present when personProperties is null + assertNull(event.properties!!["\$set"]) + } + @Test fun `feature flag called events not sent when disabled`() { val mockQueue = MockQueue()