ai-station/.venv/lib/python3.12/site-packages/posthog/test/test_feature_flags.py

5391 lines
168 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import datetime
import unittest
import mock
from dateutil import parser, tz
from freezegun import freeze_time
from posthog.client import Client
from posthog.feature_flags import (
InconclusiveMatchError,
match_property,
relative_date_parse_for_feature_flag_matching,
)
from posthog.request import APIError
from posthog.test.test_utils import FAKE_TEST_API_KEY
class TestLocalEvaluation(unittest.TestCase):
@classmethod
def setUpClass(cls):
# This ensures no real HTTP POST requests are made
cls.capture_patch = mock.patch.object(Client, "capture")
cls.capture_patch.start()
@classmethod
def tearDownClass(cls):
cls.capture_patch.stop()
def set_fail(self, e, batch):
"""Mark the failure handler"""
print("FAIL", e, batch) # noqa: T201
self.failed = True
def setUp(self):
self.failed = False
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
@mock.patch("posthog.client.get")
def test_flag_person_properties(self, patch_get):
self.client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
}
],
"rollout_percentage": 100,
}
],
},
}
]
feature_flag_match = self.client.get_feature_flag(
"person-flag", "some-distinct-id", person_properties={"region": "USA"}
)
not_feature_flag_match = self.client.get_feature_flag(
"person-flag", "some-distinct-2", person_properties={"region": "Canada"}
)
self.assertTrue(feature_flag_match)
self.assertFalse(not_feature_flag_match)
def test_case_insensitive_matching(self):
self.client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"is_simple_flag": True,
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "location",
"operator": "exact",
"value": ["Straße"],
"type": "person",
}
],
"rollout_percentage": 100,
},
{
"properties": [
{
"key": "star",
"operator": "exact",
"value": ["ſun"],
"type": "person",
}
],
"rollout_percentage": 100,
},
],
},
}
]
self.assertTrue(
self.client.get_feature_flag(
"person-flag",
"some-distinct-id",
person_properties={"location": "straße"},
)
)
self.assertTrue(
self.client.get_feature_flag(
"person-flag",
"some-distinct-id",
person_properties={"location": "strasse"},
)
)
self.assertTrue(
self.client.get_feature_flag(
"person-flag", "some-distinct-id", person_properties={"star": "ſun"}
)
)
self.assertTrue(
self.client.get_feature_flag(
"person-flag", "some-distinct-id", person_properties={"star": "sun"}
)
)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_flag_group_properties(self, patch_get, patch_flags):
self.client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "group-flag",
"active": True,
"filters": {
"aggregation_group_type_index": 0,
"groups": [
{
"properties": [
{
"group_type_index": 0,
"key": "name",
"operator": "exact",
"value": ["Project Name 1"],
"type": "group",
}
],
"rollout_percentage": 35,
}
],
},
}
]
self.client.group_type_mapping = {"0": "company", "1": "project"}
# Group names not passed in
self.assertFalse(
self.client.get_feature_flag(
"group-flag",
"some-distinct-id",
group_properties={"company": {"name": "Project Name 1"}},
)
)
self.assertFalse(
self.client.get_feature_flag(
"group-flag",
"some-distinct-2",
group_properties={"company": {"name": "Project Name 2"}},
)
)
# this is good
self.assertTrue(
self.client.get_feature_flag(
"group-flag",
"some-distinct-id",
groups={"company": "amazon_without_rollout"},
group_properties={"company": {"name": "Project Name 1"}},
)
)
# rollout %
self.assertFalse(
self.client.get_feature_flag(
"group-flag",
"some-distinct-id",
groups={"company": "amazon"},
group_properties={"company": {"name": "Project Name 1"}},
)
)
# property mismatch
self.assertFalse(
self.client.get_feature_flag(
"group-flag",
"some-distinct-2",
groups={"company": "amazon_without_rollout"},
group_properties={"company": {"name": "Project Name 2"}},
)
)
self.assertEqual(patch_flags.call_count, 0)
# Now group type mappings are gone, so fall back to /decide/
patch_flags.return_value = {
"featureFlags": {"group-flag": "decide-fallback-value"}
}
self.client.group_type_mapping = {}
self.assertEqual(
self.client.get_feature_flag(
"group-flag",
"some-distinct-id",
groups={"company": "amazon"},
group_properties={"company": {"name": "Project Name 1"}},
),
"decide-fallback-value",
)
self.assertEqual(patch_flags.call_count, 1)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_flag_with_complex_definition(self, patch_get, patch_flags):
patch_flags.return_value = {
"featureFlags": {"complex-flag": "decide-fallback-value"}
}
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "complex-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
},
{
"key": "name",
"operator": "exact",
"value": ["Aloha"],
"type": "person",
},
],
"rollout_percentage": 100,
},
{
"properties": [
{
"key": "email",
"operator": "exact",
"value": ["a@b.com", "b@c.com"],
"type": "person",
},
],
"rollout_percentage": 30,
},
{
"properties": [
{
"key": "doesnt_matter",
"operator": "exact",
"value": ["1", "2"],
"type": "person",
},
],
"rollout_percentage": 0,
},
],
},
}
]
self.assertTrue(
client.get_feature_flag(
"complex-flag",
"some-distinct-id",
person_properties={"region": "USA", "name": "Aloha"},
)
)
self.assertEqual(patch_flags.call_count, 0)
# this distinctIDs hash is < rollout %
self.assertTrue(
client.get_feature_flag(
"complex-flag",
"some-distinct-id_within_rollout?",
person_properties={"region": "USA", "email": "a@b.com"},
)
)
self.assertEqual(patch_flags.call_count, 0)
# will fall back on `/decide`, as all properties present for second group, but that group resolves to false
self.assertEqual(
client.get_feature_flag(
"complex-flag",
"some-distinct-id_outside_rollout?",
person_properties={"region": "USA", "email": "a@b.com"},
),
"decide-fallback-value",
)
self.assertEqual(patch_flags.call_count, 1)
patch_flags.reset_mock()
# same as above
self.assertEqual(
client.get_feature_flag(
"complex-flag",
"some-distinct-id",
person_properties={"doesnt_matter": "1"},
),
"decide-fallback-value",
)
self.assertEqual(patch_flags.call_count, 1)
patch_flags.reset_mock()
# this one will need to fall back
self.assertEqual(
client.get_feature_flag(
"complex-flag", "some-distinct-id", person_properties={"region": "USA"}
),
"decide-fallback-value",
)
self.assertEqual(patch_flags.call_count, 1)
patch_flags.reset_mock()
# won't need to fall back when all values are present
self.assertFalse(
client.get_feature_flag(
"complex-flag",
"some-distinct-id_outside_rollout?",
person_properties={
"region": "USA",
"email": "a@b.com",
"name": "X",
"doesnt_matter": "1",
},
)
)
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flags_fallback_to_decide(self, patch_get, patch_flags):
patch_flags.return_value = {
"featureFlags": {"beta-feature": "alakazam", "beta-feature2": "alakazam2"}
}
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "id",
"value": 98,
"operator": None,
"type": "cohort",
}
],
"rollout_percentage": 100,
}
],
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "beta-feature2",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
}
],
"rollout_percentage": 100,
}
],
},
},
]
# beta-feature fallbacks to decide because property type is unknown
feature_flag_match = client.get_feature_flag("beta-feature", "some-distinct-id")
self.assertEqual(feature_flag_match, "alakazam")
self.assertEqual(patch_flags.call_count, 1)
# beta-feature2 fallbacks to decide because region property not given with call
feature_flag_match = client.get_feature_flag(
"beta-feature2", "some-distinct-id"
)
self.assertEqual(feature_flag_match, "alakazam2")
self.assertEqual(patch_flags.call_count, 2)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flags_dont_fallback_to_decide_when_only_local_evaluation_is_true(
self, patch_get, patch_flags
):
patch_flags.return_value = {
"featureFlags": {"beta-feature": "alakazam", "beta-feature2": "alakazam2"}
}
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "id",
"value": 98,
"operator": None,
"type": "cohort",
}
],
"rollout_percentage": 100,
}
],
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "beta-feature2",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
}
],
"rollout_percentage": 100,
}
],
},
},
]
# beta-feature should fallback to decide because property type is unknown,
# but doesn't because only_evaluate_locally is true
feature_flag_match = client.get_feature_flag(
"beta-feature", "some-distinct-id", only_evaluate_locally=True
)
self.assertEqual(feature_flag_match, None)
self.assertEqual(patch_flags.call_count, 0)
feature_flag_match = client.feature_enabled(
"beta-feature", "some-distinct-id", only_evaluate_locally=True
)
self.assertEqual(feature_flag_match, None)
self.assertEqual(patch_flags.call_count, 0)
# beta-feature2 should fallback to decide because region property not given with call
# but doesn't because only_evaluate_locally is true
feature_flag_match = client.get_feature_flag(
"beta-feature2", "some-distinct-id", only_evaluate_locally=True
)
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, None)
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flag_never_returns_undefined_during_regular_evaluation(
self, patch_get, patch_flags
):
patch_flags.return_value = {"featureFlags": {}}
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
],
},
},
]
# beta-feature resolves to False, so no matter the default, stays 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"))
self.assertEqual(patch_flags.call_count, 1)
self.assertFalse(client.feature_enabled("beta-feature2", "some-distinct-id"))
self.assertEqual(patch_flags.call_count, 2)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flag_return_none_when_decide_errors_out(
self, patch_get, patch_flags
):
patch_flags.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 returns None
self.assertIsNone(client.get_feature_flag("beta-feature2", "some-distinct-id"))
self.assertEqual(patch_flags.call_count, 1)
self.assertIsNone(client.feature_enabled("beta-feature2", "some-distinct-id"))
self.assertEqual(patch_flags.call_count, 2)
@mock.patch("posthog.client.flags")
def test_experience_continuity_flag_not_evaluated_locally(self, patch_flags):
patch_flags.return_value = {
"featureFlags": {"beta-feature": "decide-fallback-value"}
}
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
"ensure_experience_continuity": True,
}
]
# decide called always because experience_continuity is set
self.assertEqual(
client.get_feature_flag("beta-feature", "distinct_id"),
"decide-fallback-value",
)
self.assertEqual(patch_flags.call_count, 1)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_with_fallback(self, patch_flags, patch_capture):
patch_flags.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 = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
]
},
},
{
"id": 3,
"name": "Beta Feature",
"key": "beta-feature2",
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "country", "value": "US"}],
"rollout_percentage": 0,
}
]
},
},
]
# beta-feature value overridden by /decide
self.assertEqual(
client.get_all_flags("distinct_id"),
{
"beta-feature": "variant-1",
"beta-feature2": "variant-2",
"disabled-feature": False,
},
)
self.assertEqual(patch_flags.call_count, 1)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_and_payloads_with_fallback(self, patch_flags, patch_capture):
patch_flags.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",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
"payloads": {
"true": "some-payload",
},
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
],
"payloads": {
"true": "another-payload",
},
},
},
{
"id": 3,
"name": "Beta Feature",
"key": "beta-feature2",
"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_flags.call_count, 1)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_with_fallback_empty_local_flags(
self, patch_flags, patch_capture
):
patch_flags.return_value = {
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}
}
client = self.client
client.feature_flags = []
# beta-feature value overridden by /decide
self.assertEqual(
client.get_all_flags("distinct_id"),
{"beta-feature": "variant-1", "beta-feature2": "variant-2"},
)
self.assertEqual(patch_flags.call_count, 1)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_and_payloads_with_fallback_empty_local_flags(
self, patch_flags, patch_capture
):
patch_flags.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_flags.call_count, 1)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_with_no_fallback(self, patch_flags, patch_capture):
patch_flags.return_value = {
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}
}
client = self.client
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
]
},
},
]
self.assertEqual(
client.get_all_flags("distinct_id"),
{"beta-feature": True, "disabled-feature": False},
)
# decide not called because this can be evaluated locally
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_and_payloads_with_no_fallback(
self, patch_flags, patch_capture
):
client = self.client
basic_flag = {
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
"payloads": {
"true": "new",
},
},
}
disabled_flag = {
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
],
"payloads": {
"true": "some-payload",
},
},
}
client.feature_flags = [
basic_flag,
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_flags.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_with_fallback_but_only_local_evaluation_set(
self, patch_flags, patch_capture
):
patch_flags.return_value = {
"featureFlags": {"beta-feature": "variant-1", "beta-feature2": "variant-2"}
}
client = self.client
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
]
},
},
{
"id": 3,
"name": "Beta Feature",
"key": "beta-feature2",
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "country", "value": "US"}],
"rollout_percentage": 0,
}
]
},
},
]
# beta-feature2 has no value
self.assertEqual(
client.get_all_flags("distinct_id", only_evaluate_locally=True),
{"beta-feature": True, "disabled-feature": False},
)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_get_all_flags_and_payloads_with_fallback_but_only_local_evaluation_set(
self, patch_flags, patch_capture
):
patch_flags.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",
"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",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
],
"payloads": {
"true": "another-payload",
},
},
}
flag_3 = {
"id": 3,
"name": "Beta Feature",
"key": "beta-feature2",
"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,
]
# 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_flags.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_compute_inactive_flags_locally(self, patch_flags, patch_capture):
client = self.client
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
]
},
},
]
self.assertEqual(
client.get_all_flags("distinct_id"),
{"beta-feature": True, "disabled-feature": False},
)
# decide not called because this can be evaluated locally
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
# Now, after a poll interval, flag 1 is inactive, and flag 2 rollout is set to 100%.
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": False,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
},
{
"id": 2,
"name": "Beta Feature",
"key": "disabled-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
},
]
self.assertEqual(
client.get_all_flags("distinct_id"),
{"beta-feature": False, "disabled-feature": True},
)
# decide not called because this can be evaluated locally
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_capture.call_count, 0)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flags_local_evaluation_None_values(self, patch_get, patch_flags):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
id: 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"variant": None,
"properties": [
{
"key": "latestBuildVersion",
"type": "person",
"value": ".+",
"operator": "regex",
},
{
"key": "latestBuildVersionMajor",
"type": "person",
"value": "23",
"operator": "gt",
},
{
"key": "latestBuildVersionMinor",
"type": "person",
"value": "31",
"operator": "gt",
},
{
"key": "latestBuildVersionPatch",
"type": "person",
"value": "0",
"operator": "gt",
},
],
"rollout_percentage": 100,
}
],
},
},
]
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={
"latestBuildVersion": None,
"latestBuildVersionMajor": None,
"latestBuildVersionMinor": None,
"latestBuildVersionPatch": None,
},
)
self.assertEqual(feature_flag_match, False)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={
"latestBuildVersion": "24.32.1",
"latestBuildVersionMajor": "24",
"latestBuildVersionMinor": "32",
"latestBuildVersionPatch": "1",
},
)
self.assertEqual(feature_flag_match, True)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flags_local_evaluation_for_cohorts(self, patch_get, patch_flags):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 2,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
},
{
"key": "id",
"value": 98,
"operator": None,
"type": "cohort",
},
],
"rollout_percentage": 100,
}
],
},
},
]
client.cohorts = {
"98": {
"type": "OR",
"values": [
{"key": "id", "value": 1, "type": "cohort"},
{
"key": "nation",
"operator": "exact",
"value": ["UK"],
"type": "person",
},
],
},
"1": {
"type": "AND",
"values": [
{
"key": "other",
"operator": "exact",
"value": ["thing"],
"type": "person",
}
],
},
}
feature_flag_match = client.get_feature_flag(
"beta-feature", "some-distinct-id", person_properties={"region": "UK"}
)
self.assertEqual(feature_flag_match, False)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={"region": "USA", "nation": "UK"},
)
# even though 'other' property is not present, the cohort should still match since it's an OR condition
self.assertEqual(feature_flag_match, True)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={"region": "USA", "other": "thing"},
)
self.assertEqual(feature_flag_match, True)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flags_local_evaluation_for_negated_cohorts(
self, patch_get, patch_flags
):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 2,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "region",
"operator": "exact",
"value": ["USA"],
"type": "person",
},
{
"key": "id",
"value": 98,
"operator": None,
"type": "cohort",
},
],
"rollout_percentage": 100,
}
],
},
},
]
client.cohorts = {
"98": {
"type": "OR",
"values": [
{"key": "id", "value": 1, "type": "cohort"},
{
"key": "nation",
"operator": "exact",
"value": ["UK"],
"type": "person",
},
],
},
"1": {
"type": "AND",
"values": [
{
"key": "other",
"operator": "exact",
"value": ["thing"],
"type": "person",
"negation": True,
}
],
},
}
feature_flag_match = client.get_feature_flag(
"beta-feature", "some-distinct-id", person_properties={"region": "UK"}
)
self.assertEqual(feature_flag_match, False)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={"region": "USA", "nation": "UK"},
)
# even though 'other' property is not present, the cohort should still match since it's an OR condition
self.assertEqual(feature_flag_match, True)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={"region": "USA", "other": "thing"},
)
# since 'other' is negated, we return False. Since 'nation' is not present, we can't tell whether the flag should be true or false, so go to decide
self.assertEqual(patch_flags.call_count, 1)
self.assertEqual(patch_get.call_count, 0)
patch_flags.reset_mock()
feature_flag_match = client.get_feature_flag(
"beta-feature",
"some-distinct-id",
person_properties={"region": "USA", "other": "thing2"},
)
self.assertEqual(feature_flag_match, True)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
@mock.patch("posthog.feature_flags.log")
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_flags_with_flag_dependencies(
self, patch_get, patch_flags, mock_log
):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Flag with Dependencies",
"key": "flag-with-dependencies",
"active": True,
"filters": {
"groups": [
{
"properties": [
{
"key": "beta-feature",
"operator": "exact",
"value": True,
"type": "flag",
},
{
"key": "email",
"operator": "icontains",
"value": "@example.com",
"type": "person",
},
],
"rollout_percentage": 100,
}
],
},
}
]
# Test that flag evaluation doesn't fail when encountering a flag dependency
# The flag should evaluate based on other conditions (email contains @example.com)
# Since flag dependencies aren't implemented, it should skip the flag condition
# and evaluate based on the email condition only
feature_flag_match = client.get_feature_flag(
"flag-with-dependencies",
"test-user",
person_properties={"email": "test@example.com"},
)
self.assertEqual(feature_flag_match, True)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
# Verify warning was logged for flag dependency
mock_log.warning.assert_called_with(
"Flag dependency filters are not supported in local evaluation. "
"Skipping condition for flag '%s' with dependency on flag '%s'",
"flag-with-dependencies",
"beta-feature",
)
# Test with email that doesn't match
feature_flag_match = client.get_feature_flag(
"flag-with-dependencies",
"test-user-2",
person_properties={"email": "test@other.com"},
)
self.assertEqual(feature_flag_match, False)
self.assertEqual(patch_flags.call_count, 0)
self.assertEqual(patch_get.call_count, 0)
# Verify warning was logged again for the second evaluation
self.assertEqual(mock_log.warning.call_count, 2)
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_load_feature_flags(self, patch_get, patch_poll):
patch_get.return_value = {
"flags": [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
},
{
"id": 2,
"name": "Alpha Feature",
"key": "alpha-feature",
"active": False,
},
],
"group_type_mapping": {"0": "company"},
}
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
with freeze_time("2020-01-01T12:01:00.0000Z"):
client.load_feature_flags()
self.assertEqual(len(client.feature_flags), 2)
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
self.assertEqual(client.group_type_mapping, {"0": "company"})
self.assertEqual(
client._last_feature_flag_poll.isoformat(), "2020-01-01T12:01:00+00:00"
)
self.assertEqual(patch_poll.call_count, 1)
def test_load_feature_flags_wrong_key(self):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
with self.assertLogs("posthog", level="ERROR") as logs:
client.load_feature_flags()
self.assertEqual(
logs.output[0],
"ERROR:posthog:[FEATURE FLAGS] Error loading feature flags: To use feature flags, please set a valid personal_api_key. More information: https://posthog.com/docs/api/overview",
)
client.debug = True
self.assertRaises(APIError, client.load_feature_flags)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_enabled_simple(self, patch_get, patch_flags):
client = Client(FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_is_false(self, patch_get, patch_flags):
client = Client(FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 0,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 0,
}
]
},
}
]
self.assertFalse(client.feature_enabled("beta-feature", "distinct_id"))
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.flags")
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_is_true_when_rollout_is_undefined(
self, patch_get, patch_flags
):
client = Client(FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": None,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": None,
}
]
},
}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_with_project_api_key(self, patch_get):
client = Client(project_api_key=FAKE_TEST_API_KEY, on_error=self.set_fail)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.flags")
def test_feature_enabled_request_multi_variate(self, patch_flags):
patch_flags.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",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
]
},
}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
# decide not called because this can be evaluated locally
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.get")
def test_feature_enabled_simple_without_rollout_percentage(self, patch_get):
client = Client(FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"active": True,
"filters": {
"groups": [
{
"properties": [],
}
]
},
}
]
self.assertTrue(client.feature_enabled("beta-feature", "distinct_id"))
@mock.patch("posthog.client.flags")
def test_get_feature_flag(self, patch_flags):
patch_flags.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",
"active": True,
"rollout_percentage": 100,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
"multivariate": {
"variants": [
{"key": "variant-1", "rollout_percentage": 50},
{"key": "variant-2", "rollout_percentage": 50},
]
},
},
}
]
self.assertEqual(
client.get_feature_flag("beta-feature", "distinct_id"), "variant-1"
)
# decide not called because this can be evaluated locally
self.assertEqual(patch_flags.call_count, 0)
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.flags")
def test_feature_enabled_doesnt_exist(self, patch_flags, patch_poll):
client = Client(FAKE_TEST_API_KEY)
client.feature_flags = []
patch_flags.return_value = {"featureFlags": {}}
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
patch_flags.side_effect = APIError(401, "decide error")
self.assertIsNone(client.feature_enabled("doesnt-exist", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.flags")
def test_personal_api_key_doesnt_exist(self, patch_flags, patch_poll):
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
client.feature_flags = []
patch_flags.return_value = {"featureFlags": {"feature-flag": True}}
self.assertTrue(client.feature_enabled("feature-flag", "distinct_id"))
@mock.patch("posthog.client.Poller")
@mock.patch("posthog.client.get")
def test_load_feature_flags_error(self, patch_get, patch_poll):
def raise_effect():
raise Exception("http exception")
patch_get.return_value.raiseError.side_effect = raise_effect
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
client.feature_flags = []
self.assertFalse(client.feature_enabled("doesnt-exist", "distinct_id"))
@mock.patch("posthog.client.flags")
def test_get_feature_flag_with_variant_overrides(self, patch_flags):
patch_flags.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",
"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_flags.call_count, 0)
@mock.patch("posthog.client.flags")
def test_flag_with_clashing_variant_overrides(self, patch_flags):
patch_flags.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",
"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_flags.call_count, 0)
@mock.patch("posthog.client.flags")
def test_flag_with_invalid_variant_overrides(self, patch_flags):
patch_flags.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",
"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_flags.call_count, 0)
@mock.patch("posthog.client.flags")
def test_flag_with_multiple_variant_overrides(self, patch_flags):
patch_flags.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",
"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_flags.call_count, 0)
@mock.patch("posthog.client.flags")
def test_boolean_feature_flag_payloads_local(self, patch_flags):
basic_flag = {
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"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.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_flags.call_count, 0)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_boolean_feature_flag_payload_decide(self, patch_flags, patch_capture):
patch_flags.return_value = {
"featureFlags": {"person-flag": True},
"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_flags.call_count, 2)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.reset_mock()
@mock.patch("posthog.client.flags")
def test_multivariate_feature_flag_payloads(self, patch_flags):
multivariate_flag = {
"id": 1,
"name": "Beta Feature",
"key": "beta-feature",
"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.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_flags.call_count, 0)
class TestMatchProperties(unittest.TestCase):
def property(self, key, value, operator=None):
result = {"key": key, "value": value}
if operator is not None:
result.update({"operator": operator})
return result
def test_match_properties_exact(self):
property_a = self.property(key="key", value="value")
self.assertTrue(match_property(property_a, {"key": "value"}))
self.assertFalse(match_property(property_a, {"key": "value2"}))
self.assertFalse(match_property(property_a, {"key": ""}))
self.assertFalse(match_property(property_a, {"key": None}))
with self.assertRaises(InconclusiveMatchError):
match_property(property_a, {"key2": "value"})
match_property(property_a, {})
property_b = self.property(key="key", value="value", operator="exact")
self.assertTrue(match_property(property_b, {"key": "value"}))
self.assertFalse(match_property(property_b, {"key": "value2"}))
property_c = self.property(
key="key", value=["value1", "value2", "value3"], operator="exact"
)
self.assertTrue(match_property(property_c, {"key": "value1"}))
self.assertTrue(match_property(property_c, {"key": "value2"}))
self.assertTrue(match_property(property_c, {"key": "value3"}))
self.assertFalse(match_property(property_c, {"key": "value4"}))
with self.assertRaises(InconclusiveMatchError):
match_property(property_c, {"key2": "value"})
def test_match_properties_not_in(self):
property_a = self.property(key="key", value="value", operator="is_not")
self.assertTrue(match_property(property_a, {"key": "value2"}))
self.assertTrue(match_property(property_a, {"key": ""}))
self.assertTrue(match_property(property_a, {"key": None}))
property_c = self.property(
key="key", value=["value1", "value2", "value3"], operator="is_not"
)
self.assertTrue(match_property(property_c, {"key": "value4"}))
self.assertTrue(match_property(property_c, {"key": "value5"}))
self.assertTrue(match_property(property_c, {"key": "value6"}))
self.assertTrue(match_property(property_c, {"key": ""}))
self.assertTrue(match_property(property_c, {"key": None}))
self.assertFalse(match_property(property_c, {"key": "value2"}))
self.assertFalse(match_property(property_c, {"key": "value3"}))
self.assertFalse(match_property(property_c, {"key": "value1"}))
with self.assertRaises(InconclusiveMatchError):
match_property(property_a, {"key2": "value"})
match_property(property_c, {"key2": "value1"}) # overrides don't have 'key'
def test_match_properties_is_set(self):
property_a = self.property(key="key", value="is_set", operator="is_set")
self.assertTrue(match_property(property_a, {"key": "value"}))
self.assertTrue(match_property(property_a, {"key": "value2"}))
self.assertTrue(match_property(property_a, {"key": ""}))
self.assertFalse(match_property(property_a, {"key": None}))
with self.assertRaises(InconclusiveMatchError):
match_property(property_a, {"key2": "value"})
match_property(property_a, {})
def test_match_properties_icontains(self):
property_a = self.property(key="key", value="valUe", operator="icontains")
self.assertTrue(match_property(property_a, {"key": "value"}))
self.assertTrue(match_property(property_a, {"key": "value2"}))
self.assertTrue(match_property(property_a, {"key": "value3"}))
self.assertTrue(match_property(property_a, {"key": "vaLue4"}))
self.assertTrue(match_property(property_a, {"key": "343tfvalue5"}))
self.assertFalse(match_property(property_a, {"key": "Alakazam"}))
self.assertFalse(match_property(property_a, {"key": 123}))
property_b = self.property(key="key", value="3", operator="icontains")
self.assertTrue(match_property(property_b, {"key": "3"}))
self.assertTrue(match_property(property_b, {"key": 323}))
self.assertTrue(match_property(property_b, {"key": "val3"}))
self.assertFalse(match_property(property_b, {"key": "three"}))
def test_match_properties_regex(self):
property_a = self.property(key="key", value=r"\.com$", operator="regex")
self.assertTrue(match_property(property_a, {"key": "value.com"}))
self.assertTrue(match_property(property_a, {"key": "value2.com"}))
self.assertFalse(match_property(property_a, {"key": "value2com"}))
self.assertFalse(match_property(property_a, {"key": ".com343tfvalue5"}))
self.assertFalse(match_property(property_a, {"key": "Alakazam"}))
self.assertFalse(match_property(property_a, {"key": 123}))
self.assertFalse(match_property(property_a, {"key": "valuecom"}))
self.assertFalse(match_property(property_a, {"key": r"value\com"}))
property_b = self.property(key="key", value="3", operator="regex")
self.assertTrue(match_property(property_b, {"key": "3"}))
self.assertTrue(match_property(property_b, {"key": 323}))
self.assertTrue(match_property(property_b, {"key": "val3"}))
self.assertFalse(match_property(property_b, {"key": "three"}))
# invalid regex
property_c = self.property(key="key", value="?*", operator="regex")
self.assertFalse(match_property(property_c, {"key": "value"}))
self.assertFalse(match_property(property_c, {"key": "value2"}))
# non string value
property_d = self.property(key="key", value=4, operator="regex")
self.assertTrue(match_property(property_d, {"key": "4"}))
self.assertTrue(match_property(property_d, {"key": 4}))
self.assertFalse(match_property(property_d, {"key": "value"}))
def test_match_properties_math_operators(self):
property_a = self.property(key="key", value=1, operator="gt")
self.assertTrue(match_property(property_a, {"key": 2}))
self.assertTrue(match_property(property_a, {"key": 3}))
self.assertFalse(match_property(property_a, {"key": 0}))
self.assertFalse(match_property(property_a, {"key": -1}))
# now we handle type mismatches so this should be true
self.assertTrue(match_property(property_a, {"key": "23"}))
property_b = self.property(key="key", value=1, operator="lt")
self.assertTrue(match_property(property_b, {"key": 0}))
self.assertTrue(match_property(property_b, {"key": -1}))
self.assertTrue(match_property(property_b, {"key": -3}))
self.assertFalse(match_property(property_b, {"key": 1}))
self.assertFalse(match_property(property_b, {"key": "1"}))
self.assertFalse(match_property(property_b, {"key": "3"}))
property_c = self.property(key="key", value=1, operator="gte")
self.assertTrue(match_property(property_c, {"key": 1}))
self.assertTrue(match_property(property_c, {"key": 2}))
self.assertFalse(match_property(property_c, {"key": 0}))
self.assertFalse(match_property(property_c, {"key": -1}))
# now we handle type mismatches so this should be true
self.assertTrue(match_property(property_c, {"key": "3"}))
property_d = self.property(key="key", value="43", operator="lte")
self.assertTrue(match_property(property_d, {"key": "41"}))
self.assertTrue(match_property(property_d, {"key": "42"}))
self.assertTrue(match_property(property_d, {"key": "43"}))
self.assertFalse(match_property(property_d, {"key": "44"}))
self.assertFalse(match_property(property_d, {"key": 44}))
self.assertTrue(match_property(property_d, {"key": 42}))
property_e = self.property(key="key", value="30", operator="lt")
self.assertTrue(match_property(property_e, {"key": "29"}))
# depending on the type of override, we adjust type comparison
self.assertTrue(match_property(property_e, {"key": "100"}))
self.assertFalse(match_property(property_e, {"key": 100}))
property_f = self.property(key="key", value="123aloha", operator="gt")
self.assertFalse(match_property(property_f, {"key": "123"}))
self.assertFalse(match_property(property_f, {"key": 122}))
# this turns into a string comparison
self.assertTrue(match_property(property_f, {"key": 129}))
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"})
)
@freeze_time("2022-05-01")
def test_match_property_relative_date_operators(self):
property_a = self.property(key="key", value="-6h", 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.datetime(2022, 4, 30, 1, 2, 3)})
)
# false because date comparison, instead of datetime, so reduces to same date
self.assertFalse(
match_property(property_a, {"key": datetime.date(2022, 4, 30)})
)
self.assertFalse(
match_property(
property_a, {"key": datetime.datetime(2022, 4, 30, 19, 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="1h", 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):
self.assertFalse(match_property(property_b, {"key": "abcdef"}))
# Invalid flag property
property_c = self.property(key="key", value=1234, operator="is_date_after")
with self.assertRaises(InconclusiveMatchError):
self.assertFalse(match_property(property_c, {"key": 1}))
# parsed as 1234-05-01 for some reason?
self.assertTrue(match_property(property_c, {"key": "2022-05-30"}))
# # Timezone aware property
property_d = self.property(key="key", value="12d", 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-19 01:34:11+02:00"})
)
self.assertFalse(
match_property(property_d, {"key": "2022-04-19 02:00:01+02:00"})
)
# Try all possible relative dates
property_e = self.property(key="key", value="1h", operator="is_date_before")
self.assertFalse(match_property(property_e, {"key": "2022-05-01 00:00:00"}))
self.assertTrue(match_property(property_e, {"key": "2022-04-30 22:00:00"}))
property_f = self.property(key="key", value="-1d", operator="is_date_before")
self.assertTrue(match_property(property_f, {"key": "2022-04-29 23:59:00"}))
self.assertFalse(match_property(property_f, {"key": "2022-04-30 00:00:01"}))
property_g = self.property(key="key", value="1w", operator="is_date_before")
self.assertTrue(match_property(property_g, {"key": "2022-04-23 00:00:00"}))
self.assertFalse(match_property(property_g, {"key": "2022-04-24 00:00:00"}))
self.assertFalse(match_property(property_g, {"key": "2022-04-24 00:00:01"}))
property_h = self.property(key="key", value="1m", operator="is_date_before")
self.assertTrue(match_property(property_h, {"key": "2022-03-01 00:00:00"}))
self.assertFalse(match_property(property_h, {"key": "2022-04-05 00:00:00"}))
property_i = self.property(key="key", value="1y", operator="is_date_before")
self.assertTrue(match_property(property_i, {"key": "2021-04-28 00:00:00"}))
self.assertFalse(match_property(property_i, {"key": "2021-05-01 00:00:01"}))
property_j = self.property(key="key", value="122h", operator="is_date_after")
self.assertTrue(match_property(property_j, {"key": "2022-05-01 00:00:00"}))
self.assertFalse(match_property(property_j, {"key": "2022-04-23 01:00:00"}))
property_k = self.property(key="key", value="2d", operator="is_date_after")
self.assertTrue(match_property(property_k, {"key": "2022-05-01 00:00:00"}))
self.assertTrue(match_property(property_k, {"key": "2022-04-29 00:00:01"}))
self.assertFalse(match_property(property_k, {"key": "2022-04-29 00:00:00"}))
property_l = self.property(key="key", value="-02w", operator="is_date_after")
self.assertTrue(match_property(property_l, {"key": "2022-05-01 00:00:00"}))
self.assertFalse(match_property(property_l, {"key": "2022-04-16 00:00:00"}))
property_m = self.property(key="key", value="1m", operator="is_date_after")
self.assertTrue(match_property(property_m, {"key": "2022-04-01 00:00:01"}))
self.assertFalse(match_property(property_m, {"key": "2022-04-01 00:00:00"}))
property_n = self.property(key="key", value="1y", operator="is_date_after")
self.assertTrue(match_property(property_n, {"key": "2022-05-01 00:00:00"}))
self.assertTrue(match_property(property_n, {"key": "2021-05-01 00:00:01"}))
self.assertFalse(match_property(property_n, {"key": "2021-05-01 00:00:00"}))
self.assertFalse(match_property(property_n, {"key": "2021-04-30 00:00:00"}))
self.assertFalse(match_property(property_n, {"key": "2021-03-01 12:13:00"}))
def test_none_property_value_with_all_operators(self):
property_a = self.property(key="key", value="none", operator="is_not")
self.assertFalse(match_property(property_a, {"key": None}))
self.assertTrue(match_property(property_a, {"key": "non"}))
property_b = self.property(key="key", value=None, operator="is_set")
self.assertFalse(match_property(property_b, {"key": None}))
property_c = self.property(key="key", value="no", operator="icontains")
self.assertFalse(match_property(property_c, {"key": None}))
self.assertFalse(match_property(property_c, {"key": "smh"}))
property_d = self.property(key="key", value="No", operator="regex")
self.assertFalse(match_property(property_d, {"key": None}))
property_d_lower_case = self.property(key="key", value="no", operator="regex")
self.assertFalse(match_property(property_d_lower_case, {"key": None}))
property_e = self.property(key="key", value=1, operator="gt")
self.assertFalse(match_property(property_e, {"key": None}))
property_f = self.property(key="key", value=1, operator="lt")
self.assertFalse(match_property(property_f, {"key": None}))
property_g = self.property(key="key", value="xyz", operator="gte")
self.assertFalse(match_property(property_g, {"key": None}))
property_h = self.property(key="key", value="Oo", operator="lte")
self.assertFalse(match_property(property_h, {"key": None}))
property_i = self.property(
key="key", value="2022-05-01", operator="is_date_before"
)
self.assertFalse(match_property(property_i, {"key": None}))
property_j = self.property(
key="key", value="2022-05-01", operator="is_date_after"
)
self.assertFalse(match_property(property_j, {"key": None}))
property_k = self.property(
key="key", value="2022-05-01", operator="is_date_before"
)
with self.assertRaises(InconclusiveMatchError):
self.assertFalse(match_property(property_k, {"key": "random"}))
def test_unknown_operator(self):
property_a = self.property(key="key", value="2022-05-01", operator="is_unknown")
with self.assertRaises(InconclusiveMatchError) as exception_context:
match_property(property_a, {"key": "random"})
self.assertEqual(
str(exception_context.exception), "Unknown operator is_unknown"
)
class TestRelativeDateParsing(unittest.TestCase):
def test_invalid_input(self):
with freeze_time("2020-01-01T12:01:20.1340Z"):
assert relative_date_parse_for_feature_flag_matching("1") is None
assert relative_date_parse_for_feature_flag_matching("1x") is None
assert relative_date_parse_for_feature_flag_matching("1.2y") is None
assert relative_date_parse_for_feature_flag_matching("1z") is None
assert relative_date_parse_for_feature_flag_matching("1s") is None
assert (
relative_date_parse_for_feature_flag_matching("123344000.134m") is None
)
assert relative_date_parse_for_feature_flag_matching("bazinga") is None
assert relative_date_parse_for_feature_flag_matching("000bello") is None
assert relative_date_parse_for_feature_flag_matching("000hello") is None
assert relative_date_parse_for_feature_flag_matching("000h") is not None
assert relative_date_parse_for_feature_flag_matching("1000h") is not None
def test_overflow(self):
assert relative_date_parse_for_feature_flag_matching("1000000h") is None
assert (
relative_date_parse_for_feature_flag_matching("100000000000000000y") is None
)
def test_hour_parsing(self):
with freeze_time("2020-01-01T12:01:20.1340Z"):
assert relative_date_parse_for_feature_flag_matching(
"1h"
) == datetime.datetime(
2020, 1, 1, 11, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"2h"
) == datetime.datetime(
2020, 1, 1, 10, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"24h"
) == datetime.datetime(
2019, 12, 31, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"30h"
) == datetime.datetime(
2019, 12, 31, 6, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"48h"
) == datetime.datetime(
2019, 12, 30, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"24h"
) == relative_date_parse_for_feature_flag_matching("1d")
assert relative_date_parse_for_feature_flag_matching(
"48h"
) == relative_date_parse_for_feature_flag_matching("2d")
def test_day_parsing(self):
with freeze_time("2020-01-01T12:01:20.1340Z"):
assert relative_date_parse_for_feature_flag_matching(
"1d"
) == datetime.datetime(
2019, 12, 31, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"2d"
) == datetime.datetime(
2019, 12, 30, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"7d"
) == datetime.datetime(
2019, 12, 25, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"14d"
) == datetime.datetime(
2019, 12, 18, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"30d"
) == datetime.datetime(
2019, 12, 2, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"7d"
) == relative_date_parse_for_feature_flag_matching("1w")
def test_week_parsing(self):
with freeze_time("2020-01-01T12:01:20.1340Z"):
assert relative_date_parse_for_feature_flag_matching(
"1w"
) == datetime.datetime(
2019, 12, 25, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"2w"
) == datetime.datetime(
2019, 12, 18, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"4w"
) == datetime.datetime(
2019, 12, 4, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"8w"
) == datetime.datetime(
2019, 11, 6, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"1m"
) == datetime.datetime(
2019, 12, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"4w"
) != relative_date_parse_for_feature_flag_matching("1m")
def test_month_parsing(self):
with freeze_time("2020-01-01T12:01:20.1340Z"):
assert relative_date_parse_for_feature_flag_matching(
"1m"
) == datetime.datetime(
2019, 12, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"2m"
) == datetime.datetime(
2019, 11, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"4m"
) == datetime.datetime(
2019, 9, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"8m"
) == datetime.datetime(
2019, 5, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"1y"
) == datetime.datetime(
2019, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"12m"
) == relative_date_parse_for_feature_flag_matching("1y")
with freeze_time("2020-04-03T00:00:00"):
assert relative_date_parse_for_feature_flag_matching(
"1m"
) == datetime.datetime(2020, 3, 3, 0, 0, 0, tzinfo=tz.gettz("UTC"))
assert relative_date_parse_for_feature_flag_matching(
"2m"
) == datetime.datetime(2020, 2, 3, 0, 0, 0, tzinfo=tz.gettz("UTC"))
assert relative_date_parse_for_feature_flag_matching(
"4m"
) == datetime.datetime(2019, 12, 3, 0, 0, 0, tzinfo=tz.gettz("UTC"))
assert relative_date_parse_for_feature_flag_matching(
"8m"
) == datetime.datetime(2019, 8, 3, 0, 0, 0, tzinfo=tz.gettz("UTC"))
assert relative_date_parse_for_feature_flag_matching(
"1y"
) == datetime.datetime(2019, 4, 3, 0, 0, 0, tzinfo=tz.gettz("UTC"))
assert relative_date_parse_for_feature_flag_matching(
"12m"
) == relative_date_parse_for_feature_flag_matching("1y")
def test_year_parsing(self):
with freeze_time("2020-01-01T12:01:20.1340Z"):
assert relative_date_parse_for_feature_flag_matching(
"1y"
) == datetime.datetime(
2019, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"2y"
) == datetime.datetime(
2018, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"4y"
) == datetime.datetime(
2016, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
assert relative_date_parse_for_feature_flag_matching(
"8y"
) == datetime.datetime(
2012, 1, 1, 12, 1, 20, 134000, tzinfo=tz.gettz("UTC")
)
class TestCaptureCalls(unittest.TestCase):
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_capture_is_called(self, patch_flags, patch_capture):
patch_flags.return_value = {"featureFlags": {"decide-flag": "decide-value"}}
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "complex-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "region", "value": "USA"}],
"rollout_percentage": 100,
}
],
},
}
]
self.assertTrue(
client.get_feature_flag(
"complex-flag",
"some-distinct-id",
person_properties={"region": "USA", "name": "Aloha"},
)
)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id",
properties={
"$feature_flag": "complex-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/complex-flag": True,
},
groups={},
disable_geoip=None,
)
patch_capture.reset_mock()
# called again for same user, shouldn't call capture again
self.assertTrue(
client.get_feature_flag(
"complex-flag",
"some-distinct-id",
person_properties={"region": "USA", "name": "Aloha"},
)
)
self.assertEqual(patch_capture.call_count, 0)
patch_capture.reset_mock()
# called for different user, should call capture again
self.assertTrue(
client.get_feature_flag(
"complex-flag",
"some-distinct-id2",
person_properties={"region": "USA", "name": "Aloha"},
)
)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id2",
properties={
"$feature_flag": "complex-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/complex-flag": True,
},
groups={},
disable_geoip=None,
)
patch_capture.reset_mock()
# called for different user, but send configuration is false, so should NOT call capture again
self.assertTrue(
client.get_feature_flag(
"complex-flag",
"some-distinct-id345",
person_properties={"region": "USA", "name": "Aloha"},
send_feature_flag_events=False,
)
)
self.assertEqual(patch_capture.call_count, 0)
patch_capture.reset_mock()
# called for different flag, falls back to decide, should call capture again
self.assertEqual(
client.get_feature_flag(
"decide-flag",
"some-distinct-id2",
person_properties={"region": "USA", "name": "Aloha"},
groups={"organization": "org1"},
),
"decide-value",
)
self.assertEqual(patch_flags.call_count, 1)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id2",
properties={
"$feature_flag": "decide-flag",
"$feature_flag_response": "decide-value",
"locally_evaluated": False,
"$feature/decide-flag": "decide-value",
},
groups={"organization": "org1"},
disable_geoip=None,
)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_capture_is_called_with_flag_details(self, patch_flags, patch_capture):
patch_flags.return_value = {
"flags": {
"decide-flag": {
"key": "decide-flag",
"enabled": True,
"variant": "decide-variant",
"reason": {
"description": "Matched condition set 1",
},
"metadata": {
"id": 23,
"version": 42,
},
},
"false-flag": {
"key": "false-flag",
"enabled": False,
"variant": None,
"reason": {
"code": "no_matching_condition",
"description": "No matching condition",
"condition_index": None,
},
"metadata": {
"id": 1,
"version": 2,
},
},
},
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
}
client = Client(FAKE_TEST_API_KEY)
self.assertEqual(
client.get_feature_flag("decide-flag", "some-distinct-id"), "decide-variant"
)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id",
properties={
"$feature_flag": "decide-flag",
"$feature_flag_response": "decide-variant",
"locally_evaluated": False,
"$feature/decide-flag": "decide-variant",
"$feature_flag_reason": "Matched condition set 1",
"$feature_flag_id": 23,
"$feature_flag_version": 42,
"$feature_flag_request_id": "18043bf7-9cf6-44cd-b959-9662ee20d371",
},
groups={},
disable_geoip=None,
)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_capture_is_called_with_flag_details_and_payload(
self, patch_flags, patch_capture
):
patch_flags.return_value = {
"flags": {
"decide-flag-with-payload": {
"key": "decide-flag-with-payload",
"enabled": True,
"variant": None,
"reason": {
"code": "matched_condition",
"condition_index": 0,
"description": "Matched condition set 1",
},
"metadata": {
"id": 23,
"version": 42,
"payload": '{"foo": "bar"}',
},
}
},
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
}
client = Client(FAKE_TEST_API_KEY)
self.assertEqual(
client.get_feature_flag_payload(
"decide-flag-with-payload", "some-distinct-id"
),
{"foo": "bar"},
)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id",
properties={
"$feature_flag": "decide-flag-with-payload",
"$feature_flag_response": True,
"locally_evaluated": False,
"$feature/decide-flag-with-payload": True,
"$feature_flag_reason": "Matched condition set 1",
"$feature_flag_id": 23,
"$feature_flag_version": 42,
"$feature_flag_request_id": "18043bf7-9cf6-44cd-b959-9662ee20d371",
"$feature_flag_payload": {"foo": "bar"},
},
groups={},
disable_geoip=None,
)
@mock.patch("posthog.client.flags")
def test_capture_is_called_but_does_not_add_all_flags(self, patch_flags):
patch_flags.return_value = {"featureFlags": {"decide-flag": "decide-value"}}
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "complex-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "region", "value": "USA"}],
"rollout_percentage": 100,
},
],
},
},
{
"id": 2,
"name": "Gamma Feature",
"key": "simple-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
},
],
},
},
]
self.assertTrue(
client.get_feature_flag(
"complex-flag", "some-distinct-id", person_properties={"region": "USA"}
)
)
# Grab the capture message that was just added to the queue
msg = client.queue.get(block=False)
assert msg["event"] == "$feature_flag_called"
assert msg["properties"]["$feature_flag"] == "complex-flag"
assert msg["properties"]["$feature_flag_response"] is True
assert msg["properties"]["locally_evaluated"] is True
assert msg["properties"]["$feature/complex-flag"] is True
assert "$feature/simple-flag" not in msg["properties"]
assert "$active_feature_flags" not in msg["properties"]
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_capture_is_called_in_get_feature_flag_payload(
self, patch_flags, patch_capture
):
patch_flags.return_value = {
"featureFlags": {"person-flag": True},
"featureFlagPayloads": {"person-flag": 300},
}
client = Client(
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "person-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "region", "value": "USA"}],
"rollout_percentage": 100,
}
],
},
}
]
# Call get_feature_flag_payload with match_value=None to trigger get_feature_flag
client.get_feature_flag_payload(
key="person-flag",
distinct_id="some-distinct-id",
person_properties={"region": "USA", "name": "Aloha"},
)
# Assert that capture was called once, with the correct parameters
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id",
properties={
"$feature_flag": "person-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/person-flag": True,
},
groups={},
disable_geoip=None,
)
# Reset mocks for further tests
patch_capture.reset_mock()
patch_flags.reset_mock()
# Call get_feature_flag_payload again for the same user; capture should not be called again because we've already reported an event for this distinct_id + flag
client.get_feature_flag_payload(
key="person-flag",
distinct_id="some-distinct-id",
person_properties={"region": "USA", "name": "Aloha"},
)
self.assertEqual(patch_capture.call_count, 0)
patch_capture.reset_mock()
# Call get_feature_flag_payload for a different user; capture should be called
client.get_feature_flag_payload(
key="person-flag",
distinct_id="some-distinct-id2",
person_properties={"region": "USA", "name": "Aloha"},
)
self.assertEqual(patch_capture.call_count, 1)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id2",
properties={
"$feature_flag": "person-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/person-flag": True,
},
groups={},
disable_geoip=None,
)
patch_capture.reset_mock()
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_disable_geoip_get_flag_capture_call(self, patch_flags, patch_capture):
patch_flags.return_value = {"featureFlags": {"decide-flag": "decide-value"}}
client = Client(
FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY, disable_geoip=True
)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "complex-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [{"key": "region", "value": "USA"}],
"rollout_percentage": 100,
}
],
},
}
]
client.get_feature_flag(
"complex-flag",
"some-distinct-id",
person_properties={"region": "USA", "name": "Aloha"},
disable_geoip=False,
)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id="some-distinct-id",
properties={
"$feature_flag": "complex-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/complex-flag": True,
},
groups={},
disable_geoip=False,
)
@mock.patch("posthog.client.MAX_DICT_SIZE", 100)
@mock.patch.object(Client, "capture")
@mock.patch("posthog.client.flags")
def test_capture_multiple_users_doesnt_out_of_memory(
self, patch_flags, patch_capture
):
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "complex-flag",
"active": True,
"filters": {
"groups": [
{
"properties": [],
"rollout_percentage": 100,
}
],
},
}
]
for i in range(1000):
distinct_id = f"some-distinct-id{i}"
client.get_feature_flag(
"complex-flag",
distinct_id,
person_properties={"region": "USA", "name": "Aloha"},
)
patch_capture.assert_called_with(
"$feature_flag_called",
distinct_id=distinct_id,
properties={
"$feature_flag": "complex-flag",
"$feature_flag_response": True,
"locally_evaluated": True,
"$feature/complex-flag": True,
},
groups={},
disable_geoip=None,
)
self.assertEqual(
len(client.distinct_ids_feature_flags_reported), i % 100 + 1
)
class TestConsistency(unittest.TestCase):
@classmethod
def setUpClass(cls):
# This ensures no real HTTP POST requests are made
cls.capture_patch = mock.patch.object(Client, "capture")
cls.capture_patch.start()
@classmethod
def tearDownClass(cls):
cls.capture_patch.stop()
def set_fail(self, e, batch):
"""Mark the failure handler"""
print("FAIL", e, batch) # noqa: T201
self.failed = True
def setUp(self):
self.failed = False
self.client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail)
@mock.patch("posthog.client.get")
def test_simple_flag_consistency(self, patch_get):
self.client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "simple-flag",
"active": True,
"filters": {
"groups": [{"properties": [], "rollout_percentage": 45}],
},
}
]
results = [
False,
True,
True,
False,
True,
False,
False,
True,
False,
True,
False,
True,
True,
False,
True,
False,
False,
False,
True,
True,
False,
True,
False,
False,
True,
False,
True,
True,
False,
False,
False,
True,
True,
True,
True,
False,
False,
False,
False,
False,
False,
True,
True,
False,
True,
True,
False,
False,
False,
True,
True,
False,
False,
False,
False,
True,
False,
True,
False,
True,
False,
True,
True,
False,
True,
False,
True,
False,
True,
True,
False,
False,
True,
False,
False,
True,
False,
True,
False,
False,
True,
False,
False,
False,
True,
True,
False,
True,
True,
False,
True,
True,
True,
True,
True,
False,
True,
True,
False,
False,
True,
True,
True,
True,
False,
False,
True,
False,
True,
True,
True,
False,
False,
False,
False,
False,
True,
False,
False,
True,
True,
True,
False,
False,
True,
False,
True,
False,
False,
True,
False,
False,
False,
False,
False,
False,
False,
False,
True,
True,
False,
False,
True,
False,
False,
True,
True,
False,
False,
True,
False,
True,
False,
True,
True,
True,
False,
False,
False,
True,
False,
False,
False,
False,
True,
True,
False,
True,
True,
False,
True,
False,
True,
True,
False,
True,
False,
True,
True,
True,
False,
True,
False,
False,
True,
True,
False,
True,
False,
True,
True,
False,
False,
True,
True,
True,
True,
False,
True,
True,
False,
False,
True,
False,
True,
False,
False,
True,
True,
False,
True,
False,
True,
False,
False,
False,
False,
False,
False,
False,
True,
False,
True,
True,
False,
False,
True,
False,
True,
False,
False,
False,
True,
False,
True,
False,
False,
False,
True,
False,
False,
True,
False,
True,
True,
False,
False,
False,
False,
True,
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
True,
True,
False,
True,
False,
True,
True,
False,
True,
False,
True,
False,
False,
False,
True,
True,
True,
True,
False,
False,
False,
False,
False,
True,
True,
True,
False,
False,
True,
True,
False,
False,
False,
False,
False,
True,
False,
True,
True,
True,
True,
False,
True,
True,
True,
False,
False,
True,
False,
True,
False,
False,
True,
True,
True,
False,
True,
False,
False,
False,
True,
True,
False,
True,
False,
True,
False,
True,
True,
True,
True,
True,
False,
False,
True,
False,
True,
False,
True,
True,
True,
False,
True,
False,
True,
True,
False,
True,
True,
True,
True,
True,
False,
False,
False,
False,
False,
True,
False,
True,
False,
False,
True,
True,
False,
False,
False,
True,
False,
True,
True,
True,
True,
False,
False,
False,
False,
True,
True,
False,
False,
True,
True,
False,
True,
True,
True,
True,
False,
True,
True,
True,
False,
False,
True,
True,
False,
False,
True,
False,
False,
True,
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
True,
True,
False,
False,
True,
False,
False,
True,
False,
True,
False,
False,
True,
False,
False,
False,
False,
False,
False,
True,
False,
False,
False,
False,
False,
False,
False,
False,
False,
True,
True,
True,
False,
False,
False,
True,
False,
True,
False,
False,
False,
True,
False,
False,
False,
False,
False,
False,
False,
True,
False,
False,
False,
False,
False,
False,
False,
False,
True,
False,
True,
False,
True,
True,
True,
False,
False,
False,
True,
True,
True,
False,
True,
False,
True,
True,
False,
False,
False,
True,
False,
False,
False,
False,
True,
False,
True,
False,
True,
True,
False,
True,
False,
False,
False,
True,
False,
False,
True,
True,
False,
True,
False,
False,
False,
False,
False,
False,
True,
True,
False,
False,
True,
False,
False,
True,
True,
True,
False,
False,
False,
True,
False,
False,
False,
False,
True,
False,
True,
False,
False,
False,
True,
False,
True,
True,
False,
True,
False,
True,
False,
True,
False,
False,
True,
False,
False,
True,
False,
True,
False,
True,
False,
True,
False,
False,
True,
True,
True,
True,
False,
True,
False,
False,
False,
False,
False,
True,
False,
False,
True,
False,
False,
True,
True,
False,
False,
False,
False,
True,
True,
True,
False,
False,
True,
False,
False,
True,
True,
True,
True,
False,
False,
False,
True,
False,
False,
False,
True,
False,
False,
True,
True,
True,
True,
False,
False,
True,
True,
False,
True,
False,
True,
False,
False,
True,
True,
False,
True,
True,
True,
True,
False,
False,
True,
False,
False,
True,
True,
False,
True,
False,
True,
False,
False,
True,
False,
False,
False,
False,
True,
True,
True,
False,
True,
False,
False,
True,
False,
False,
True,
False,
False,
False,
False,
True,
False,
True,
False,
True,
True,
False,
False,
True,
False,
True,
True,
True,
False,
False,
False,
False,
True,
True,
False,
True,
False,
False,
False,
True,
False,
False,
False,
False,
True,
True,
True,
False,
False,
False,
True,
True,
True,
True,
False,
True,
True,
False,
True,
True,
True,
False,
True,
False,
False,
True,
False,
True,
True,
True,
True,
False,
True,
False,
True,
False,
True,
False,
False,
True,
True,
False,
False,
True,
False,
True,
False,
False,
False,
False,
True,
False,
True,
False,
False,
False,
True,
True,
True,
False,
False,
False,
True,
False,
True,
True,
False,
False,
False,
False,
False,
True,
False,
True,
False,
False,
True,
True,
False,
True,
True,
True,
True,
False,
False,
True,
False,
False,
True,
False,
True,
False,
True,
True,
False,
False,
False,
True,
False,
True,
True,
False,
False,
False,
True,
False,
True,
False,
True,
True,
False,
True,
False,
False,
True,
False,
False,
False,
True,
True,
True,
False,
False,
False,
False,
False,
True,
False,
False,
True,
True,
True,
True,
True,
False,
False,
False,
False,
False,
False,
False,
False,
True,
True,
True,
False,
False,
True,
True,
False,
True,
True,
False,
True,
False,
True,
False,
False,
False,
True,
False,
False,
True,
False,
False,
True,
True,
True,
True,
False,
False,
True,
False,
True,
True,
False,
False,
True,
False,
False,
True,
True,
False,
True,
False,
False,
True,
True,
True,
False,
False,
False,
False,
False,
True,
False,
True,
False,
False,
False,
False,
False,
True,
True,
False,
True,
True,
True,
False,
False,
False,
False,
True,
True,
True,
True,
False,
True,
True,
False,
True,
False,
True,
False,
True,
False,
False,
False,
False,
True,
True,
True,
True,
False,
False,
True,
False,
True,
True,
False,
False,
False,
False,
False,
False,
True,
False,
True,
False,
True,
True,
False,
False,
True,
True,
True,
True,
False,
False,
True,
False,
True,
True,
False,
False,
True,
True,
True,
False,
True,
False,
False,
True,
True,
False,
False,
False,
True,
False,
False,
True,
False,
False,
False,
True,
True,
True,
True,
False,
True,
False,
True,
False,
True,
False,
True,
False,
False,
True,
False,
False,
True,
False,
True,
True,
]
for i in range(1000):
distinctID = f"distinct_id_{i}"
feature_flag_match = self.client.feature_enabled("simple-flag", distinctID)
if results[i]:
self.assertTrue(feature_flag_match)
else:
self.assertFalse(feature_flag_match)
@mock.patch("posthog.client.get")
def test_multivariate_flag_consistency(self, patch_get):
self.client.feature_flags = [
{
"id": 1,
"name": "Beta Feature",
"key": "multivariate-flag",
"active": True,
"filters": {
"groups": [{"properties": [], "rollout_percentage": 55}],
"multivariate": {
"variants": [
{
"key": "first-variant",
"name": "First Variant",
"rollout_percentage": 50,
},
{
"key": "second-variant",
"name": "Second Variant",
"rollout_percentage": 20,
},
{
"key": "third-variant",
"name": "Third Variant",
"rollout_percentage": 20,
},
{
"key": "fourth-variant",
"name": "Fourth Variant",
"rollout_percentage": 5,
},
{
"key": "fifth-variant",
"name": "Fifth Variant",
"rollout_percentage": 5,
},
],
},
},
}
]
results = [
"second-variant",
"second-variant",
"first-variant",
False,
False,
"second-variant",
"first-variant",
False,
False,
False,
"first-variant",
"third-variant",
False,
"first-variant",
"second-variant",
"first-variant",
False,
False,
"fourth-variant",
"first-variant",
False,
"third-variant",
False,
False,
False,
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
False,
"third-variant",
"second-variant",
"first-variant",
False,
"third-variant",
False,
False,
"first-variant",
"second-variant",
False,
"first-variant",
"first-variant",
"second-variant",
False,
"first-variant",
False,
False,
"first-variant",
"first-variant",
"first-variant",
"second-variant",
"first-variant",
False,
"second-variant",
"second-variant",
"third-variant",
"second-variant",
"first-variant",
False,
"first-variant",
"second-variant",
"fourth-variant",
False,
"first-variant",
"first-variant",
"first-variant",
False,
"first-variant",
"second-variant",
False,
"third-variant",
False,
False,
False,
False,
False,
False,
"first-variant",
"fifth-variant",
False,
"second-variant",
"first-variant",
"second-variant",
False,
"third-variant",
"third-variant",
False,
False,
False,
False,
"third-variant",
False,
False,
"first-variant",
"first-variant",
False,
"third-variant",
"third-variant",
False,
"third-variant",
"second-variant",
"third-variant",
False,
False,
"second-variant",
"first-variant",
False,
False,
"first-variant",
False,
False,
False,
False,
"first-variant",
"first-variant",
"first-variant",
False,
False,
False,
"first-variant",
"first-variant",
False,
"first-variant",
"first-variant",
False,
False,
False,
False,
False,
False,
False,
False,
False,
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
"second-variant",
False,
"second-variant",
"first-variant",
"second-variant",
"first-variant",
False,
"second-variant",
"second-variant",
False,
"first-variant",
False,
False,
False,
"third-variant",
"first-variant",
False,
False,
"first-variant",
False,
False,
False,
False,
"first-variant",
False,
False,
False,
False,
False,
False,
False,
"first-variant",
"first-variant",
"third-variant",
"first-variant",
"first-variant",
False,
False,
"first-variant",
False,
False,
"fifth-variant",
"second-variant",
False,
"second-variant",
False,
"first-variant",
"third-variant",
"first-variant",
"fifth-variant",
"third-variant",
False,
False,
"fourth-variant",
False,
False,
False,
False,
"third-variant",
False,
False,
"third-variant",
False,
"first-variant",
"second-variant",
"second-variant",
"second-variant",
False,
"first-variant",
"third-variant",
"first-variant",
"first-variant",
False,
False,
False,
False,
False,
"first-variant",
"first-variant",
"first-variant",
"second-variant",
False,
False,
False,
"second-variant",
False,
False,
"first-variant",
False,
"first-variant",
False,
False,
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"first-variant",
"third-variant",
"first-variant",
"first-variant",
"second-variant",
"third-variant",
"third-variant",
False,
"second-variant",
"first-variant",
False,
"second-variant",
"first-variant",
False,
"first-variant",
False,
False,
"first-variant",
"fifth-variant",
"first-variant",
False,
False,
False,
False,
"first-variant",
"first-variant",
"second-variant",
False,
"second-variant",
"third-variant",
"third-variant",
False,
"first-variant",
"third-variant",
False,
False,
"first-variant",
False,
"third-variant",
"first-variant",
False,
"third-variant",
"first-variant",
"first-variant",
False,
"first-variant",
"second-variant",
"second-variant",
"first-variant",
False,
False,
False,
"second-variant",
False,
False,
"first-variant",
"first-variant",
False,
"third-variant",
False,
"first-variant",
False,
"third-variant",
False,
"third-variant",
"second-variant",
"first-variant",
False,
False,
"first-variant",
"third-variant",
"first-variant",
"second-variant",
"fifth-variant",
False,
False,
"first-variant",
False,
False,
False,
"third-variant",
False,
"second-variant",
"first-variant",
False,
False,
False,
False,
"third-variant",
False,
False,
"third-variant",
False,
False,
"first-variant",
"third-variant",
False,
False,
"first-variant",
False,
False,
"fourth-variant",
"fourth-variant",
"third-variant",
"second-variant",
"first-variant",
"third-variant",
"fifth-variant",
False,
"first-variant",
"fifth-variant",
False,
"first-variant",
"first-variant",
"first-variant",
False,
False,
False,
"second-variant",
"fifth-variant",
"second-variant",
"first-variant",
"first-variant",
"second-variant",
False,
False,
"third-variant",
False,
"second-variant",
"fifth-variant",
False,
"third-variant",
"first-variant",
False,
False,
"fourth-variant",
False,
False,
"second-variant",
False,
False,
"first-variant",
"fourth-variant",
"first-variant",
"second-variant",
False,
False,
False,
"first-variant",
"third-variant",
"third-variant",
False,
"first-variant",
"first-variant",
"first-variant",
False,
"first-variant",
False,
"first-variant",
"third-variant",
"third-variant",
False,
False,
"first-variant",
False,
False,
"second-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
False,
"fifth-variant",
"first-variant",
False,
False,
False,
"second-variant",
"third-variant",
"first-variant",
"fourth-variant",
"first-variant",
"third-variant",
False,
"first-variant",
"first-variant",
False,
"third-variant",
"first-variant",
"first-variant",
"third-variant",
False,
"fourth-variant",
"fifth-variant",
"first-variant",
"first-variant",
False,
False,
False,
"first-variant",
"first-variant",
"first-variant",
False,
"first-variant",
"first-variant",
"second-variant",
"first-variant",
False,
"first-variant",
"second-variant",
"first-variant",
False,
"first-variant",
"second-variant",
False,
"first-variant",
"first-variant",
False,
"first-variant",
False,
"first-variant",
False,
"first-variant",
False,
False,
False,
"third-variant",
"third-variant",
"first-variant",
False,
False,
"second-variant",
"third-variant",
"first-variant",
"first-variant",
False,
False,
False,
"second-variant",
"first-variant",
False,
"first-variant",
"third-variant",
False,
"first-variant",
False,
False,
False,
"first-variant",
"third-variant",
"third-variant",
False,
False,
False,
False,
"third-variant",
"fourth-variant",
"fourth-variant",
"first-variant",
"second-variant",
False,
"first-variant",
False,
"second-variant",
"first-variant",
"third-variant",
False,
"third-variant",
False,
"first-variant",
"first-variant",
"third-variant",
False,
False,
False,
"fourth-variant",
"second-variant",
"first-variant",
False,
False,
"first-variant",
"fourth-variant",
False,
"first-variant",
"third-variant",
"first-variant",
False,
False,
"third-variant",
False,
"first-variant",
False,
"first-variant",
"first-variant",
"third-variant",
"second-variant",
"fourth-variant",
False,
"first-variant",
False,
False,
False,
False,
"second-variant",
"first-variant",
"second-variant",
False,
"first-variant",
False,
"first-variant",
"first-variant",
False,
"first-variant",
"first-variant",
"second-variant",
"third-variant",
"first-variant",
"first-variant",
"first-variant",
False,
False,
False,
"third-variant",
False,
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"first-variant",
"first-variant",
"second-variant",
"first-variant",
"fifth-variant",
"fourth-variant",
"first-variant",
"second-variant",
False,
"fourth-variant",
False,
False,
False,
"fourth-variant",
False,
False,
"third-variant",
False,
False,
False,
"first-variant",
"third-variant",
"third-variant",
"second-variant",
"first-variant",
"second-variant",
"first-variant",
False,
"first-variant",
False,
False,
False,
False,
False,
"first-variant",
"first-variant",
False,
"second-variant",
False,
False,
"first-variant",
False,
"second-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"second-variant",
False,
False,
"fifth-variant",
"third-variant",
False,
False,
"first-variant",
False,
False,
False,
"first-variant",
"second-variant",
"third-variant",
"third-variant",
False,
False,
"first-variant",
False,
"third-variant",
"first-variant",
False,
False,
False,
False,
"fourth-variant",
"first-variant",
False,
False,
False,
"third-variant",
False,
False,
"second-variant",
"first-variant",
False,
False,
"second-variant",
"third-variant",
"first-variant",
"first-variant",
False,
"first-variant",
"first-variant",
False,
False,
"second-variant",
"third-variant",
"second-variant",
"third-variant",
False,
False,
"first-variant",
False,
False,
"first-variant",
False,
"second-variant",
False,
False,
False,
False,
"first-variant",
False,
"third-variant",
False,
"first-variant",
False,
False,
"second-variant",
"third-variant",
"second-variant",
"fourth-variant",
"first-variant",
"first-variant",
"first-variant",
False,
"first-variant",
False,
"second-variant",
False,
False,
False,
False,
False,
"first-variant",
False,
False,
False,
False,
False,
"first-variant",
False,
"second-variant",
False,
False,
False,
False,
"second-variant",
False,
"first-variant",
False,
"third-variant",
False,
False,
"first-variant",
"third-variant",
False,
"third-variant",
False,
False,
"second-variant",
False,
"first-variant",
"second-variant",
"first-variant",
False,
False,
False,
False,
False,
"second-variant",
False,
False,
"first-variant",
"third-variant",
False,
"first-variant",
False,
False,
False,
False,
False,
"first-variant",
"second-variant",
False,
False,
False,
"first-variant",
"first-variant",
"fifth-variant",
False,
False,
False,
"first-variant",
False,
"third-variant",
False,
False,
"second-variant",
False,
False,
False,
False,
False,
"fourth-variant",
"second-variant",
"first-variant",
"second-variant",
False,
"second-variant",
False,
"second-variant",
False,
"first-variant",
False,
"first-variant",
"first-variant",
False,
"second-variant",
False,
"first-variant",
False,
"fifth-variant",
False,
"first-variant",
"first-variant",
False,
False,
False,
"first-variant",
False,
"first-variant",
"third-variant",
False,
False,
"first-variant",
"first-variant",
False,
False,
"fifth-variant",
False,
False,
"third-variant",
False,
"third-variant",
"first-variant",
"first-variant",
"third-variant",
"third-variant",
False,
"first-variant",
False,
False,
False,
False,
False,
"first-variant",
False,
False,
False,
False,
"second-variant",
"first-variant",
"second-variant",
"first-variant",
False,
"fifth-variant",
"first-variant",
False,
False,
"fourth-variant",
"first-variant",
"first-variant",
False,
False,
"fourth-variant",
"first-variant",
False,
"second-variant",
"third-variant",
"third-variant",
"first-variant",
"first-variant",
False,
False,
False,
"first-variant",
"first-variant",
"first-variant",
False,
"third-variant",
"third-variant",
"third-variant",
False,
False,
"first-variant",
"first-variant",
False,
"second-variant",
False,
False,
"second-variant",
False,
"third-variant",
"first-variant",
"second-variant",
"fifth-variant",
"first-variant",
"first-variant",
False,
"first-variant",
"fifth-variant",
False,
False,
False,
"third-variant",
"first-variant",
"first-variant",
"second-variant",
"fourth-variant",
"first-variant",
"second-variant",
"first-variant",
False,
False,
False,
"second-variant",
"third-variant",
False,
False,
"first-variant",
False,
False,
False,
False,
False,
False,
"first-variant",
"first-variant",
False,
"third-variant",
False,
"first-variant",
False,
"third-variant",
"third-variant",
"first-variant",
"first-variant",
False,
"second-variant",
False,
"second-variant",
"first-variant",
False,
False,
False,
"second-variant",
False,
"third-variant",
False,
"first-variant",
"fifth-variant",
"first-variant",
"first-variant",
False,
False,
"first-variant",
False,
False,
False,
"first-variant",
"fourth-variant",
"first-variant",
"first-variant",
"first-variant",
"fifth-variant",
False,
False,
False,
"second-variant",
False,
False,
False,
"first-variant",
"first-variant",
False,
False,
"first-variant",
"first-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"first-variant",
False,
"second-variant",
False,
False,
"third-variant",
"second-variant",
"third-variant",
False,
"first-variant",
"third-variant",
"second-variant",
"first-variant",
"third-variant",
False,
False,
"first-variant",
"first-variant",
False,
False,
False,
"first-variant",
"third-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
False,
"third-variant",
"second-variant",
"third-variant",
False,
False,
"third-variant",
"first-variant",
False,
"first-variant",
]
for i in range(1000):
distinctID = f"distinct_id_{i}"
feature_flag_match = self.client.get_feature_flag(
"multivariate-flag", distinctID
)
if results[i]:
self.assertEqual(feature_flag_match, results[i])
else:
self.assertFalse(feature_flag_match)
@mock.patch("posthog.client.flags")
def test_feature_flag_case_sensitive(self, mock_decide):
mock_decide.return_value = {
"featureFlags": {}
} # Ensure decide returns empty flags
client = Client(
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
)
client.feature_flags = [
{
"id": 1,
"key": "Beta-Feature",
"active": True,
"filters": {
"groups": [{"properties": [], "rollout_percentage": 100}],
},
}
]
# Test that flag evaluation is case-sensitive
self.assertTrue(client.feature_enabled("Beta-Feature", "user1"))
self.assertFalse(client.feature_enabled("beta-feature", "user1"))
self.assertFalse(client.feature_enabled("BETA-FEATURE", "user1"))
@mock.patch("posthog.client.flags")
def test_feature_flag_payload_case_sensitive(self, mock_decide):
mock_decide.return_value = {
"featureFlags": {"Beta-Feature": True},
"featureFlagPayloads": {"Beta-Feature": {"some": "value"}},
}
client = Client(
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
)
client.feature_flags = [
{
"id": 1,
"key": "Beta-Feature",
"active": True,
"filters": {
"groups": [{"properties": [], "rollout_percentage": 100}],
"payloads": {
"true": {"some": "value"},
},
},
}
]
# Test that payload retrieval is case-sensitive
self.assertEqual(
client.get_feature_flag_payload("Beta-Feature", "user1"), {"some": "value"}
)
self.assertIsNone(client.get_feature_flag_payload("beta-feature", "user1"))
self.assertIsNone(client.get_feature_flag_payload("BETA-FEATURE", "user1"))
@mock.patch("posthog.client.flags")
def test_feature_flag_case_sensitive_consistency(self, mock_decide):
mock_decide.return_value = {
"featureFlags": {"Beta-Feature": True},
"featureFlagPayloads": {"Beta-Feature": {"some": "value"}},
}
client = Client(
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
)
client.feature_flags = [
{
"id": 1,
"key": "Beta-Feature",
"active": True,
"filters": {
"groups": [{"properties": [], "rollout_percentage": 100}],
"payloads": {
"true": {"some": "value"},
},
},
}
]
# Test that flag evaluation and payload retrieval are consistently case-sensitive
# Only exact match should work
self.assertTrue(client.feature_enabled("Beta-Feature", "user1"))
self.assertEqual(
client.get_feature_flag_payload("Beta-Feature", "user1"), {"some": "value"}
)
# Different cases should not match
test_cases = ["beta-feature", "BETA-FEATURE", "bEtA-FeAtUrE"]
for case in test_cases:
self.assertFalse(client.feature_enabled(case, "user1"))
self.assertIsNone(client.get_feature_flag_payload(case, "user1"))