posthoganalytics 6.6.1__tar.gz → 6.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {posthoganalytics-6.6.1/posthoganalytics.egg-info → posthoganalytics-6.7.0}/PKG-INFO +1 -1
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/anthropic/anthropic.py +1 -1
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/openai/openai_async.py +1 -1
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/client.py +10 -10
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/feature_flags.py +61 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_feature_flags.py +522 -12
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/version.py +1 -1
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0/posthoganalytics.egg-info}/PKG-INFO +1 -1
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/LICENSE +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/MANIFEST.in +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/README.md +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/anthropic/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/anthropic/anthropic_async.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/gemini/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/gemini/gemini.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/langchain/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/langchain/callbacks.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/openai/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/openai/openai.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/openai/openai_providers.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/utils.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/args.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/consumer.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/contexts.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/exception_capture.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/exception_utils.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/integrations/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/integrations/django.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/poller.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/py.typed +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/request.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/__init__.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_before_send.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_client.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_consumer.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_contexts.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_exception_capture.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_feature_flag.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_feature_flag_result.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_module.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_request.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_size_limited_dict.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_types.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_utils.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/types.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/utils.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics.egg-info/SOURCES.txt +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics.egg-info/dependency_links.txt +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics.egg-info/requires.txt +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics.egg-info/top_level.txt +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/pyproject.toml +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/setup.cfg +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/setup.py +0 -0
- {posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/setup_analytics.py +0 -0
|
@@ -329,7 +329,7 @@ class Client(object):
|
|
|
329
329
|
only these flags will be evaluated, improving performance.
|
|
330
330
|
|
|
331
331
|
Category:
|
|
332
|
-
Feature
|
|
332
|
+
Feature flags
|
|
333
333
|
"""
|
|
334
334
|
resp_data = self.get_flags_decision(
|
|
335
335
|
distinct_id,
|
|
@@ -368,7 +368,7 @@ class Client(object):
|
|
|
368
368
|
```
|
|
369
369
|
|
|
370
370
|
Category:
|
|
371
|
-
Feature
|
|
371
|
+
Feature flags
|
|
372
372
|
"""
|
|
373
373
|
resp_data = self.get_flags_decision(
|
|
374
374
|
distinct_id,
|
|
@@ -407,7 +407,7 @@ class Client(object):
|
|
|
407
407
|
```
|
|
408
408
|
|
|
409
409
|
Category:
|
|
410
|
-
Feature
|
|
410
|
+
Feature flags
|
|
411
411
|
"""
|
|
412
412
|
resp = self.get_flags_decision(
|
|
413
413
|
distinct_id,
|
|
@@ -446,7 +446,7 @@ class Client(object):
|
|
|
446
446
|
```
|
|
447
447
|
|
|
448
448
|
Category:
|
|
449
|
-
Feature
|
|
449
|
+
Feature flags
|
|
450
450
|
"""
|
|
451
451
|
groups = groups or {}
|
|
452
452
|
person_properties = person_properties or {}
|
|
@@ -1169,7 +1169,7 @@ class Client(object):
|
|
|
1169
1169
|
```
|
|
1170
1170
|
|
|
1171
1171
|
Category:
|
|
1172
|
-
Feature
|
|
1172
|
+
Feature flags
|
|
1173
1173
|
"""
|
|
1174
1174
|
if not self.personal_api_key:
|
|
1175
1175
|
self.log.warning(
|
|
@@ -1291,7 +1291,7 @@ class Client(object):
|
|
|
1291
1291
|
```
|
|
1292
1292
|
|
|
1293
1293
|
Category:
|
|
1294
|
-
Feature
|
|
1294
|
+
Feature flags
|
|
1295
1295
|
"""
|
|
1296
1296
|
response = self.get_feature_flag(
|
|
1297
1297
|
key,
|
|
@@ -1499,7 +1499,7 @@ class Client(object):
|
|
|
1499
1499
|
```
|
|
1500
1500
|
|
|
1501
1501
|
Category:
|
|
1502
|
-
Feature
|
|
1502
|
+
Feature flags
|
|
1503
1503
|
"""
|
|
1504
1504
|
feature_flag_result = self.get_feature_flag_result(
|
|
1505
1505
|
key,
|
|
@@ -1589,7 +1589,7 @@ class Client(object):
|
|
|
1589
1589
|
```
|
|
1590
1590
|
|
|
1591
1591
|
Category:
|
|
1592
|
-
Feature
|
|
1592
|
+
Feature flags
|
|
1593
1593
|
"""
|
|
1594
1594
|
feature_flag_result = self._get_feature_flag_result(
|
|
1595
1595
|
key,
|
|
@@ -1759,7 +1759,7 @@ class Client(object):
|
|
|
1759
1759
|
```
|
|
1760
1760
|
|
|
1761
1761
|
Category:
|
|
1762
|
-
Feature
|
|
1762
|
+
Feature flags
|
|
1763
1763
|
"""
|
|
1764
1764
|
response = self.get_all_flags_and_payloads(
|
|
1765
1765
|
distinct_id,
|
|
@@ -1803,7 +1803,7 @@ class Client(object):
|
|
|
1803
1803
|
```
|
|
1804
1804
|
|
|
1805
1805
|
Category:
|
|
1806
|
-
Feature
|
|
1806
|
+
Feature flags
|
|
1807
1807
|
"""
|
|
1808
1808
|
if self.disabled:
|
|
1809
1809
|
return {"featureFlags": None, "featureFlagPayloads": None}
|
|
@@ -139,9 +139,70 @@ def evaluate_flag_dependency(
|
|
|
139
139
|
# Definitive False result - dependency failed
|
|
140
140
|
return False
|
|
141
141
|
|
|
142
|
+
# All dependencies in the chain have been evaluated successfully
|
|
143
|
+
# Now check if the final flag value matches the expected value in the property
|
|
144
|
+
flag_key = property.get("key")
|
|
145
|
+
expected_value = property.get("value")
|
|
146
|
+
operator = property.get("operator", "exact")
|
|
147
|
+
|
|
148
|
+
if flag_key and expected_value is not None:
|
|
149
|
+
# Get the actual value of the flag we're checking
|
|
150
|
+
actual_value = evaluation_cache.get(flag_key)
|
|
151
|
+
|
|
152
|
+
if actual_value is None:
|
|
153
|
+
# Flag wasn't evaluated - this shouldn't happen if dependency chain is correct
|
|
154
|
+
raise InconclusiveMatchError(
|
|
155
|
+
f"Flag '{flag_key}' was not evaluated despite being in dependency chain"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# For flag dependencies, we need to compare the actual flag result with expected value
|
|
159
|
+
# using the flag_evaluates_to operator logic
|
|
160
|
+
if operator == "flag_evaluates_to":
|
|
161
|
+
return matches_dependency_value(expected_value, actual_value)
|
|
162
|
+
else:
|
|
163
|
+
# This should never happen, but just to be defensive.
|
|
164
|
+
raise InconclusiveMatchError(
|
|
165
|
+
f"Flag dependency property for '{property.get('key', 'unknown')}' has invalid operator '{operator}'"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# If no value check needed, return True (all dependencies passed)
|
|
142
169
|
return True
|
|
143
170
|
|
|
144
171
|
|
|
172
|
+
def matches_dependency_value(expected_value, actual_value):
|
|
173
|
+
"""
|
|
174
|
+
Check if the actual flag value matches the expected dependency value.
|
|
175
|
+
|
|
176
|
+
This follows the same logic as the C# MatchesDependencyValue function:
|
|
177
|
+
- String variant case: check for exact match or boolean true
|
|
178
|
+
- Boolean case: must match expected boolean value
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
expected_value: The expected value from the property
|
|
182
|
+
actual_value: The actual value returned by the flag evaluation
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
bool: True if the values match according to flag dependency rules
|
|
186
|
+
"""
|
|
187
|
+
# String variant case - check for exact match or boolean true
|
|
188
|
+
if isinstance(actual_value, str) and len(actual_value) > 0:
|
|
189
|
+
if isinstance(expected_value, bool):
|
|
190
|
+
# Any variant matches boolean true
|
|
191
|
+
return expected_value
|
|
192
|
+
elif isinstance(expected_value, str):
|
|
193
|
+
# variants are case-sensitive, hence our comparison is too
|
|
194
|
+
return actual_value == expected_value
|
|
195
|
+
else:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
# Boolean case - must match expected boolean value
|
|
199
|
+
elif isinstance(actual_value, bool) and isinstance(expected_value, bool):
|
|
200
|
+
return actual_value == expected_value
|
|
201
|
+
|
|
202
|
+
# Default case
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
145
206
|
def match_feature_flag_properties(
|
|
146
207
|
flag,
|
|
147
208
|
distinct_id,
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_feature_flags.py
RENAMED
|
@@ -1376,7 +1376,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1376
1376
|
"properties": [
|
|
1377
1377
|
{
|
|
1378
1378
|
"key": "beta-feature",
|
|
1379
|
-
"operator": "
|
|
1379
|
+
"operator": "flag_evaluates_to",
|
|
1380
1380
|
"value": True,
|
|
1381
1381
|
"type": "flag",
|
|
1382
1382
|
"dependency_chain": ["beta-feature"],
|
|
@@ -1456,7 +1456,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1456
1456
|
"properties": [
|
|
1457
1457
|
{
|
|
1458
1458
|
"key": "flag-a",
|
|
1459
|
-
"operator": "
|
|
1459
|
+
"operator": "flag_evaluates_to",
|
|
1460
1460
|
"value": True,
|
|
1461
1461
|
"type": "flag",
|
|
1462
1462
|
"dependency_chain": ["flag-a"],
|
|
@@ -1504,7 +1504,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1504
1504
|
"properties": [
|
|
1505
1505
|
{
|
|
1506
1506
|
"key": "flag-b",
|
|
1507
|
-
"operator": "
|
|
1507
|
+
"operator": "flag_evaluates_to",
|
|
1508
1508
|
"value": True,
|
|
1509
1509
|
"type": "flag",
|
|
1510
1510
|
"dependency_chain": [], # Empty chain indicates circular dependency
|
|
@@ -1526,7 +1526,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1526
1526
|
"properties": [
|
|
1527
1527
|
{
|
|
1528
1528
|
"key": "flag-a",
|
|
1529
|
-
"operator": "
|
|
1529
|
+
"operator": "flag_evaluates_to",
|
|
1530
1530
|
"value": True,
|
|
1531
1531
|
"type": "flag",
|
|
1532
1532
|
"dependency_chain": [], # Empty chain indicates circular dependency
|
|
@@ -1566,7 +1566,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1566
1566
|
"properties": [
|
|
1567
1567
|
{
|
|
1568
1568
|
"key": "non-existent-flag",
|
|
1569
|
-
"operator": "
|
|
1569
|
+
"operator": "flag_evaluates_to",
|
|
1570
1570
|
"value": True,
|
|
1571
1571
|
"type": "flag",
|
|
1572
1572
|
"dependency_chain": ["non-existent-flag"],
|
|
@@ -1629,14 +1629,14 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1629
1629
|
"properties": [
|
|
1630
1630
|
{
|
|
1631
1631
|
"key": "flag-a",
|
|
1632
|
-
"operator": "
|
|
1632
|
+
"operator": "flag_evaluates_to",
|
|
1633
1633
|
"value": True,
|
|
1634
1634
|
"type": "flag",
|
|
1635
1635
|
"dependency_chain": ["flag-a"],
|
|
1636
1636
|
},
|
|
1637
1637
|
{
|
|
1638
1638
|
"key": "flag-b",
|
|
1639
|
-
"operator": "
|
|
1639
|
+
"operator": "flag_evaluates_to",
|
|
1640
1640
|
"value": True,
|
|
1641
1641
|
"type": "flag",
|
|
1642
1642
|
"dependency_chain": ["flag-b"],
|
|
@@ -1658,7 +1658,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1658
1658
|
"properties": [
|
|
1659
1659
|
{
|
|
1660
1660
|
"key": "flag-c",
|
|
1661
|
-
"operator": "
|
|
1661
|
+
"operator": "flag_evaluates_to",
|
|
1662
1662
|
"value": True,
|
|
1663
1663
|
"type": "flag",
|
|
1664
1664
|
"dependency_chain": ["flag-a", "flag-b", "flag-c"],
|
|
@@ -1711,7 +1711,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1711
1711
|
"properties": [
|
|
1712
1712
|
{
|
|
1713
1713
|
"key": "base-flag",
|
|
1714
|
-
"operator": "
|
|
1714
|
+
"operator": "flag_evaluates_to",
|
|
1715
1715
|
"value": True,
|
|
1716
1716
|
"type": "flag",
|
|
1717
1717
|
"dependency_chain": ["base-flag"],
|
|
@@ -1788,10 +1788,10 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1788
1788
|
"properties": [
|
|
1789
1789
|
{
|
|
1790
1790
|
"key": "base-flag",
|
|
1791
|
-
"operator": "
|
|
1791
|
+
"operator": "flag_evaluates_to",
|
|
1792
1792
|
"value": True,
|
|
1793
1793
|
"type": "flag",
|
|
1794
|
-
# No dependency_chain property - should
|
|
1794
|
+
# No dependency_chain property - should evaluate as inconclusive
|
|
1795
1795
|
}
|
|
1796
1796
|
],
|
|
1797
1797
|
"rollout_percentage": 100,
|
|
@@ -1815,7 +1815,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1815
1815
|
|
|
1816
1816
|
property_with_flag_dep = {
|
|
1817
1817
|
"key": "some-flag",
|
|
1818
|
-
"operator": "
|
|
1818
|
+
"operator": "flag_evaluates_to",
|
|
1819
1819
|
"value": True,
|
|
1820
1820
|
"type": "flag",
|
|
1821
1821
|
"dependency_chain": ["some-flag"],
|
|
@@ -1835,6 +1835,516 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1835
1835
|
self.assertIn("Cannot evaluate flag dependency", str(cm.exception))
|
|
1836
1836
|
self.assertIn("some-flag", str(cm.exception))
|
|
1837
1837
|
|
|
1838
|
+
@mock.patch("posthog.client.flags")
|
|
1839
|
+
@mock.patch("posthog.client.get")
|
|
1840
|
+
def test_multi_level_multivariate_dependency_chain(self, patch_get, patch_flags):
|
|
1841
|
+
"""Test multi-level multivariate dependency chain: dependent-flag -> intermediate-flag -> leaf-flag"""
|
|
1842
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1843
|
+
client.feature_flags = [
|
|
1844
|
+
# Leaf flag: multivariate with "control" and "test" variants using person property overrides
|
|
1845
|
+
{
|
|
1846
|
+
"id": 1,
|
|
1847
|
+
"name": "Leaf Flag",
|
|
1848
|
+
"key": "leaf-flag",
|
|
1849
|
+
"active": True,
|
|
1850
|
+
"rollout_percentage": 100,
|
|
1851
|
+
"filters": {
|
|
1852
|
+
"groups": [
|
|
1853
|
+
{
|
|
1854
|
+
"properties": [
|
|
1855
|
+
{
|
|
1856
|
+
"key": "email",
|
|
1857
|
+
"type": "person",
|
|
1858
|
+
"value": "control@example.com",
|
|
1859
|
+
"operator": "exact",
|
|
1860
|
+
}
|
|
1861
|
+
],
|
|
1862
|
+
"rollout_percentage": 100,
|
|
1863
|
+
"variant": "control",
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
"properties": [
|
|
1867
|
+
{
|
|
1868
|
+
"key": "email",
|
|
1869
|
+
"type": "person",
|
|
1870
|
+
"value": "test@example.com",
|
|
1871
|
+
"operator": "exact",
|
|
1872
|
+
}
|
|
1873
|
+
],
|
|
1874
|
+
"rollout_percentage": 100,
|
|
1875
|
+
"variant": "test",
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
"rollout_percentage": 50,
|
|
1879
|
+
"variant": "control",
|
|
1880
|
+
}, # Default fallback
|
|
1881
|
+
],
|
|
1882
|
+
"multivariate": {
|
|
1883
|
+
"variants": [
|
|
1884
|
+
{
|
|
1885
|
+
"key": "control",
|
|
1886
|
+
"name": "Control",
|
|
1887
|
+
"rollout_percentage": 50,
|
|
1888
|
+
},
|
|
1889
|
+
{"key": "test", "name": "Test", "rollout_percentage": 50},
|
|
1890
|
+
]
|
|
1891
|
+
},
|
|
1892
|
+
},
|
|
1893
|
+
},
|
|
1894
|
+
# Intermediate flag: multivariate with "blue" and "green" variants, depends on leaf-flag="control"
|
|
1895
|
+
{
|
|
1896
|
+
"id": 2,
|
|
1897
|
+
"name": "Intermediate Flag",
|
|
1898
|
+
"key": "intermediate-flag",
|
|
1899
|
+
"active": True,
|
|
1900
|
+
"rollout_percentage": 100,
|
|
1901
|
+
"filters": {
|
|
1902
|
+
"groups": [
|
|
1903
|
+
{
|
|
1904
|
+
"properties": [
|
|
1905
|
+
{
|
|
1906
|
+
"key": "leaf-flag",
|
|
1907
|
+
"operator": "flag_evaluates_to",
|
|
1908
|
+
"value": "control",
|
|
1909
|
+
"type": "flag",
|
|
1910
|
+
"dependency_chain": ["leaf-flag"],
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
"key": "variant_type",
|
|
1914
|
+
"type": "person",
|
|
1915
|
+
"value": "blue",
|
|
1916
|
+
"operator": "exact",
|
|
1917
|
+
},
|
|
1918
|
+
],
|
|
1919
|
+
"rollout_percentage": 100,
|
|
1920
|
+
"variant": "blue",
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
"properties": [
|
|
1924
|
+
{
|
|
1925
|
+
"key": "leaf-flag",
|
|
1926
|
+
"operator": "flag_evaluates_to",
|
|
1927
|
+
"value": "control",
|
|
1928
|
+
"type": "flag",
|
|
1929
|
+
"dependency_chain": ["leaf-flag"],
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
"key": "variant_type",
|
|
1933
|
+
"type": "person",
|
|
1934
|
+
"value": "green",
|
|
1935
|
+
"operator": "exact",
|
|
1936
|
+
},
|
|
1937
|
+
],
|
|
1938
|
+
"rollout_percentage": 100,
|
|
1939
|
+
"variant": "green",
|
|
1940
|
+
},
|
|
1941
|
+
],
|
|
1942
|
+
"multivariate": {
|
|
1943
|
+
"variants": [
|
|
1944
|
+
{"key": "blue", "name": "Blue", "rollout_percentage": 50},
|
|
1945
|
+
{"key": "green", "name": "Green", "rollout_percentage": 50},
|
|
1946
|
+
]
|
|
1947
|
+
},
|
|
1948
|
+
},
|
|
1949
|
+
},
|
|
1950
|
+
# Dependent flag: boolean flag that depends on intermediate-flag="blue"
|
|
1951
|
+
{
|
|
1952
|
+
"id": 3,
|
|
1953
|
+
"name": "Dependent Flag",
|
|
1954
|
+
"key": "dependent-flag",
|
|
1955
|
+
"active": True,
|
|
1956
|
+
"rollout_percentage": 100,
|
|
1957
|
+
"filters": {
|
|
1958
|
+
"groups": [
|
|
1959
|
+
{
|
|
1960
|
+
"properties": [
|
|
1961
|
+
{
|
|
1962
|
+
"key": "intermediate-flag",
|
|
1963
|
+
"operator": "flag_evaluates_to",
|
|
1964
|
+
"value": "blue",
|
|
1965
|
+
"type": "flag",
|
|
1966
|
+
"dependency_chain": [
|
|
1967
|
+
"leaf-flag",
|
|
1968
|
+
"intermediate-flag",
|
|
1969
|
+
],
|
|
1970
|
+
}
|
|
1971
|
+
],
|
|
1972
|
+
"rollout_percentage": 100,
|
|
1973
|
+
}
|
|
1974
|
+
],
|
|
1975
|
+
},
|
|
1976
|
+
},
|
|
1977
|
+
]
|
|
1978
|
+
|
|
1979
|
+
# Test using person properties and variant overrides to ensure predictable variants
|
|
1980
|
+
|
|
1981
|
+
# Test 1: Make sure the leaf flag evaluates to the variant we expect using email overrides
|
|
1982
|
+
self.assertEqual(
|
|
1983
|
+
"control",
|
|
1984
|
+
client.get_feature_flag(
|
|
1985
|
+
"leaf-flag",
|
|
1986
|
+
"any-user",
|
|
1987
|
+
person_properties={"email": "control@example.com"},
|
|
1988
|
+
),
|
|
1989
|
+
)
|
|
1990
|
+
self.assertEqual(
|
|
1991
|
+
"test",
|
|
1992
|
+
client.get_feature_flag(
|
|
1993
|
+
"leaf-flag",
|
|
1994
|
+
"any-user",
|
|
1995
|
+
person_properties={"email": "test@example.com"},
|
|
1996
|
+
),
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1999
|
+
# Test 2: Make sure the intermediate flag evaluates to the expected variants when dependency is satisfied
|
|
2000
|
+
self.assertEqual(
|
|
2001
|
+
"blue",
|
|
2002
|
+
client.get_feature_flag(
|
|
2003
|
+
"intermediate-flag",
|
|
2004
|
+
"any-user",
|
|
2005
|
+
person_properties={
|
|
2006
|
+
"email": "control@example.com",
|
|
2007
|
+
"variant_type": "blue",
|
|
2008
|
+
},
|
|
2009
|
+
),
|
|
2010
|
+
)
|
|
2011
|
+
|
|
2012
|
+
self.assertEqual(
|
|
2013
|
+
"green",
|
|
2014
|
+
client.get_feature_flag(
|
|
2015
|
+
"intermediate-flag",
|
|
2016
|
+
"any-user",
|
|
2017
|
+
person_properties={
|
|
2018
|
+
"email": "control@example.com",
|
|
2019
|
+
"variant_type": "green",
|
|
2020
|
+
},
|
|
2021
|
+
),
|
|
2022
|
+
)
|
|
2023
|
+
|
|
2024
|
+
# Test 3: Make sure the intermediate flag evaluates to false when leaf dependency fails
|
|
2025
|
+
self.assertEqual(
|
|
2026
|
+
False,
|
|
2027
|
+
client.get_feature_flag(
|
|
2028
|
+
"intermediate-flag",
|
|
2029
|
+
"any-user",
|
|
2030
|
+
person_properties={
|
|
2031
|
+
"email": "test@example.com", # This makes leaf-flag="test", breaking dependency
|
|
2032
|
+
"variant_type": "blue",
|
|
2033
|
+
},
|
|
2034
|
+
),
|
|
2035
|
+
)
|
|
2036
|
+
|
|
2037
|
+
# Test 4: When leaf-flag="control", intermediate="blue", dependent should be true
|
|
2038
|
+
self.assertEqual(
|
|
2039
|
+
True,
|
|
2040
|
+
client.get_feature_flag(
|
|
2041
|
+
"dependent-flag",
|
|
2042
|
+
"any-user",
|
|
2043
|
+
person_properties={
|
|
2044
|
+
"email": "control@example.com",
|
|
2045
|
+
"variant_type": "blue",
|
|
2046
|
+
},
|
|
2047
|
+
),
|
|
2048
|
+
)
|
|
2049
|
+
|
|
2050
|
+
# Test 5: When leaf-flag="control", intermediate="green", dependent should be false
|
|
2051
|
+
self.assertEqual(
|
|
2052
|
+
False,
|
|
2053
|
+
client.get_feature_flag(
|
|
2054
|
+
"dependent-flag",
|
|
2055
|
+
"any-user",
|
|
2056
|
+
person_properties={
|
|
2057
|
+
"email": "control@example.com",
|
|
2058
|
+
"variant_type": "green",
|
|
2059
|
+
},
|
|
2060
|
+
),
|
|
2061
|
+
)
|
|
2062
|
+
|
|
2063
|
+
# Test 6: When leaf-flag="test", intermediate is False, dependent should be false
|
|
2064
|
+
self.assertEqual(
|
|
2065
|
+
False,
|
|
2066
|
+
client.get_feature_flag(
|
|
2067
|
+
"dependent-flag",
|
|
2068
|
+
"any-user",
|
|
2069
|
+
person_properties={"email": "test@example.com", "variant_type": "blue"},
|
|
2070
|
+
),
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
def test_matches_dependency_value(self):
|
|
2074
|
+
"""Test the matches_dependency_value function logic"""
|
|
2075
|
+
from posthoganalytics.feature_flags import matches_dependency_value
|
|
2076
|
+
|
|
2077
|
+
# String variant matches string exactly (case-sensitive)
|
|
2078
|
+
self.assertTrue(matches_dependency_value("control", "control"))
|
|
2079
|
+
self.assertTrue(matches_dependency_value("Control", "Control"))
|
|
2080
|
+
self.assertFalse(matches_dependency_value("control", "Control"))
|
|
2081
|
+
self.assertFalse(matches_dependency_value("Control", "CONTROL"))
|
|
2082
|
+
self.assertFalse(matches_dependency_value("control", "test"))
|
|
2083
|
+
|
|
2084
|
+
# String variant matches boolean true (any variant is truthy)
|
|
2085
|
+
self.assertTrue(matches_dependency_value(True, "control"))
|
|
2086
|
+
self.assertTrue(matches_dependency_value(True, "test"))
|
|
2087
|
+
self.assertFalse(matches_dependency_value(False, "control"))
|
|
2088
|
+
|
|
2089
|
+
# Boolean matches boolean exactly
|
|
2090
|
+
self.assertTrue(matches_dependency_value(True, True))
|
|
2091
|
+
self.assertTrue(matches_dependency_value(False, False))
|
|
2092
|
+
self.assertFalse(matches_dependency_value(False, True))
|
|
2093
|
+
self.assertFalse(matches_dependency_value(True, False))
|
|
2094
|
+
|
|
2095
|
+
# Empty string doesn't match
|
|
2096
|
+
self.assertFalse(matches_dependency_value(True, ""))
|
|
2097
|
+
self.assertFalse(matches_dependency_value("control", ""))
|
|
2098
|
+
|
|
2099
|
+
# Type mismatches
|
|
2100
|
+
self.assertFalse(matches_dependency_value(123, "control"))
|
|
2101
|
+
self.assertFalse(matches_dependency_value("control", True))
|
|
2102
|
+
|
|
2103
|
+
@mock.patch("posthog.client.flags")
|
|
2104
|
+
@mock.patch("posthog.client.get")
|
|
2105
|
+
def test_production_style_multivariate_dependency_chain(
|
|
2106
|
+
self, patch_get, patch_flags
|
|
2107
|
+
):
|
|
2108
|
+
"""Test production-style multivariate dependency chain: multivariate-root-flag -> multivariate-intermediate-flag -> multivariate-leaf-flag"""
|
|
2109
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
2110
|
+
client.feature_flags = [
|
|
2111
|
+
# Leaf flag: multivariate with fruit variants
|
|
2112
|
+
{
|
|
2113
|
+
"id": 451,
|
|
2114
|
+
"name": "Multivariate Leaf Flag (Base)",
|
|
2115
|
+
"key": "multivariate-leaf-flag",
|
|
2116
|
+
"active": True,
|
|
2117
|
+
"rollout_percentage": 100,
|
|
2118
|
+
"filters": {
|
|
2119
|
+
"groups": [
|
|
2120
|
+
{
|
|
2121
|
+
"properties": [
|
|
2122
|
+
{
|
|
2123
|
+
"key": "email",
|
|
2124
|
+
"type": "person",
|
|
2125
|
+
"value": ["pineapple@example.com"],
|
|
2126
|
+
"operator": "exact",
|
|
2127
|
+
}
|
|
2128
|
+
],
|
|
2129
|
+
"rollout_percentage": 100,
|
|
2130
|
+
"variant": "pineapple",
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
"properties": [
|
|
2134
|
+
{
|
|
2135
|
+
"key": "email",
|
|
2136
|
+
"type": "person",
|
|
2137
|
+
"value": ["mango@example.com"],
|
|
2138
|
+
"operator": "exact",
|
|
2139
|
+
}
|
|
2140
|
+
],
|
|
2141
|
+
"rollout_percentage": 100,
|
|
2142
|
+
"variant": "mango",
|
|
2143
|
+
},
|
|
2144
|
+
{
|
|
2145
|
+
"properties": [
|
|
2146
|
+
{
|
|
2147
|
+
"key": "email",
|
|
2148
|
+
"type": "person",
|
|
2149
|
+
"value": ["papaya@example.com"],
|
|
2150
|
+
"operator": "exact",
|
|
2151
|
+
}
|
|
2152
|
+
],
|
|
2153
|
+
"rollout_percentage": 100,
|
|
2154
|
+
"variant": "papaya",
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
"properties": [
|
|
2158
|
+
{
|
|
2159
|
+
"key": "email",
|
|
2160
|
+
"type": "person",
|
|
2161
|
+
"value": ["kiwi@example.com"],
|
|
2162
|
+
"operator": "exact",
|
|
2163
|
+
}
|
|
2164
|
+
],
|
|
2165
|
+
"rollout_percentage": 100,
|
|
2166
|
+
"variant": "kiwi",
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
"properties": [],
|
|
2170
|
+
"rollout_percentage": 0, # Force default to false for unknown emails
|
|
2171
|
+
},
|
|
2172
|
+
],
|
|
2173
|
+
"multivariate": {
|
|
2174
|
+
"variants": [
|
|
2175
|
+
{"key": "pineapple", "rollout_percentage": 25},
|
|
2176
|
+
{"key": "mango", "rollout_percentage": 25},
|
|
2177
|
+
{"key": "papaya", "rollout_percentage": 25},
|
|
2178
|
+
{"key": "kiwi", "rollout_percentage": 25},
|
|
2179
|
+
]
|
|
2180
|
+
},
|
|
2181
|
+
},
|
|
2182
|
+
},
|
|
2183
|
+
# Intermediate flag: multivariate with color variants, depends on fruit
|
|
2184
|
+
{
|
|
2185
|
+
"id": 467,
|
|
2186
|
+
"name": "Multivariate Intermediate Flag (Depends on fruit)",
|
|
2187
|
+
"key": "multivariate-intermediate-flag",
|
|
2188
|
+
"active": True,
|
|
2189
|
+
"rollout_percentage": 100,
|
|
2190
|
+
"filters": {
|
|
2191
|
+
"groups": [
|
|
2192
|
+
{
|
|
2193
|
+
"properties": [
|
|
2194
|
+
{
|
|
2195
|
+
"key": "multivariate-leaf-flag",
|
|
2196
|
+
"type": "flag",
|
|
2197
|
+
"value": "pineapple",
|
|
2198
|
+
"operator": "flag_evaluates_to",
|
|
2199
|
+
"dependency_chain": ["multivariate-leaf-flag"],
|
|
2200
|
+
}
|
|
2201
|
+
],
|
|
2202
|
+
"rollout_percentage": 100,
|
|
2203
|
+
"variant": "blue",
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
"properties": [
|
|
2207
|
+
{
|
|
2208
|
+
"key": "multivariate-leaf-flag",
|
|
2209
|
+
"type": "flag",
|
|
2210
|
+
"value": "mango",
|
|
2211
|
+
"operator": "flag_evaluates_to",
|
|
2212
|
+
"dependency_chain": ["multivariate-leaf-flag"],
|
|
2213
|
+
}
|
|
2214
|
+
],
|
|
2215
|
+
"rollout_percentage": 100,
|
|
2216
|
+
"variant": "red",
|
|
2217
|
+
},
|
|
2218
|
+
],
|
|
2219
|
+
"multivariate": {
|
|
2220
|
+
"variants": [
|
|
2221
|
+
{"key": "blue", "rollout_percentage": 100},
|
|
2222
|
+
{"key": "red", "rollout_percentage": 0},
|
|
2223
|
+
{"key": "green", "rollout_percentage": 0},
|
|
2224
|
+
{"key": "black", "rollout_percentage": 0},
|
|
2225
|
+
]
|
|
2226
|
+
},
|
|
2227
|
+
},
|
|
2228
|
+
},
|
|
2229
|
+
# Root flag: multivariate with show variants, depends on color
|
|
2230
|
+
{
|
|
2231
|
+
"id": 468,
|
|
2232
|
+
"name": "Multivariate Root Flag (Depends on color)",
|
|
2233
|
+
"key": "multivariate-root-flag",
|
|
2234
|
+
"active": True,
|
|
2235
|
+
"rollout_percentage": 100,
|
|
2236
|
+
"filters": {
|
|
2237
|
+
"groups": [
|
|
2238
|
+
{
|
|
2239
|
+
"properties": [
|
|
2240
|
+
{
|
|
2241
|
+
"key": "multivariate-intermediate-flag",
|
|
2242
|
+
"type": "flag",
|
|
2243
|
+
"value": "blue",
|
|
2244
|
+
"operator": "flag_evaluates_to",
|
|
2245
|
+
"dependency_chain": [
|
|
2246
|
+
"multivariate-leaf-flag",
|
|
2247
|
+
"multivariate-intermediate-flag",
|
|
2248
|
+
],
|
|
2249
|
+
}
|
|
2250
|
+
],
|
|
2251
|
+
"rollout_percentage": 100,
|
|
2252
|
+
"variant": "breaking-bad",
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
"properties": [
|
|
2256
|
+
{
|
|
2257
|
+
"key": "multivariate-intermediate-flag",
|
|
2258
|
+
"type": "flag",
|
|
2259
|
+
"value": "red",
|
|
2260
|
+
"operator": "flag_evaluates_to",
|
|
2261
|
+
"dependency_chain": [
|
|
2262
|
+
"multivariate-leaf-flag",
|
|
2263
|
+
"multivariate-intermediate-flag",
|
|
2264
|
+
],
|
|
2265
|
+
}
|
|
2266
|
+
],
|
|
2267
|
+
"rollout_percentage": 100,
|
|
2268
|
+
"variant": "the-wire",
|
|
2269
|
+
},
|
|
2270
|
+
],
|
|
2271
|
+
"multivariate": {
|
|
2272
|
+
"variants": [
|
|
2273
|
+
{"key": "breaking-bad", "rollout_percentage": 100},
|
|
2274
|
+
{"key": "the-wire", "rollout_percentage": 0},
|
|
2275
|
+
{"key": "game-of-thrones", "rollout_percentage": 0},
|
|
2276
|
+
{"key": "the-expanse", "rollout_percentage": 0},
|
|
2277
|
+
]
|
|
2278
|
+
},
|
|
2279
|
+
},
|
|
2280
|
+
},
|
|
2281
|
+
]
|
|
2282
|
+
|
|
2283
|
+
# Test successful pineapple -> blue -> breaking-bad chain
|
|
2284
|
+
leaf_result = client.get_feature_flag(
|
|
2285
|
+
"multivariate-leaf-flag",
|
|
2286
|
+
"test-user",
|
|
2287
|
+
person_properties={"email": "pineapple@example.com"},
|
|
2288
|
+
)
|
|
2289
|
+
intermediate_result = client.get_feature_flag(
|
|
2290
|
+
"multivariate-intermediate-flag",
|
|
2291
|
+
"test-user",
|
|
2292
|
+
person_properties={"email": "pineapple@example.com"},
|
|
2293
|
+
)
|
|
2294
|
+
root_result = client.get_feature_flag(
|
|
2295
|
+
"multivariate-root-flag",
|
|
2296
|
+
"test-user",
|
|
2297
|
+
person_properties={"email": "pineapple@example.com"},
|
|
2298
|
+
)
|
|
2299
|
+
|
|
2300
|
+
self.assertEqual(leaf_result, "pineapple")
|
|
2301
|
+
self.assertEqual(intermediate_result, "blue")
|
|
2302
|
+
self.assertEqual(root_result, "breaking-bad")
|
|
2303
|
+
|
|
2304
|
+
# Test successful mango -> red -> the-wire chain
|
|
2305
|
+
mango_leaf_result = client.get_feature_flag(
|
|
2306
|
+
"multivariate-leaf-flag",
|
|
2307
|
+
"test-user",
|
|
2308
|
+
person_properties={"email": "mango@example.com"},
|
|
2309
|
+
)
|
|
2310
|
+
mango_intermediate_result = client.get_feature_flag(
|
|
2311
|
+
"multivariate-intermediate-flag",
|
|
2312
|
+
"test-user",
|
|
2313
|
+
person_properties={"email": "mango@example.com"},
|
|
2314
|
+
)
|
|
2315
|
+
mango_root_result = client.get_feature_flag(
|
|
2316
|
+
"multivariate-root-flag",
|
|
2317
|
+
"test-user",
|
|
2318
|
+
person_properties={"email": "mango@example.com"},
|
|
2319
|
+
)
|
|
2320
|
+
|
|
2321
|
+
self.assertEqual(mango_leaf_result, "mango")
|
|
2322
|
+
self.assertEqual(mango_intermediate_result, "red")
|
|
2323
|
+
self.assertEqual(mango_root_result, "the-wire")
|
|
2324
|
+
|
|
2325
|
+
# Test broken chain - user without matching email gets default/false results
|
|
2326
|
+
unknown_leaf_result = client.get_feature_flag(
|
|
2327
|
+
"multivariate-leaf-flag",
|
|
2328
|
+
"test-user",
|
|
2329
|
+
person_properties={"email": "unknown@example.com"},
|
|
2330
|
+
)
|
|
2331
|
+
unknown_intermediate_result = client.get_feature_flag(
|
|
2332
|
+
"multivariate-intermediate-flag",
|
|
2333
|
+
"test-user",
|
|
2334
|
+
person_properties={"email": "unknown@example.com"},
|
|
2335
|
+
)
|
|
2336
|
+
unknown_root_result = client.get_feature_flag(
|
|
2337
|
+
"multivariate-root-flag",
|
|
2338
|
+
"test-user",
|
|
2339
|
+
person_properties={"email": "unknown@example.com"},
|
|
2340
|
+
)
|
|
2341
|
+
|
|
2342
|
+
self.assertEqual(
|
|
2343
|
+
unknown_leaf_result, False
|
|
2344
|
+
) # No matching email -> null variant -> false
|
|
2345
|
+
self.assertEqual(unknown_intermediate_result, False) # Dependency not satisfied
|
|
2346
|
+
self.assertEqual(unknown_root_result, False) # Chain broken
|
|
2347
|
+
|
|
1838
2348
|
@mock.patch("posthog.client.Poller")
|
|
1839
2349
|
@mock.patch("posthog.client.get")
|
|
1840
2350
|
def test_load_feature_flags(self, patch_get, patch_poll):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/anthropic/anthropic_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/langchain/callbacks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/ai/openai/openai_providers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_exception_capture.py
RENAMED
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_feature_flag.py
RENAMED
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_feature_flag_result.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics/test/test_size_limited_dict.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{posthoganalytics-6.6.1 → posthoganalytics-6.7.0}/posthoganalytics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|