Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.3.0 - 2024-01-09

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.

## 3.2.0 - 2024-01-09

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.
Expand Down
34 changes: 33 additions & 1 deletion posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,19 @@ def capture(
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
else:
for feature, variant in feature_variants.items():
msg["properties"]["$feature/{}".format(feature)] = variant
msg["properties"][f"$feature/{feature}"] = variant
msg["properties"]["$active_feature_flags"] = list(feature_variants.keys())
elif self.feature_flags:
# Local evaluation is enabled, flags are loaded, so try and get all flags we can without going to the server
feature_variants = self.get_all_flags(
distinct_id, groups=(groups or {}), disable_geoip=disable_geoip, only_evaluate_locally=True
)
for feature, variant in feature_variants.items():
msg["properties"][f"$feature/{feature}"] = variant

active_feature_flags = [key for (key, value) in feature_variants.items() if value is not False]
if active_feature_flags:
msg["properties"]["$active_feature_flags"] = active_feature_flags

return self._enqueue(msg, disable_geoip)

Expand Down Expand Up @@ -538,6 +549,10 @@ def get_feature_flag(
if self.disabled:
return None

person_properties, group_properties = self._add_local_person_and_group_properties(
distinct_id, groups, person_properties, group_properties
)

if self.feature_flags is None and self.personal_api_key:
self.load_feature_flags()
response = None
Expand Down Expand Up @@ -685,6 +700,10 @@ def get_all_flags_and_payloads(
if self.disabled:
return {"featureFlags": None, "featureFlagPayloads": None}

person_properties, group_properties = self._add_local_person_and_group_properties(
distinct_id, groups, person_properties, group_properties
)

flags, payloads, fallback_to_decide = self._get_all_flags_and_payloads_locally(
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
)
Expand Down Expand Up @@ -743,6 +762,19 @@ def _get_all_flags_and_payloads_locally(self, distinct_id, *, groups={}, person_
def feature_flag_definitions(self):
return self.feature_flags

def _add_local_person_and_group_properties(self, distinct_id, groups, person_properties, group_properties):
all_person_properties = {"$current_distinct_id": distinct_id, **(person_properties or {})}

all_group_properties = {}
if groups:
for group_name in groups:
all_group_properties[group_name] = {
"$group_key": groups[group_name],
**(group_properties.get(group_name) or {}),
}

return all_person_properties, all_group_properties


def require(name, field, data_type):
"""Require that the named `field` has the right `data_type`"""
Expand Down
177 changes: 174 additions & 3 deletions posthog/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,110 @@ def test_basic_capture_with_feature_flags(self, patch_decide):

self.assertEqual(patch_decide.call_count, 1)

@mock.patch("posthog.client.decide")
def test_basic_capture_with_locally_evaluated_feature_flags(self, patch_decide):
patch_decide.return_value = {"featureFlags": {"beta-feature": "random-variant"}}
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, personal_api_key=FAKE_TEST_API_KEY)

multivariate_flag = {
"id": 1,
"name": "Beta Feature",
"key": "beta-feature-local",
"is_simple_flag": False,
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [
{"key": "email", "type": "person", "value": "test@posthog.com", "operator": "exact"}
],
"rollout_percentage": 100,
},
{
"rollout_percentage": 50,
},
],
"multivariate": {
"variants": [
{"key": "first-variant", "name": "First Variant", "rollout_percentage": 50},
{"key": "second-variant", "name": "Second Variant", "rollout_percentage": 25},
{"key": "third-variant", "name": "Third Variant", "rollout_percentage": 25},
]
},
"payloads": {"first-variant": "some-payload", "third-variant": {"a": "json"}},
},
}
basic_flag = {
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"is_simple_flag": True,
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
}
],
"rollout_percentage": 100,
}
],
"payloads": {"true": 300},
},
}
false_flag = {
"id": 1,
"name": "Beta Feature",
"key": "false-flag",
"is_simple_flag": True,
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
],
"payloads": {"true": 300},
},
}
client.feature_flags = [multivariate_flag, basic_flag, false_flag]

success, msg = client.capture("distinct_id", "python test event")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)

