-
Notifications
You must be signed in to change notification settings - Fork 53
fix(flags): correctly emit feature flag events with the FF response on get_feature_flag_payload calls
#143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6f44441
fdf8f54
2ef214a
b114a48
feb9560
d271880
63aeaa3
c30d17b
af2f83e
c544d64
54ae975
85c020b
2694fd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1632,9 +1632,10 @@ def test_boolean_feature_flag_payloads_local(self, patch_decide): | |
| ) | ||
| self.assertEqual(patch_decide.call_count, 0) | ||
|
|
||
| @mock.patch.object(Client, "capture") | ||
| @mock.patch("posthog.client.decide") | ||
| def test_boolean_feature_flag_payload_decide(self, patch_decide): | ||
| patch_decide.return_value = {"featureFlagPayloads": {"person-flag": 300}} | ||
| def test_boolean_feature_flag_payload_decide(self, patch_decide, patch_capture): | ||
| patch_decide.return_value = {"featureFlags": {"person-flag": True}, "featureFlagPayloads": {"person-flag": 300}} | ||
| self.assertEqual( | ||
| self.client.get_feature_flag_payload( | ||
| "person-flag", "some-distinct-id", person_properties={"region": "USA"} | ||
|
|
@@ -1649,6 +1650,8 @@ def test_boolean_feature_flag_payload_decide(self, patch_decide): | |
| 300, | ||
| ) | ||
| self.assertEqual(patch_decide.call_count, 2) | ||
| self.assertEqual(patch_capture.call_count, 1) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. testing that we call |
||
| patch_capture.reset_mock() | ||
|
|
||
| @mock.patch("posthog.client.decide") | ||
| def test_multivariate_feature_flag_payloads(self, patch_decide): | ||
|
|
@@ -2334,6 +2337,88 @@ def test_capture_is_called(self, patch_decide, patch_capture): | |
| disable_geoip=None, | ||
| ) | ||
|
|
||
| @mock.patch.object(Client, "capture") | ||
| @mock.patch("posthog.client.decide") | ||
| def test_capture_is_called_in_get_feature_flag_payload(self, patch_decide, patch_capture): | ||
|
Comment on lines
+2340
to
+2342
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. full suite of tests confirming that |
||
| patch_decide.return_value = { | ||
| "featureFlags": {"person-flag": True}, | ||
| "featureFlagPayloads": {"person-flag": 300}, | ||
| } | ||
| client = Client(api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY) | ||
|
|
||
| client.feature_flags = [ | ||
| { | ||
| "id": 1, | ||
| "name": "Beta Feature", | ||
| "key": "person-flag", | ||
| "is_simple_flag": False, | ||
| "active": True, | ||
| "filters": { | ||
| "groups": [ | ||
| { | ||
| "properties": [{"key": "region", "value": "USA"}], | ||
| "rollout_percentage": 100, | ||
| } | ||
| ], | ||
| }, | ||
| } | ||
| ] | ||
|
|
||
| # Call get_feature_flag_payload with match_value=None to trigger get_feature_flag | ||
| client.get_feature_flag_payload( | ||
| key="person-flag", distinct_id="some-distinct-id", person_properties={"region": "USA", "name": "Aloha"} | ||
| ) | ||
|
|
||
| # Assert that capture was called once, with the correct parameters | ||
| self.assertEqual(patch_capture.call_count, 1) | ||
| patch_capture.assert_called_with( | ||
| "some-distinct-id", | ||
| "$feature_flag_called", | ||
| { | ||
| "$feature_flag": "person-flag", | ||
| "$feature_flag_response": True, | ||
| "$feature_flag_payload": 300, | ||
| "locally_evaluated": False, | ||
| "$feature/person-flag": True, | ||
| }, | ||
| groups={}, | ||
| disable_geoip=None, | ||
| ) | ||
|
|
||
| # Reset mocks for further tests | ||
| patch_capture.reset_mock() | ||
| patch_decide.reset_mock() | ||
|
|
||
| # Call get_feature_flag_payload again for the same user; capture should not be called again because we've already reported an event for this distinct_id + flag | ||
| client.get_feature_flag_payload( | ||
| key="person-flag", distinct_id="some-distinct-id", person_properties={"region": "USA", "name": "Aloha"} | ||
| ) | ||
|
|
||
| self.assertEqual(patch_capture.call_count, 0) | ||
| patch_capture.reset_mock() | ||
|
|
||
| # Call get_feature_flag_payload for a different user; capture should be called | ||
| client.get_feature_flag_payload( | ||
| key="person-flag", distinct_id="some-distinct-id2", person_properties={"region": "USA", "name": "Aloha"} | ||
| ) | ||
|
|
||
| self.assertEqual(patch_capture.call_count, 1) | ||
| patch_capture.assert_called_with( | ||
| "some-distinct-id2", | ||
| "$feature_flag_called", | ||
| { | ||
| "$feature_flag": "person-flag", | ||
| "$feature_flag_response": True, | ||
| "$feature_flag_payload": 300, | ||
| "locally_evaluated": False, | ||
| "$feature/person-flag": True, | ||
| }, | ||
| groups={}, | ||
| disable_geoip=None, | ||
| ) | ||
|
|
||
| patch_capture.reset_mock() | ||
|
|
||
| @mock.patch.object(Client, "capture") | ||
| @mock.patch("posthog.client.decide") | ||
| def test_disable_geoip_get_flag_capture_call(self, patch_decide, patch_capture): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@EDsCODE you wrote this originally; given that this pattern feels to intentionally deviate from the other function arguments, is there any reason why you did this originally? I want to make sure I'm not regressing some other behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the problem now is you're making two requests to
/decidein some cases to fetch the payload (once to get the match value, 2nd time to get the payload).I can see both cases being not ideal though! I think I'd recommend setting
send_feature_flag_eventsto False here, and add a comment about this, and then manually handle sending the event in the right case in the end.Also, I expect all SDKs for payloads to follow this same format, so might have the same problem, worth a quick check in posthog-node atleast (the other popular SDK)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that idea! I feel like I want to be a bit more clever than that, though, because I don't want the event to contain the feature flag payload as the
feature_flag_response, I still want to track whatever response the feature flag returned. A few things I could do on that front:Add a new method to return both the response and payload, and use that method to get both subsets of data
And then do something like this in
get_feature_flag_payloadsAnother thought is to include both the
feature_flag_responseandfeature_flag_payloadas properties on the event (basically do what I did above, but add the payload to thecapturecall (the event would like this)I guess it's up to me to decide what to do here, since it seems like we haven't been doing this with our SDKs, so just brainstorming a bit. Seems easier to not include the payload in the event (since we don't have a nice way of showing it in the flags UI), but I like the idea of tracking both things (more changes, but it gives the users a more holistic view of their events overall, because then they'll be able to differentiate between
feature_flag_calledevents with and without payloads).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slack thread on this: https://posthog.slack.com/archives/C07Q2U4BH4L/p1732202609363039?thread_ts=1732144656.105669&cid=C07Q2U4BH4L
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, closing the loop: I made the decision to include the payload response on the event; couldn't hurt to have more things to break out by!