Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d7bf3e0b | ||
|
|
d7be253ef8 | ||
|
|
592c0f362e | ||
|
|
c7fc5a83b4 | ||
|
|
ae8817b611 | ||
|
|
acad2b142e | ||
|
|
cb62570e69 | ||
|
|
33645ecd3c | ||
|
|
81debcef27 |
+29
-1
@@ -1,3 +1,30 @@
|
||||
## 2.3.0 - 2023-01-31
|
||||
|
||||
1. Add support for returning payloads of matched feature flags
|
||||
|
||||
## 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:
|
||||
|
||||
1. Fixes issues with date comparison.
|
||||
## 2.1.1 - 2022-09-14
|
||||
|
||||
Changes:
|
||||
|
||||
1. Feature flags local evaluation now supports date property filters as well. Accepts both strings and datetime objects.
|
||||
|
||||
## 2.1.0 - 2022-08-11
|
||||
|
||||
Changes:
|
||||
|
||||
1. Feature flag defaults have been removed
|
||||
2. Setup logging only when debug mode is enabled.
|
||||
## 2.0.1 - 2022-08-04
|
||||
|
||||
- Make poll_interval configurable
|
||||
@@ -9,7 +36,8 @@
|
||||
Breaking changes:
|
||||
|
||||
1. The minimum version requirement for PostHog servers is now 1.38. If you're using PostHog Cloud, you satisfy this requirement automatically.
|
||||
2. Feature flag defaults apply only when there's an error fetching feature flag results. Earlier, if the default was set to `True`, even if a flag resolved to `False`, the default would override this.
|
||||
2. Feature flag defaults apply only when there's an error fetching feature flag results. Earlier, if the default was set to `True`, even if a flag resolved to `False`, the default would override this.
|
||||
**Note: These are removed in 2.0.2**
|
||||
3. Feature flag remote evaluation doesn't require a personal API key.
|
||||
|
||||
New 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?
|
||||
|
||||
|
||||
+21
-2
@@ -8,14 +8,29 @@ 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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Capture an event
|
||||
posthog.capture("distinct_id", "event", {"property1": "value", "property2": "value"}, send_feature_flags=True)
|
||||
@@ -25,6 +40,10 @@ print(posthog.feature_enabled("beta-feature-groups", "distinct_id", groups={"com
|
||||
|
||||
print(posthog.feature_enabled("beta-feature", "distinct_id"))
|
||||
|
||||
# get payload
|
||||
print(posthog.get_feature_flag_payload("beta-feature", "distinct_id"))
|
||||
print(posthog.get_all_flags_and_payloads("distinct_id"))
|
||||
exit()
|
||||
# # Alias a previous distinct id with a new one
|
||||
|
||||
posthog.alias("distinct_id", "new_distinct_id")
|
||||
|
||||
+40
-4
@@ -235,7 +235,6 @@ def alias(
|
||||
def feature_enabled(
|
||||
key, # type: str,
|
||||
distinct_id, # type: str,
|
||||
default=False, # type: bool
|
||||
groups={}, # type: dict
|
||||
person_properties={}, # type: dict
|
||||
group_properties={}, # type: dict
|
||||
@@ -260,7 +259,6 @@ def feature_enabled(
|
||||
"feature_enabled",
|
||||
key=key,
|
||||
distinct_id=distinct_id,
|
||||
default=default,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
@@ -272,7 +270,6 @@ def feature_enabled(
|
||||
def get_feature_flag(
|
||||
key, # type: str,
|
||||
distinct_id, # type: str,
|
||||
default=False, # type: bool
|
||||
groups={}, # type: dict
|
||||
person_properties={}, # type: dict
|
||||
group_properties={}, # type: dict
|
||||
@@ -305,7 +302,6 @@ def get_feature_flag(
|
||||
"get_feature_flag",
|
||||
key=key,
|
||||
distinct_id=distinct_id,
|
||||
default=default,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
@@ -340,6 +336,46 @@ def get_all_flags(
|
||||
)
|
||||
|
||||
|
||||
def get_feature_flag_payload(
|
||||
key,
|
||||
distinct_id,
|
||||
match_value=None,
|
||||
groups={},
|
||||
person_properties={},
|
||||
group_properties={},
|
||||
only_evaluate_locally=False,
|
||||
send_feature_flag_events=True,
|
||||
):
|
||||
return _proxy(
|
||||
"get_feature_flag_payload",
|
||||
key=key,
|
||||
distinct_id=distinct_id,
|
||||
match_value=match_value,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
only_evaluate_locally=only_evaluate_locally,
|
||||
send_feature_flag_events=send_feature_flag_events,
|
||||
)
|
||||
|
||||
|
||||
def get_all_flags_and_payloads(
|
||||
distinct_id,
|
||||
groups={},
|
||||
person_properties={},
|
||||
group_properties={},
|
||||
only_evaluate_locally=False,
|
||||
):
|
||||
return _proxy(
|
||||
"get_all_flags_and_payloads",
|
||||
distinct_id=distinct_id,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
only_evaluate_locally=only_evaluate_locally,
|
||||
)
|
||||
|
||||
|
||||
def page(*args, **kwargs):
|
||||
"""Send a page call."""
|
||||
_proxy("page", *args, **kwargs)
|
||||
|
||||
+119
-34
@@ -23,10 +23,6 @@ except ImportError:
|
||||
ID_TYPES = (numbers.Number, string_types, UUID)
|
||||
MAX_DICT_SIZE = 50_000
|
||||
|
||||
# Ensures that debug level messages are logged when debug mode is on.
|
||||
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
|
||||
logging.basicConfig()
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Create a new PostHog client."""
|
||||
@@ -68,6 +64,7 @@ class Client(object):
|
||||
self.gzip = gzip
|
||||
self.timeout = timeout
|
||||
self.feature_flags = None
|
||||
self.feature_flags_by_key = None
|
||||
self.group_type_mapping = None
|
||||
self.poll_interval = poll_interval
|
||||
self.poller = None
|
||||
@@ -76,6 +73,9 @@ class Client(object):
|
||||
# personal_api_key: This should be a generated Personal API Key, private
|
||||
self.personal_api_key = personal_api_key
|
||||
if debug:
|
||||
# Ensures that debug level messages are logged when debug mode is on.
|
||||
# Otherwise, defaults to WARNING level. See https://docs.python.org/3/howto/logging.html#what-happens-if-no-configuration-is-provided
|
||||
logging.basicConfig()
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
else:
|
||||
self.log.setLevel(logging.WARNING)
|
||||
@@ -128,6 +128,14 @@ class Client(object):
|
||||
return self._enqueue(msg)
|
||||
|
||||
def get_feature_variants(self, distinct_id, groups=None, person_properties=None, group_properties=None):
|
||||
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties)
|
||||
return resp_data["featureFlags"]
|
||||
|
||||
def get_feature_payloads(self, distinct_id, groups=None, person_properties=None, group_properties=None):
|
||||
resp_data = self.get_decide(distinct_id, groups, person_properties, group_properties)
|
||||
return resp_data["featureFlagPayloads"]
|
||||
|
||||
def get_decide(self, distinct_id, groups=None, person_properties=None, group_properties=None):
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
|
||||
if groups:
|
||||
@@ -142,7 +150,7 @@ class Client(object):
|
||||
"group_properties": group_properties,
|
||||
}
|
||||
resp_data = decide(self.api_key, self.host, timeout=10, **request_data)
|
||||
return resp_data["featureFlags"]
|
||||
return resp_data
|
||||
|
||||
def capture(
|
||||
self,
|
||||
@@ -359,10 +367,18 @@ class Client(object):
|
||||
|
||||
def _load_feature_flags(self):
|
||||
try:
|
||||
|
||||
response = get(
|
||||
self.personal_api_key, f"/api/feature_flag/local_evaluation/?token={self.api_key}", self.host
|
||||
self.personal_api_key,
|
||||
f"/api/feature_flag/local_evaluation/?token={self.api_key}",
|
||||
self.host,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
self.feature_flags = response["flags"] or []
|
||||
self.feature_flags_by_key = {
|
||||
flag["key"]: flag for flag in self.feature_flags if flag.get("key") is not None
|
||||
}
|
||||
self.group_type_mapping = response["group_type_mapping"] or {}
|
||||
|
||||
except APIError as e:
|
||||
@@ -432,7 +448,6 @@ class Client(object):
|
||||
self,
|
||||
key,
|
||||
distinct_id,
|
||||
default=False,
|
||||
*,
|
||||
groups={},
|
||||
person_properties={},
|
||||
@@ -440,24 +455,24 @@ class Client(object):
|
||||
only_evaluate_locally=False,
|
||||
send_feature_flag_events=True,
|
||||
):
|
||||
return bool(
|
||||
self.get_feature_flag(
|
||||
key,
|
||||
distinct_id,
|
||||
default,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
only_evaluate_locally=only_evaluate_locally,
|
||||
send_feature_flag_events=send_feature_flag_events,
|
||||
)
|
||||
response = self.get_feature_flag(
|
||||
key,
|
||||
distinct_id,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
only_evaluate_locally=only_evaluate_locally,
|
||||
send_feature_flag_events=send_feature_flag_events,
|
||||
)
|
||||
|
||||
if response is None:
|
||||
return None
|
||||
return bool(response)
|
||||
|
||||
def get_feature_flag(
|
||||
self,
|
||||
key,
|
||||
distinct_id,
|
||||
default=False,
|
||||
*,
|
||||
groups={},
|
||||
person_properties={},
|
||||
@@ -500,10 +515,11 @@ class Client(object):
|
||||
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
|
||||
)
|
||||
response = feature_flags.get(key)
|
||||
if response is None:
|
||||
response = False
|
||||
self.log.debug(f"Successfully computed flag remotely: #{key} -> #{response}")
|
||||
except Exception as e:
|
||||
self.log.exception(f"[FEATURE FLAGS] Unable to get flag remotely: {e}")
|
||||
response = default
|
||||
|
||||
feature_flag_reported_key = f"{key}_{str(response)}"
|
||||
if (
|
||||
@@ -523,48 +539,117 @@ class Client(object):
|
||||
self.distinct_ids_feature_flags_reported[distinct_id].add(feature_flag_reported_key)
|
||||
return response
|
||||
|
||||
def get_feature_flag_payload(
|
||||
self,
|
||||
key,
|
||||
distinct_id,
|
||||
*,
|
||||
match_value=None,
|
||||
groups={},
|
||||
person_properties={},
|
||||
group_properties={},
|
||||
only_evaluate_locally=False,
|
||||
send_feature_flag_events=True,
|
||||
):
|
||||
if match_value is None:
|
||||
match_value = self.get_feature_flag(
|
||||
key,
|
||||
distinct_id,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
send_feature_flag_events=send_feature_flag_events,
|
||||
only_evaluate_locally=True,
|
||||
)
|
||||
|
||||
response = None
|
||||
|
||||
if match_value is not None:
|
||||
response = self._compute_payload_locally(key, match_value)
|
||||
|
||||
if response is None and not only_evaluate_locally:
|
||||
decide_payloads = self.get_feature_payloads(distinct_id, groups, person_properties, group_properties)
|
||||
response = decide_payloads.get(str(key).lower(), None)
|
||||
|
||||
return response
|
||||
|
||||
def _compute_payload_locally(self, key, match_value):
|
||||
payload = None
|
||||
|
||||
if self.feature_flags_by_key is None:
|
||||
return payload
|
||||
|
||||
flag_definition = self.feature_flags_by_key.get(key) or {}
|
||||
flag_filters = flag_definition.get("filters") or {}
|
||||
flag_payloads = flag_filters.get("payloads") or {}
|
||||
payload = flag_payloads.get(str(match_value).lower(), None)
|
||||
return payload
|
||||
|
||||
def get_all_flags(
|
||||
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
|
||||
):
|
||||
flags = self.get_all_flags_and_payloads(
|
||||
distinct_id,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
only_evaluate_locally=only_evaluate_locally,
|
||||
)
|
||||
return flags["featureFlags"]
|
||||
|
||||
def get_all_flags_and_payloads(
|
||||
self, distinct_id, *, groups={}, person_properties={}, group_properties={}, only_evaluate_locally=False
|
||||
):
|
||||
flags, payloads, fallback_to_decide = self._get_all_flags_and_payloads_locally(
|
||||
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
|
||||
)
|
||||
response = {"featureFlags": flags, "featureFlagPayloads": payloads}
|
||||
|
||||
if fallback_to_decide and not only_evaluate_locally:
|
||||
try:
|
||||
flags_and_payloads = self.get_decide(
|
||||
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
|
||||
)
|
||||
response = flags_and_payloads
|
||||
except Exception as e:
|
||||
self.log.exception(f"[FEATURE FLAGS] Unable to get feature flags and payloads: {e}")
|
||||
|
||||
return response
|
||||
|
||||
def _get_all_flags_and_payloads_locally(self, distinct_id, *, groups={}, person_properties={}, group_properties={}):
|
||||
require("distinct_id", distinct_id, ID_TYPES)
|
||||
require("groups", groups, dict)
|
||||
|
||||
if self.feature_flags == None and self.personal_api_key:
|
||||
self.load_feature_flags()
|
||||
|
||||
response = {}
|
||||
flags = {}
|
||||
payloads = {}
|
||||
fallback_to_decide = False
|
||||
|
||||
# If loading in previous line failed
|
||||
if self.feature_flags:
|
||||
for flag in self.feature_flags:
|
||||
try:
|
||||
response[flag["key"]] = self._compute_flag_locally(
|
||||
flags[flag["key"]] = self._compute_flag_locally(
|
||||
flag,
|
||||
distinct_id,
|
||||
groups=groups,
|
||||
person_properties=person_properties,
|
||||
group_properties=group_properties,
|
||||
)
|
||||
matched_payload = self._compute_payload_locally(flag["key"], flags[flag["key"]])
|
||||
if matched_payload:
|
||||
payloads[flag["key"]] = matched_payload
|
||||
except InconclusiveMatchError as e:
|
||||
# No need to log this, since it's just telling us to fall back to `/decide`
|
||||
fallback_to_decide = True
|
||||
except Exception as e:
|
||||
self.log.exception(f"[FEATURE FLAGS] Error while computing variant: {e}")
|
||||
self.log.exception(f"[FEATURE FLAGS] Error while computing variant and payload: {e}")
|
||||
fallback_to_decide = True
|
||||
else:
|
||||
fallback_to_decide = True
|
||||
|
||||
if fallback_to_decide and not only_evaluate_locally:
|
||||
try:
|
||||
feature_flags = self.get_feature_variants(
|
||||
distinct_id, groups=groups, person_properties=person_properties, group_properties=group_properties
|
||||
)
|
||||
response = {**response, **feature_flags}
|
||||
except Exception as e:
|
||||
self.log.exception(f"[FEATURE FLAGS] Unable to get feature variants: {e}")
|
||||
|
||||
return response
|
||||
return flags, payloads, fallback_to_decide
|
||||
|
||||
|
||||
def require(name, field, data_type):
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
from posthog.utils import is_valid_regex
|
||||
from dateutil import parser
|
||||
|
||||
from posthog.utils import convert_to_datetime_aware, is_valid_regex
|
||||
|
||||
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
|
||||
|
||||
@@ -43,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
|
||||
|
||||
@@ -126,4 +143,35 @@ def match_property(property, property_values) -> bool:
|
||||
if operator == "lte":
|
||||
return type(override_value) == type(value) and override_value <= value
|
||||
|
||||
if operator in ["is_date_before", "is_date_after"]:
|
||||
try:
|
||||
parsed_date = parser.parse(value)
|
||||
parsed_date = convert_to_datetime_aware(parsed_date)
|
||||
except Exception:
|
||||
raise InconclusiveMatchError("The date set on the flag is not a valid format")
|
||||
|
||||
if isinstance(override_value, datetime.datetime):
|
||||
override_date = convert_to_datetime_aware(override_value)
|
||||
if operator == "is_date_before":
|
||||
return override_date < parsed_date
|
||||
else:
|
||||
return override_date > parsed_date
|
||||
elif isinstance(override_value, datetime.date):
|
||||
if operator == "is_date_before":
|
||||
return override_value < parsed_date.date()
|
||||
else:
|
||||
return override_value > parsed_date.date()
|
||||
elif isinstance(override_value, str):
|
||||
try:
|
||||
override_date = parser.parse(override_value)
|
||||
override_date = convert_to_datetime_aware(override_date)
|
||||
if operator == "is_date_before":
|
||||
return override_date < parsed_date
|
||||
else:
|
||||
return override_date > parsed_date
|
||||
except Exception:
|
||||
raise InconclusiveMatchError("The date provided is not a valid format")
|
||||
else:
|
||||
raise InconclusiveMatchError("The date provided must be a string or date object")
|
||||
|
||||
return False
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ def _process_response(
|
||||
|
||||
def decide(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, **kwargs) -> Any:
|
||||
"""Post the `kwargs to the decide API endpoint"""
|
||||
res = post(api_key, host, "/decide/?v=2", gzip, timeout, **kwargs)
|
||||
res = post(api_key, host, "/decide/?v=3", gzip, timeout, **kwargs)
|
||||
return _process_response(res, success_message="Feature flags decided successfully")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from dateutil import parser, tz
|
||||
from freezegun import freeze_time
|
||||
|
||||
from posthog.client import Client
|
||||
@@ -385,7 +387,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
|
||||
feature_flag_match = client.feature_enabled("beta-feature", "some-distinct-id", only_evaluate_locally=True)
|
||||
|
||||
self.assertEqual(feature_flag_match, False)
|
||||
self.assertEqual(feature_flag_match, None)
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
# beta-feature2 should fallback to decide because region property not given with call
|
||||
@@ -394,13 +396,13 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(feature_flag_match, None)
|
||||
|
||||
feature_flag_match = client.feature_enabled("beta-feature2", "some-distinct-id", only_evaluate_locally=True)
|
||||
self.assertEqual(feature_flag_match, False)
|
||||
self.assertEqual(feature_flag_match, None)
|
||||
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_flag_defaults_dont_hinder_regular_evaluation(self, patch_get, patch_decide):
|
||||
def test_feature_flag_never_returns_undefined_during_regular_evaluation(self, patch_get, patch_decide):
|
||||
patch_decide.return_value = {"featureFlags": {}}
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
client.feature_flags = [
|
||||
@@ -422,28 +424,28 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
]
|
||||
|
||||
# beta-feature resolves to False, so no matter the default, stays False
|
||||
self.assertFalse(client.get_feature_flag("beta-feature", "some-distinct-id", default=True))
|
||||
self.assertFalse(client.get_feature_flag("beta-feature", "some-distinct-id", default=False))
|
||||
self.assertFalse(client.get_feature_flag("beta-feature", "some-distinct-id"))
|
||||
self.assertFalse(client.feature_enabled("beta-feature", "some-distinct-id"))
|
||||
|
||||
# beta-feature2 falls back to decide, and whatever decide returns is the value
|
||||
self.assertFalse(client.get_feature_flag("beta-feature2", "some-distinct-id", default=False))
|
||||
self.assertFalse(client.get_feature_flag("beta-feature2", "some-distinct-id"))
|
||||
self.assertEqual(patch_decide.call_count, 1)
|
||||
|
||||
self.assertFalse(client.get_feature_flag("beta-feature2", "some-distinct-id", default=True))
|
||||
self.assertFalse(client.feature_enabled("beta-feature2", "some-distinct-id"))
|
||||
self.assertEqual(patch_decide.call_count, 2)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
@mock.patch("posthog.client.get")
|
||||
def test_feature_flag_defaults_come_into_play_only_when_decide_errors_out(self, patch_get, patch_decide):
|
||||
def test_feature_flag_return_none_when_decide_errors_out(self, patch_get, patch_decide):
|
||||
patch_decide.side_effect = APIError(400, "Decide error")
|
||||
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
||||
client.feature_flags = []
|
||||
|
||||
# beta-feature2 falls back to decide, which on error falls back to default
|
||||
self.assertFalse(client.get_feature_flag("beta-feature2", "some-distinct-id", default=False))
|
||||
# beta-feature2 falls back to decide, which on error returns None
|
||||
self.assertIsNone(client.get_feature_flag("beta-feature2", "some-distinct-id"))
|
||||
self.assertEqual(patch_decide.call_count, 1)
|
||||
|
||||
self.assertTrue(client.get_feature_flag("beta-feature2", "some-distinct-id", default=True))
|
||||
self.assertIsNone(client.feature_enabled("beta-feature2", "some-distinct-id"))
|
||||
self.assertEqual(patch_decide.call_count, 2)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
@@ -476,7 +478,9 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_with_fallback(self, patch_decide, patch_capture):
|
||||
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}}
|
||||
patch_decide.return_value = {
|
||||
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2", "disabled-feature": False}
|
||||
} # decide should return the same flags
|
||||
client = self.client
|
||||
client.feature_flags = [
|
||||
{
|
||||
@@ -534,6 +538,82 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_decide.call_count, 1)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_and_payloads_with_fallback(self, patch_decide, patch_capture):
|
||||
patch_decide.return_value = {
|
||||
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"},
|
||||
"featureFlagPayloads": {"beta-feature": 100, "beta-feature2": 300},
|
||||
}
|
||||
client = self.client
|
||||
client.feature_flags = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"rollout_percentage": 100,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [],
|
||||
"rollout_percentage": 100,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "some-payload",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Beta Feature",
|
||||
"key": "disabled-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [],
|
||||
"rollout_percentage": 0,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "another-payload",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature2",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [{"key": "country", "value": "US"}],
|
||||
"rollout_percentage": 0,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "payload-3",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
# beta-feature value overridden by /decide
|
||||
self.assertEqual(
|
||||
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
|
||||
{
|
||||
"beta-feature": 100,
|
||||
"beta-feature2": 300,
|
||||
},
|
||||
)
|
||||
self.assertEqual(patch_decide.call_count, 1)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_with_fallback_empty_local_flags(self, patch_decide, patch_capture):
|
||||
@@ -547,6 +627,23 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_decide.call_count, 1)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_and_payloads_with_fallback_empty_local_flags(self, patch_decide, patch_capture):
|
||||
patch_decide.return_value = {
|
||||
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"},
|
||||
"featureFlagPayloads": {"beta-feature": 100, "beta-feature2": 300},
|
||||
}
|
||||
client = self.client
|
||||
client.feature_flags = []
|
||||
# beta-feature value overridden by /decide
|
||||
self.assertEqual(
|
||||
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
|
||||
{"beta-feature": 100, "beta-feature2": 300},
|
||||
)
|
||||
self.assertEqual(patch_decide.call_count, 1)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_with_no_fallback(self, patch_decide, patch_capture):
|
||||
@@ -590,6 +687,60 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_and_payloads_with_no_fallback(self, patch_decide, patch_capture):
|
||||
patch_decide.return_value = {"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}}
|
||||
client = self.client
|
||||
basic_flag = {
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"rollout_percentage": 100,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [],
|
||||
"rollout_percentage": 100,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "new",
|
||||
},
|
||||
},
|
||||
}
|
||||
disabled_flag = {
|
||||
"id": 2,
|
||||
"name": "Beta Feature",
|
||||
"key": "disabled-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [],
|
||||
"rollout_percentage": 0,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "some-payload",
|
||||
},
|
||||
},
|
||||
}
|
||||
client.feature_flags = [
|
||||
basic_flag,
|
||||
disabled_flag,
|
||||
]
|
||||
client.feature_flags_by_key = {"beta-feature": basic_flag, "disabled-feature": disabled_flag}
|
||||
self.assertEqual(
|
||||
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"], {"beta-feature": "new"}
|
||||
)
|
||||
# decide not called because this can be evaluated locally
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_with_fallback_but_only_local_evaluation_set(self, patch_decide, patch_capture):
|
||||
@@ -651,6 +802,83 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_get_all_flags_and_payloads_with_fallback_but_only_local_evaluation_set(self, patch_decide, patch_capture):
|
||||
patch_decide.return_value = {
|
||||
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"},
|
||||
"featureFlagPayloads": {"beta-feature": 100, "beta-feature2": 300},
|
||||
}
|
||||
client = self.client
|
||||
flag_1 = {
|
||||
"id": 1,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"rollout_percentage": 100,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [],
|
||||
"rollout_percentage": 100,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "some-payload",
|
||||
},
|
||||
},
|
||||
}
|
||||
flag_2 = {
|
||||
"id": 2,
|
||||
"name": "Beta Feature",
|
||||
"key": "disabled-feature",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [],
|
||||
"rollout_percentage": 0,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "another-payload",
|
||||
},
|
||||
},
|
||||
}
|
||||
flag_3 = {
|
||||
"id": 3,
|
||||
"name": "Beta Feature",
|
||||
"key": "beta-feature2",
|
||||
"is_simple_flag": False,
|
||||
"active": True,
|
||||
"filters": {
|
||||
"groups": [
|
||||
{
|
||||
"properties": [{"key": "country", "value": "US"}],
|
||||
"rollout_percentage": 0,
|
||||
}
|
||||
],
|
||||
"payloads": {
|
||||
"true": "payload-3",
|
||||
},
|
||||
},
|
||||
}
|
||||
client.feature_flags = [
|
||||
flag_1,
|
||||
flag_2,
|
||||
flag_3,
|
||||
]
|
||||
client.feature_flags_by_key = {"beta-feature": flag_1, "disabled-feature": flag_2, "beta-feature2": flag_3}
|
||||
# beta-feature2 has no value
|
||||
self.assertEqual(
|
||||
client.get_all_flags_and_payloads("distinct_id", only_evaluate_locally=True)["featureFlagPayloads"],
|
||||
{"beta-feature": "some-payload"},
|
||||
)
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
self.assertEqual(patch_capture.call_count, 0)
|
||||
|
||||
@mock.patch.object(Client, "capture")
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_compute_inactive_flags_locally(self, patch_decide, patch_capture):
|
||||
@@ -943,7 +1171,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
||||
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
|
||||
|
||||
patch_decide.side_effect = APIError(401, "decide error")
|
||||
self.assertTrue(client.feature_enabled("doesnt-exist", "distinct_id", True))
|
||||
self.assertIsNone(client.feature_enabled("doesnt-exist", "distinct_id"))
|
||||
|
||||
@mock.patch("posthog.client.Poller")
|
||||
@mock.patch("posthog.client.decide")
|
||||
@@ -967,6 +1195,302 @@ 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)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_boolean_feature_flag_payloads_local(self, patch_decide):
|
||||
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},
|
||||
},
|
||||
}
|
||||
self.client.feature_flags = [basic_flag]
|
||||
self.client.feature_flags_by_key = {"person-flag": basic_flag}
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"person-flag", "some-distinct-id", person_properties={"region": "USA"}
|
||||
),
|
||||
300,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"person-flag", "some-distinct-id", match_value=True, person_properties={"region": "USA"}
|
||||
),
|
||||
300,
|
||||
)
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_boolean_feature_flag_payload_decide(self, patch_decide):
|
||||
patch_decide.return_value = {"featureFlagPayloads": {"person-flag": 300}}
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"person-flag", "some-distinct-id", person_properties={"region": "USA"}
|
||||
),
|
||||
300,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"person-flag", "some-distinct-id", match_value=True, person_properties={"region": "USA"}
|
||||
),
|
||||
300,
|
||||
)
|
||||
self.assertEqual(patch_decide.call_count, 2)
|
||||
|
||||
@mock.patch("posthog.client.decide")
|
||||
def test_multivariate_feature_flag_payloads(self, patch_decide):
|
||||
multivariate_flag = {
|
||||
"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},
|
||||
]
|
||||
},
|
||||
"payloads": {"first-variant": "some-payload", "third-variant": {"a": "json"}},
|
||||
},
|
||||
}
|
||||
self.client.feature_flags = [multivariate_flag]
|
||||
self.client.feature_flags_by_key = {"beta-feature": multivariate_flag}
|
||||
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"beta-feature", "test_id", person_properties={"email": "test@posthog.com"}
|
||||
),
|
||||
{"a": "json"},
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"beta-feature", "test_id", match_value="third-variant", person_properties={"email": "test@posthog.com"}
|
||||
),
|
||||
{"a": "json"},
|
||||
)
|
||||
|
||||
# Force different match value
|
||||
self.assertEqual(
|
||||
self.client.get_feature_flag_payload(
|
||||
"beta-feature", "test_id", match_value="first-variant", person_properties={"email": "test@posthog.com"}
|
||||
),
|
||||
"some-payload",
|
||||
)
|
||||
self.assertEqual(patch_decide.call_count, 0)
|
||||
|
||||
|
||||
class TestMatchProperties(unittest.TestCase):
|
||||
def property(self, key, value, operator=None):
|
||||
@@ -1118,6 +1642,58 @@ class TestMatchProperties(unittest.TestCase):
|
||||
self.assertFalse(match_property(property_d, {"key": "44"}))
|
||||
self.assertFalse(match_property(property_d, {"key": 44}))
|
||||
|
||||
def test_match_property_date_operators(self):
|
||||
property_a = self.property(key="key", value="2022-05-01", operator="is_date_before")
|
||||
self.assertTrue(match_property(property_a, {"key": "2022-03-01"}))
|
||||
self.assertTrue(match_property(property_a, {"key": "2022-04-30"}))
|
||||
self.assertTrue(match_property(property_a, {"key": datetime.date(2022, 4, 30)}))
|
||||
self.assertTrue(match_property(property_a, {"key": datetime.datetime(2022, 4, 30, 1, 2, 3)}))
|
||||
self.assertTrue(
|
||||
match_property(
|
||||
property_a, {"key": datetime.datetime(2022, 4, 30, 1, 2, 3, tzinfo=tz.gettz("Europe/Madrid"))}
|
||||
)
|
||||
)
|
||||
self.assertTrue(match_property(property_a, {"key": parser.parse("2022-04-30")}))
|
||||
self.assertFalse(match_property(property_a, {"key": "2022-05-30"}))
|
||||
|
||||
# Can't be a number
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(property_a, {"key": 1})
|
||||
|
||||
# can't be invalid string
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(property_a, {"key": "abcdef"})
|
||||
|
||||
property_b = self.property(key="key", value="2022-05-01", operator="is_date_after")
|
||||
self.assertTrue(match_property(property_b, {"key": "2022-05-02"}))
|
||||
self.assertTrue(match_property(property_b, {"key": "2022-05-30"}))
|
||||
self.assertTrue(match_property(property_b, {"key": datetime.datetime(2022, 5, 30)}))
|
||||
self.assertTrue(match_property(property_b, {"key": parser.parse("2022-05-30")}))
|
||||
self.assertFalse(match_property(property_b, {"key": "2022-04-30"}))
|
||||
|
||||
# can't be invalid string
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(property_b, {"key": "abcdef"})
|
||||
|
||||
# Invalid flag property
|
||||
property_c = self.property(key="key", value=1234, operator="is_date_before")
|
||||
|
||||
with self.assertRaises(InconclusiveMatchError):
|
||||
match_property(property_c, {"key": 1})
|
||||
|
||||
# Timezone aware property
|
||||
property_d = self.property(key="key", value="2022-04-05 12:34:12 +01:00", operator="is_date_before")
|
||||
self.assertFalse(match_property(property_d, {"key": "2022-05-30"}))
|
||||
|
||||
self.assertTrue(match_property(property_d, {"key": "2022-03-30"}))
|
||||
self.assertTrue(match_property(property_d, {"key": "2022-04-05 12:34:11 +01:00"}))
|
||||
self.assertTrue(match_property(property_d, {"key": "2022-04-05 12:34:11 +01:00"}))
|
||||
|
||||
self.assertFalse(match_property(property_d, {"key": "2022-04-05 12:34:13 +01:00"}))
|
||||
|
||||
self.assertTrue(match_property(property_d, {"key": "2022-04-05 11:34:11 +00:00"}))
|
||||
self.assertFalse(match_property(property_d, {"key": "2022-04-05 11:34:13 +00:00"}))
|
||||
|
||||
|
||||
class TestCaptureCalls(unittest.TestCase):
|
||||
@mock.patch.object(Client, "capture")
|
||||
|
||||
+7
-1
@@ -2,7 +2,7 @@ import logging
|
||||
import numbers
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
@@ -109,3 +109,9 @@ class SizeLimitedDict(defaultdict):
|
||||
self.clear()
|
||||
|
||||
super().__setitem__(key, value)
|
||||
|
||||
|
||||
def convert_to_datetime_aware(date_obj):
|
||||
if date_obj.tzinfo is None:
|
||||
date_obj = date_obj.replace(tzinfo=timezone.utc)
|
||||
return date_obj
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
VERSION = "2.0.1"
|
||||
VERSION = "2.3.0"
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(VERSION, end="")
|
||||
|
||||
Reference in New Issue
Block a user