self.assertEqual(msg["event"], "python test event")
self.assertTrue(isinstance(msg["timestamp"], str))
self.assertIsNone(msg.get("uuid"))
self.assertEqual(msg["distinct_id"], "distinct_id")
self.assertEqual(msg["properties"]["$lib"], "posthog-python")
self.assertEqual(msg["properties"]["$lib_version"], VERSION)
self.assertEqual(msg["properties"]["$feature/beta-feature-local"], "third-variant")
self.assertEqual(msg["properties"]["$feature/false-flag"], False)
self.assertEqual(msg["properties"]["$active_feature_flags"], ["beta-feature-local"])
assert "$feature/beta-feature" not in msg["properties"]

self.assertEqual(patch_decide.call_count, 0)

# test that flags are not evaluated without local evaluation
client.feature_flags = []
success, msg = client.capture("distinct_id", "python test event")
client.flush()
self.assertTrue(success)
self.assertFalse(self.failed)
assert "$feature/beta-feature" not in msg["properties"]
assert "$feature/beta-feature-local" not in msg["properties"]
assert "$feature/false-flag" not in msg["properties"]
assert "$active_feature_flags" not in msg["properties"]

@mock.patch("posthog.client.decide")
def test_get_active_feature_flags(self, patch_decide):
patch_decide.return_value = {
Expand Down Expand Up @@ -620,7 +724,7 @@ def test_disable_geoip_default_on_decide(self, patch_decide):
timeout=10,
distinct_id="some_id",
groups={},
person_properties={},
person_properties={"$current_distinct_id": "some_id"},
group_properties={},
disable_geoip=True,
)
Expand All @@ -632,7 +736,7 @@ def test_disable_geoip_default_on_decide(self, patch_decide):
timeout=10,
distinct_id="feature_enabled_distinct_id",
groups={},
person_properties={},
person_properties={"$current_distinct_id": "feature_enabled_distinct_id"},
group_properties={},
disable_geoip=True,
)
Expand All @@ -644,7 +748,7 @@ def test_disable_geoip_default_on_decide(self, patch_decide):
timeout=10,
distinct_id="all_flags_payloads_id",
groups={},
person_properties={},
person_properties={"$current_distinct_id": "all_flags_payloads_id"},
group_properties={},
disable_geoip=False,
)
Expand All @@ -660,3 +764,70 @@ def raise_effect():
client.feature_flags = [{"key": "example", "is_simple_flag": False}]

self.assertFalse(client.feature_enabled("example", "distinct_id"))

@mock.patch("posthog.client.decide")
def test_default_properties_get_added_properly(self, patch_decide):
patch_decide.return_value = {
"featureFlags": {"beta-feature": "random-variant", "alpha-feature": True, "off-feature": False}
}
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, disable_geoip=False)
client.get_feature_flag(
"random_key",
"some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"x1": "y1"},
group_properties={"company": {"x": "y"}},
)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
distinct_id="some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"$current_distinct_id": "some_id", "x1": "y1"},
group_properties={
"company": {"$group_key": "id:5", "x": "y"},
"instance": {"$group_key": "app.posthog.com"},
},
disable_geoip=False,
)

patch_decide.reset_mock()
client.get_feature_flag(
"random_key",
"some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"$current_distinct_id": "override"},
group_properties={
"company": {
"$group_key": "group_override",
}
},
)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
distinct_id="some_id",
groups={"company": "id:5", "instance": "app.posthog.com"},
person_properties={"$current_distinct_id": "override"},
group_properties={
"company": {"$group_key": "group_override"},
"instance": {"$group_key": "app.posthog.com"},
},
disable_geoip=False,
)

patch_decide.reset_mock()
# test nones
client.get_all_flags_and_payloads("some_id", groups={}, person_properties=None, group_properties=None)
patch_decide.assert_called_with(
"random_key",
None,
timeout=10,
distinct_id="some_id",
groups={},
person_properties={"$current_distinct_id": "some_id"},
group_properties={},
disable_geoip=False,
)
2 changes: 1 addition & 1 deletion posthog/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = "3.2.0"
VERSION = "3.3.0"

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