Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion posthog/src/main/java/com/posthog/PostHogStateless.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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!! }
Comment thread
igormq marked this conversation as resolved.
captureStateless(
PostHogEventName.FEATURE_FLAG_CALLED.event,
distinctId,
properties = props,
userProperties = userProps,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Providing userProperties here will mutate persisted person properties. That seems unexpected for a feature flag called event.

What's the behavior we're looking for by doing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

shouldn't it? for me if we change a property of the user with a feature flag, we should reflect it , shouldnt we?
please let me know if this is not the intention

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dustinbyrne mind to follow up here? PR is stale for a while

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm open to hearing more about the case of changing a property with a feature flag. Currently, however, I don't believe this is the behavior in any of our other SDKs. The $feature_flag_called event is fired off as a side-effect of reading a feature flag. The reason I say it feels unexpected is because we'd now be implicitly mutating a person record in a read-only operation.

The additional propagation of groups that you've added does look like a gap. We should be passing that.

groups = groups,
)
}
}
}
Expand Down
116 changes: 116 additions & 0 deletions posthog/src/test/java/com/posthog/PostHogStatelessTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any?>("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<String, Any>
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<String, Any?>("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<String, Any>
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()
Expand Down