Skip to content

Commit addd2e3

Browse files
neilkakkarUtku Zihnioglu
andauthored
Have an option to send feature variants with the .capture(...) calls (#65)
Co-authored-by: Utku Zihnioglu <[email protected]>
1 parent 9d2fa72 commit addd2e3

File tree

4 files changed

+129
-19
lines changed

4 files changed

+129
-19
lines changed

example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111

1212
# Where you host PostHog, with no trailing /.
1313
# You can remove this line if you're using posthog.com
14-
posthog.host = "http://127.0.0.1:8000"
14+
posthog.host = "http://localhost:8000"
1515

1616
# Capture an event
17-
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"})
17+
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"}, send_feature_flags=True)
1818

1919
print(posthog.feature_enabled("beta-feature", "distinct_id"))
2020
print(posthog.feature_enabled("beta-feature", "distinct_id", groups={"company": "id:5"}))

posthog/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def capture(
2727
timestamp=None, # type: Optional[datetime.datetime]
2828
uuid=None, # type: Optional[str]
2929
groups=None, # type: Optional[Dict]
30+
send_feature_flags=False,
3031
):
3132
# type: (...) -> None
3233
"""
@@ -58,6 +59,7 @@ def capture(
5859
timestamp=timestamp,
5960
uuid=uuid,
6061
groups=groups,
62+
send_feature_flags=send_feature_flags,
6163
)
6264

6365

posthog/client.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,33 @@ def identify(self, distinct_id=None, properties=None, context=None, timestamp=No
120120

121121
return self._enqueue(msg)
122122

123+
def get_feature_variants(self, distinct_id, groups=None):
124+
assert self.personal_api_key, "You have to specify a personal_api_key to use feature flags."
125+
require("distinct_id", distinct_id, ID_TYPES)
126+
127+
if groups:
128+
require("groups", groups, dict)
129+
else:
130+
groups = {}
131+
132+
request_data = {
133+
"distinct_id": distinct_id,
134+
"personal_api_key": self.personal_api_key,
135+
"groups": groups,
136+
}
137+
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
138+
return resp_data["featureFlags"]
139+
123140
def capture(
124-
self, distinct_id=None, event=None, properties=None, context=None, timestamp=None, uuid=None, groups=None
141+
self,
142+
distinct_id=None,
143+
event=None,
144+
properties=None,
145+
context=None,
146+
timestamp=None,
147+
uuid=None,
148+
groups=None,
149+
send_feature_flags=False,
125150
):
126151
properties = properties or {}
127152
context = context or {}
@@ -142,6 +167,16 @@ def capture(
142167
require("groups", groups, dict)
143168
msg["properties"]["$groups"] = groups
144169

170+
if send_feature_flags:
171+
try:
172+
feature_variants = self.get_feature_variants(distinct_id, groups)
173+
except Exception as e:
174+
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
175+
else:
176+
for feature, variant in feature_variants.items():
177+
msg["properties"]["$feature/{}".format(feature)] = variant
178+
msg["properties"]["$active_feature_flags"] = list(feature_variants.keys())
179+
145180
return self._enqueue(msg)
146181

147182
def set(self, distinct_id=None, properties=None, context=None, timestamp=None, uuid=None):
@@ -364,23 +399,20 @@ def feature_enabled(self, key, distinct_id, default=False, *, groups={}):
364399
if flag["key"] == key:
365400
feature_flag = flag
366401
if feature_flag.get("is_simple_flag"):
367-
response = _hash(key, distinct_id) <= (feature_flag.get("rollout_percentage", 100) / 100)
402+
rollout_percentage = (
403+
feature_flag.get("rollout_percentage")
404+
if feature_flag.get("rollout_percentage") is not None
405+
else 100
406+
)
407+
response = _hash(key, distinct_id) <= (rollout_percentage / 100)
368408
if response == None:
369409
try:
370-
request_data = {
371-
"distinct_id": distinct_id,
372-
"personal_api_key": self.personal_api_key,
373-
"groups": groups,
374-
}
375-
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
410+
feature_flags = self.get_feature_variants(distinct_id, groups=groups)
376411
except Exception as e:
412+
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
377413
response = default
378-
self.log.warning(
379-
"[FEATURE FLAGS] Unable to get data for flag %s, because of the following error:" % key
380-
)
381-
self.log.warning(e)
382414
else:
383-
response = resp_data["featureFlags"].get(key, default)
415+
response = feature_flags.get(key, default)
384416
self.capture(distinct_id, "$feature_flag_called", {"$feature_flag": key, "$feature_flag_response": response})
385417
return response
386418

posthog/test/test_client.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import time
22
import unittest
33
from datetime import date, datetime
4+
from unittest.mock import MagicMock
45
from uuid import uuid4
56

67
import mock
@@ -74,6 +75,71 @@ def test_basic_capture_with_project_api_key(self):
7475
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
7576
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
7677

78+
@mock.patch("posthog.client.decide")
79+
def test_basic_capture_with_feature_flags(self, patch_decide):
80+
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
81+
82+
client = Client(TEST_API_KEY, on_error=self.set_fail, personal_api_key=TEST_API_KEY)
83+
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=True)
84+
client.flush()
85+
self.assertTrue(success)
86+
self.assertFalse(self.failed)
87+
88+
self.assertEqual(msg["event"], "python test event")
89+
self.assertTrue(isinstance(msg["timestamp"], str))
90+
self.assertIsNone(msg.get("uuid"))
91+
self.assertEqual(msg["distinct_id"], "distinct_id")
92+
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
93+
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
94+
self.assertEqual(msg["properties"]["$feature/beta-feature"], "random-variant")
95+
self.assertEqual(msg["properties"]["$active_feature_flags"], ["beta-feature"])
96+
97+
self.assertEqual(patch_decide.call_count, 1)
98+
99+
@mock.patch("posthog.client.decide")
100+
def test_basic_capture_with_feature_flags_switched_off_doesnt_send_them(self, patch_decide):
101+
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
102+
103+
client = Client(TEST_API_KEY, on_error=self.set_fail, personal_api_key=TEST_API_KEY)
104+
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=False)
105+
client.flush()
106+
self.assertTrue(success)
107+
self.assertFalse(self.failed)
108+
109+
self.assertEqual(msg["event"], "python test event")
110+
self.assertTrue(isinstance(msg["timestamp"], str))
111+
self.assertIsNone(msg.get("uuid"))
112+
self.assertEqual(msg["distinct_id"], "distinct_id")
113+
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
114+
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
115+
self.assertTrue("$feature/beta-feature" not in msg["properties"])
116+
self.assertTrue("$active_feature_flags" not in msg["properties"])
117+
118+
self.assertEqual(patch_decide.call_count, 0)
119+
120+
@mock.patch("posthog.client.decide")
121+
def test_basic_capture_with_feature_flags_without_api_key(self, patch_decide):
122+
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
123+
124+
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
125+
client.log = MagicMock()
126+
success, msg = client.capture("distinct_id", "python test event", send_feature_flags=True)
127+
client.flush()
128+
self.assertTrue(success)
129+
self.assertFalse(self.failed)
130+
131+
self.assertEqual(msg["event"], "python test event")
132+
self.assertTrue(isinstance(msg["timestamp"], str))
133+
self.assertIsNone(msg.get("uuid"))
134+
self.assertEqual(msg["distinct_id"], "distinct_id")
135+
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
136+
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
137+
138+
self.assertEqual(client.log.exception.call_count, 1)
139+
client.log.exception.assert_called_with(
140+
"[FEATURE FLAGS] Unable to get feature variants: You have to specify a personal_api_key to use feature flags."
141+
)
142+
77143
def test_stringifies_distinct_id(self):
78144
# A large number that loses precision in node:
79145
# node -e "console.log(157963456373623802 + 1)" > 157963456373623800
@@ -424,6 +490,16 @@ def test_feature_enabled_simple_is_false(self, patch_get, patch_decide):
424490
self.assertFalse(client.feature_enabled("beta-feature", "distinct_id"))
425491
self.assertEqual(patch_decide.call_count, 0)
426492

493+
@mock.patch("posthog.client.decide")
494+
@mock.patch("posthog.client.get")
495+
def test_feature_enabled_simple_is_true_when_rollout_is_undefined(self, patch_get, patch_decide):
496+
client = Client(TEST_API_KEY)
497+
client.feature_flags = [
498+
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": True, "rollout_percentage": None}
499+
]
500+
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
501+
self.assertEqual(patch_decide.call_count, 0)
502+
427503
@mock.patch("posthog.client.get")
428504
def test_feature_enabled_simple_with_project_api_key(self, patch_get):
429505
client = Client(project_api_key=TEST_API_KEY, on_error=self.set_fail)
@@ -435,7 +511,7 @@ def test_feature_enabled_simple_with_project_api_key(self, patch_get):
435511
@mock.patch("posthog.client.decide")
436512
def test_feature_enabled_request(self, patch_decide):
437513
patch_decide.return_value = {"featureFlags": {"beta-feature": True}}
438-
client = Client(TEST_API_KEY)
514+
client = Client(TEST_API_KEY, personal_api_key="test")
439515
client.feature_flags = [
440516
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": False, "rollout_percentage": 100}
441517
]
@@ -444,7 +520,7 @@ def test_feature_enabled_request(self, patch_decide):
444520
@mock.patch("posthog.client.decide")
445521
def test_feature_enabled_request_multi_variate(self, patch_decide):
446522
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
447-
client = Client(TEST_API_KEY)
523+
client = Client(TEST_API_KEY, personal_api_key="test")
448524
client.feature_flags = [
449525
{"id": 1, "name": "Beta Feature", "key": "beta-feature", "is_simple_flag": False, "rollout_percentage": 100}
450526
]
@@ -468,7 +544,7 @@ def test_feature_enabled_simple_with_none_rollout_percentage(self, patch_get):
468544
@mock.patch("posthog.client.decide")
469545
def test_feature_enabled_doesnt_exist(self, patch_decide, patch_poll):
470546
patch_decide.return_value = {"featureFlags": {}}
471-
client = Client(TEST_API_KEY, personal_api_key="test")
547+
client = Client(TEST_API_KEY)
472548
client.feature_flags = []
473549

474550
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
@@ -477,7 +553,7 @@ def test_feature_enabled_doesnt_exist(self, patch_decide, patch_poll):
477553
@mock.patch("posthog.client.Poller")
478554
@mock.patch("posthog.client.decide")
479555
def test_personal_api_key_doesnt_exist(self, patch_decide, patch_poll):
480-
client = Client(TEST_API_KEY)
556+
client = Client(TEST_API_KEY, personal_api_key="test")
481557
client.feature_flags = []
482558

483559
patch_decide.return_value = {"featureFlags": {"feature-flag": True}}

0 commit comments

Comments
 (0)