Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae8817b611 |
@@ -1,3 +1,9 @@
|
||||
## 2.2.0 - 2022-11-14
|
||||
|
||||
Changes:
|
||||
|
||||
1. Add support for feature flag variant overrides with local evaluation
|
||||
|
||||
## 2.1.2 - 2022-09-15
|
||||
|
||||
Changes:
|
||||
|
||||
@@ -39,7 +39,7 @@ Then navigate to `http://127.0.0.1:8080/sentry-debug/` and you should get an eve
|
||||
|
||||
### Releasing Versions
|
||||
|
||||
Updated are released using GitHub Actions: after bumping `version.py` in `master`, go to [our release workflow's page](https://github.com/PostHog/posthog-python/actions/workflows/release.yaml) and dispatch it manually, using workflow from `master`.
|
||||
Updated are released using GitHub Actions: after bumping `version.py` in `master` and adding to `CHANGELOG.md`, go to [our release workflow's page](https://github.com/PostHog/posthog-python/actions/workflows/release.yaml) and dispatch it manually, using workflow from `master`.
|
||||
|
||||
## Questions?
|
||||
|
||||
|
||||
+18
-2
@@ -8,14 +8,30 @@ import posthog
|
||||
posthog.debug = True
|
||||
|
||||
# You can find this key on the /setup page in PostHog
|
||||
posthog.project_api_key = ""
|
||||
posthog.personal_api_key = ""
|
||||
posthog.project_api_key = "phc_gtWmTq3Pgl06u4sZY3TRcoQfp42yfuXHKoe8ZVSR6Kh"
|
||||
posthog.personal_api_key = "phx_fiRCOQkTA3o2ePSdLrFDAILLHjMu2Mv52vUi8MNruIm"
|
||||
|
||||
# Where you host PostHog, with no trailing /.
|
||||
# You can remove this line if you're using posthog.com
|
||||
posthog.host = "http://localhost:8000"
|
||||
posthog.poll_interval = 10
|
||||
|
||||
print(
|
||||
posthog.feature_enabled(
|
||||
"person-on-events-enabled",
|
||||
"12345",
|
||||
groups={"organization": str("0182ee91-8ef7-0000-4cb9-fedc5f00926a")},
|
||||
group_properties={
|
||||
"organization": {
|
||||
"id": "0182ee91-8ef7-0000-4cb9-fedc5f00926a",
|
||||
"created_at": "2022-06-30 11:44:52.984121+00:00",
|
||||
}
|
||||
},
|
||||
only_evaluate_locally=True,
|
||||
)
|
||||
)
|
||||
|
||||
exit()
|
||||
|
||||
# Capture an event
|
||||
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"}, send_feature_flags=True)
|
||||
|
||||
@@ -46,12 +46,26 @@ def match_feature_flag_properties(flag, distinct_id, properties):
|
||||
flag_conditions = (flag.get("filters") or {}).get("groups") or []
|
||||
is_inconclusive = False
|
||||
|
||||
for condition in flag_conditions:
|
||||
# Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
|
||||
# evaluated first, and the variant override is applied to the first matching condition.
|
||||
sorted_flag_conditions = sorted(
|
||||
flag_conditions,
|
||||
key=lambda condition: 0 if condition.get("variant") else 1,
|
||||
)
|
||||
|
||||
for condition in sorted_flag_conditions:
|
||||
try:
|
||||
# if any one condition resolves to True, we can shortcircuit and return
|
||||
# the matching variant
|
||||
if is_condition_match(flag, distinct_id, condition, properties):
|
||||
return get_matching_variant(flag, distinct_id) or True
|
||||
variant_override = condition.get("variant")
|
||||
# Some filters can be explicitly set to null, which require accessing variants like so
|
||||
flag_variants = ((flag.get("filters") or {}).get("multivariate") or {}).get("variants") or []
|
||||
if variant_override and variant_override in [variant["key"] for variant in flag_variants]:
|
||||
variant = variant_override
|
||||
else:
|
||||
variant = get_matching_variant(flag, distinct_id)
|
||||
return variant or True
|
||||
except InconclusiveMatchError:
|
||||
is_inconclusive = True
|
||||
|
||||
|
||||
@@ -969,6 +969,186 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_feature_flag_with_variant_overrides(self, patch_decide):
|
||||
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
client.feature_flags = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"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,
|
||||
"variant": "second-variant",
|
||||
},
|
||||
{"rollout_percentage": 50, "variant": "first-variant"},
|
||||
],
|
||||
"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},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
client.get_feature_flag("beta-feature", "test_id", person_properties={"email": "test@posthog.com"}),
|
||||
"second-variant",
|
||||
)
|
||||
self.assertEqual(client.get_feature_flag("beta-feature", "example_id"), "first-variant")
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_flag_with_clashing_variant_overrides(self, patch_decide):
|
||||
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
client.feature_flags = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"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,
|
||||
"variant": "second-variant",
|
||||
},
|
||||
# since second-variant comes first in the list, it will be the one that gets picked
|
||||
{
|
||||
"properties": [
|
||||
{"key": "email", "type": "person", "value": "test@posthog.com", "operator": "exact"}
|
||||
],
|
||||
"rollout_percentage": 100,
|
||||
"variant": "first-variant",
|
||||
},
|
||||
{"rollout_percentage": 50, "variant": "first-variant"},
|
||||
],
|
||||
"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},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
client.get_feature_flag("beta-feature", "test_id", person_properties={"email": "test@posthog.com"}),
|
||||
"second-variant",
|
||||
)
|
||||
self.assertEqual(
|
||||
client.get_feature_flag("beta-feature", "example_id", person_properties={"email": "test@posthog.com"}),
|
||||
"second-variant",
|
||||
)
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_flag_with_invalid_variant_overrides(self, patch_decide):
|
||||
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
client.feature_flags = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"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,
|
||||
"variant": "second???",
|
||||
},
|
||||
{"rollout_percentage": 50, "variant": "first??"},
|
||||
],
|
||||
"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},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
client.get_feature_flag("beta-feature", "test_id", person_properties={"email": "test@posthog.com"}),
|
||||
"third-variant",
|
||||
)
|
||||
self.assertEqual(client.get_feature_flag("beta-feature", "example_id"), "second-variant")
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_flag_with_multiple_variant_overrides(self, patch_decide):
|
||||
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1"}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
||||
client.feature_flags = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"rollout_percentage": 100,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"rollout_percentage": 100,
|
||||
# The override applies even if the first condition matches all and gives everyone their default group
|
||||
},
|
||||
{
|
||||
"properties": [
|
||||
{"key": "email", "type": "person", "value": "test@posthog.com", "operator": "exact"}
|
||||
],
|
||||
"rollout_percentage": 100,
|
||||
"variant": "second-variant",
|
||||
},
|
||||
{"rollout_percentage": 50, "variant": "third-variant"},
|
||||
],
|
||||
"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},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
client.get_feature_flag("beta-feature", "test_id", person_properties={"email": "test@posthog.com"}),
|
||||
"second-variant",
|
||||
)
|
||||
self.assertEqual(client.get_feature_flag("beta-feature", "example_id"), "third-variant")
|
||||
self.assertEqual(client.get_feature_flag("beta-feature", "another_id"), "second-variant")
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
|
||||
class TestMatchProperties(unittest.TestCase):
|
||||
def property(self, key, value, operator=None):
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
VERSION = "2.1.2"
|
||||
VERSION = "2.2.0"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(VERSION, end="")
|
||||
|
||||
Reference in New Issue
Block a user