Skip to content

Commit

Permalink
feat(flags): Add local props and flags to all calls (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
neilkakkar authored Jan 9, 2024
1 parent a2c73d0 commit e60d52c
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 5 deletions.
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

0 comments on commit e60d52c

Please sign in to comment.