Skip to content

Commit e60d52c

Browse files
authored
feat(flags): Add local props and flags to all calls (#106)
1 parent a2c73d0 commit e60d52c

File tree

4 files changed

+212
-5
lines changed

4 files changed

+212
-5
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.3.0 - 2024-01-09
2+
3+
1. When local evaluation is enabled, we automatically add flag information to all events sent to PostHog, whenever possible. This makes it easier to use these events in experiments.
4+
15
## 3.2.0 - 2024-01-09
26

37
1. Numeric property handling for feature flags now does the expected: When passed in a number, we do a numeric comparison. When passed in a string, we do a string comparison. Previously, we always did a string comparison.

posthog/client.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,19 @@ def capture(
213213
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
214214
else:
215215
for feature, variant in feature_variants.items():
216-
msg["properties"]["$feature/{}".format(feature)] = variant
216+
msg["properties"][f"$feature/{feature}"] = variant
217217
msg["properties"]["$active_feature_flags"] = list(feature_variants.keys())
218+
elif self.feature_flags:
219+
# Local evaluation is enabled, flags are loaded, so try and get all flags we can without going to the server
220+
feature_variants = self.get_all_flags(
221+
distinct_id, groups=(groups or {}), disable_geoip=disable_geoip, only_evaluate_locally=True
222+
)
223+
for feature, variant in feature_variants.items():
224+
msg["properties"][f"$feature/{feature}"] = variant
225+
226+
active_feature_flags = [key for (key, value) in feature_variants.items() if value is not False]
227+
if active_feature_flags:
228+
msg["properties"]["$active_feature_flags"] = active_feature_flags
218229

219230
return self._enqueue(msg, disable_geoip)
220231

@@ -538,6 +549,10 @@ def get_feature_flag(
538549
if self.disabled:
539550
return None
540551

552+
person_properties, group_properties = self._add_local_person_and_group_properties(
553+
distinct_id, groups, person_properties, group_properties
554+
)
555+
541556
if self.feature_flags is None and self.personal_api_key:
542557
self.load_feature_flags()
543558
response = None
@@ -685,6 +700,10 @@ def get_all_flags_and_payloads(
685700
if self.disabled:
686701
return {"featureFlags": None, "featureFlagPayloads": None}
687702

703+
person_properties, group_properties = self._add_local_person_and_group_properties(
704+
distinct_id, groups, person_properties, group_properties
705+
)
706+
688707
flags, payloads, fallback_to_decide = self._get_all_flags_and_payloads_locally(
689708
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
690709
)
@@ -743,6 +762,19 @@ def _get_all_flags_and_payloads_locally(self, distinct_id, *, groups={}, person_
743762
def feature_flag_definitions(self):
744763
return self.feature_flags
745764

765+
def _add_local_person_and_group_properties(self, distinct_id, groups, person_properties, group_properties):
766+
all_person_properties = {"$current_distinct_id": distinct_id, **(person_properties or {})}
767+
768+
all_group_properties = {}
769+
if groups:
770+
for group_name in groups:
771+
all_group_properties[group_name] = {
772+
"$group_key": groups[group_name],
773+
**(group_properties.get(group_name) or {}),
774+
}
775+
776+
return all_person_properties, all_group_properties
777+
746778

747779
def require(name, field, data_type):
748780
"""Require that the named `field` has the right `data_type`"""

posthog/test/test_client.py

Lines changed: 174 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,110 @@ def test_basic_capture_with_feature_flags(self, patch_decide):
105105

106106
self.assertEqual(patch_decide.call_count, 1)
107107

108+
@mock.patch("posthog.client.decide")
109+
def test_basic_capture_with_locally_evaluated_feature_flags(self, patch_decide):
110+
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
111+
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY)
112+
113+
multivariate_flag = {
114+
"id": 1,
115+
"name": "Beta Feature",
116+
"key": "beta-feature-local",
117+
"is_simple_flag": False,
118+
"active": True,
119+
"rollout_percentage": 100,
120+
"filters": {
121+
"groups": [
122+
{
123+
"properties": [
124+
{"key": "email", "type": "person", "value": "test@posthog.com", "operator": "exact"}
125+
],
126+
"rollout_percentage": 100,
127+
},
128+
{
129+
"rollout_percentage": 50,
130+
},
131+
],
132+
"multivariate": {
133+
"variants": [
134+
{"key": "first-variant", "name": "First Variant", "rollout_percentage": 50},
135+
{"key": "second-variant", "name": "Second Variant", "rollout_percentage": 25},
136+
{"key": "third-variant", "name": "Third Variant", "rollout_percentage": 25},
137+
]
138+
},
139+
"payloads": {"first-variant": "some-payload", "third-variant": {"a": "json"}},
140+
},
141+
}
142+
basic_flag = {
143+
"id": 1,
144+
"name": "Beta Feature",
145+
"key": "person-flag",
146+
"is_simple_flag": True,
147+
"active": True,
148+
"filters": {
149+
"groups": [
150+
{
151+
"properties": [
152+
{
153+
"key": "region",
154+
"operator": "exact",
155+
"value": ["USA"],
156+
"type": "person",
157+
}
158+
],
159+
"rollout_percentage": 100,
160+
}
161+
],
162+
"payloads": {"true": 300},
163+
},
164+
}
165+
false_flag = {
166+
"id": 1,
167+
"name": "Beta Feature",
168+
"key": "false-flag",
169+
"is_simple_flag": True,
170+
"active": True,
171+
"filters": {
172+
"groups": [
173+
{
174+
"properties": [],
175+
"rollout_percentage": 0,
176+
}
177+
],
178+
"payloads": {"true": 300},
179+
},
180+
}
181+
client.feature_flags = [multivariate_flag, basic_flag, false_flag]
182+
183+
success, msg = client.capture("distinct_id", "python test event")
184+
client.flush()
185+
self.assertTrue(success)
186+
self.assertFalse(self.failed)
187+
188+
self.assertEqual(msg["event"], "python test event")
189+
self.assertTrue(isinstance(msg["timestamp"], str))
190+
self.assertIsNone(msg.get("uuid"))
191+
self.assertEqual(msg["distinct_id"], "distinct_id")
192+
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
193+
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
194+
self.assertEqual(msg["properties"]["$feature/beta-feature-local"], "third-variant")
195+
self.assertEqual(msg["properties"]["$feature/false-flag"], False)
196+
self.assertEqual(msg["properties"]["$active_feature_flags"], ["beta-feature-local"])
197+
assert "$feature/beta-feature" not in msg["properties"]
198+
199+
self.assertEqual(patch_decide.call_count, 0)
200+
201+
# test that flags are not evaluated without local evaluation
202+
client.feature_flags = []
203+
success, msg = client.capture("distinct_id", "python test event")
204+
client.flush()
205+
self.assertTrue(success)
206+
self.assertFalse(self.failed)
207+
assert "$feature/beta-feature" not in msg["properties"]
208+
assert "$feature/beta-feature-local" not in msg["properties"]
209+
assert "$feature/false-flag" not in msg["properties"]
210+
assert "$active_feature_flags" not in msg["properties"]
211+
108212
@mock.patch("posthog.client.decide")
109213
def test_get_active_feature_flags(self, patch_decide):
110214
patch_decide.return_value = {
@@ -620,7 +724,7 @@ def test_disable_geoip_default_on_decide(self, patch_decide):
620724
timeout=10,
621725
distinct_id="some_id",
622726
groups={},
623-
person_properties={},
727+
person_properties={"$current_distinct_id": "some_id"},
624728
group_properties={},
625729
disable_geoip=True,
626730
)
@@ -632,7 +736,7 @@ def test_disable_geoip_default_on_decide(self, patch_decide):
632736
timeout=10,
633737
distinct_id="feature_enabled_distinct_id",
634738
groups={},
635-
person_properties={},
739+
person_properties={"$current_distinct_id": "feature_enabled_distinct_id"},
636740
group_properties={},
637741
disable_geoip=True,
638742
)
@@ -644,7 +748,7 @@ def test_disable_geoip_default_on_decide(self, patch_decide):
644748
timeout=10,
645749
distinct_id="all_flags_payloads_id",
646750
groups={},
647-
person_properties={},
751+
person_properties={"$current_distinct_id": "all_flags_payloads_id"},
648752
group_properties={},
649753
disable_geoip=False,
650754
)
@@ -660,3 +764,70 @@ def raise_effect():
660764
client.feature_flags = [{"key": "example", "is_simple_flag": False}]
661765

662766
self.assertFalse(client.feature_enabled("example", "distinct_id"))
767+
768+
@mock.patch("posthog.client.decide")
769+
def test_default_properties_get_added_properly(self, patch_decide):
770+
patch_decide.return_value = {
771+
"featureFlags": {"beta-feature": "random-variant", "alpha-feature": True, "off-feature": False}
772+
}
773+
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=False)
774+
client.get_feature_flag(
775+
"random_key",
776+
"some_id",
777+
groups={"company": "id:5", "instance": "app.posthog.com"},
778+
person_properties={"x1": "y1"},
779+
group_properties={"company": {"x": "y"}},
780+
)
781+
patch_decide.assert_called_with(
782+
"random_key",
783+
None,
784+
timeout=10,
785+
distinct_id="some_id",
786+
groups={"company": "id:5", "instance": "app.posthog.com"},
787+
person_properties={"$current_distinct_id": "some_id", "x1": "y1"},
788+
group_properties={
789+
"company": {"$group_key": "id:5", "x": "y"},
790+
"instance": {"$group_key": "app.posthog.com"},
791+
},
792+
disable_geoip=False,
793+
)
794+
795+
patch_decide.reset_mock()
796+
client.get_feature_flag(
797+
"random_key",
798+
"some_id",
799+
groups={"company": "id:5", "instance": "app.posthog.com"},
800+
person_properties={"$current_distinct_id": "override"},
801+
group_properties={
802+
"company": {
803+
"$group_key": "group_override",
804+
}
805+
},
806+
)
807+
patch_decide.assert_called_with(
808+
"random_key",
809+
None,
810+
timeout=10,
811+
distinct_id="some_id",
812+
groups={"company": "id:5", "instance": "app.posthog.com"},
813+
person_properties={"$current_distinct_id": "override"},
814+
group_properties={
815+
"company": {"$group_key": "group_override"},
816+
"instance": {"$group_key": "app.posthog.com"},
817+
},
818+
disable_geoip=False,
819+
)
820+
821+
patch_decide.reset_mock()
822+
# test nones
823+
client.get_all_flags_and_payloads("some_id", groups={}, person_properties=None, group_properties=None)
824+
patch_decide.assert_called_with(
825+
"random_key",
826+
None,
827+
timeout=10,
828+
distinct_id="some_id",
829+
groups={},
830+
person_properties={"$current_distinct_id": "some_id"},
831+
group_properties={},
832+
disable_geoip=False,
833+
)

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "3.2.0"
1+
VERSION = "3.3.0"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

0 commit comments

Comments
 (0)