Skip to content
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

feat(flags): Add local props and flags to all calls #106

Merged
merged 9 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
33 changes: 32 additions & 1 deletion posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,18 @@ 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
msg["properties"]["$active_feature_flags"] = [
key for (key, value) in feature_variants.items() if value is not False
]

return self._enqueue(msg, disable_geoip)

Expand Down Expand Up @@ -538,6 +548,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 +699,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 +761,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": "[email protected]", "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,
)
Loading