posthoganalytics 6.6.0__py3-none-any.whl → 6.7.0__py3-none-any.whl
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/ai/anthropic/anthropic.py +1 -1
- posthoganalytics/ai/openai/openai_async.py +1 -1
- posthoganalytics/client.py +25 -13
- posthoganalytics/feature_flags.py +230 -20
- posthoganalytics/test/test_feature_flags.py +939 -20
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.6.0.dist-info → posthoganalytics-6.7.0.dist-info}/METADATA +1 -1
- {posthoganalytics-6.6.0.dist-info → posthoganalytics-6.7.0.dist-info}/RECORD +11 -11
- {posthoganalytics-6.6.0.dist-info → posthoganalytics-6.7.0.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.6.0.dist-info → posthoganalytics-6.7.0.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.6.0.dist-info → posthoganalytics-6.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1361,6 +1361,8 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1361
1361
|
def test_feature_flags_with_flag_dependencies(
|
|
1362
1362
|
self, patch_get, patch_flags, mock_log
|
|
1363
1363
|
):
|
|
1364
|
+
# Mock remote flags call to return empty for this flag (fallback returns None)
|
|
1365
|
+
patch_flags.return_value = {"featureFlags": {}}
|
|
1364
1366
|
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1365
1367
|
client.feature_flags = [
|
|
1366
1368
|
{
|
|
@@ -1374,9 +1376,10 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1374
1376
|
"properties": [
|
|
1375
1377
|
{
|
|
1376
1378
|
"key": "beta-feature",
|
|
1377
|
-
"operator": "
|
|
1379
|
+
"operator": "flag_evaluates_to",
|
|
1378
1380
|
"value": True,
|
|
1379
1381
|
"type": "flag",
|
|
1382
|
+
"dependency_chain": ["beta-feature"],
|
|
1380
1383
|
},
|
|
1381
1384
|
{
|
|
1382
1385
|
"key": "email",
|
|
@@ -1392,39 +1395,955 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
1392
1395
|
}
|
|
1393
1396
|
]
|
|
1394
1397
|
|
|
1395
|
-
# Test that flag evaluation
|
|
1396
|
-
# The flag
|
|
1397
|
-
# Since
|
|
1398
|
-
#
|
|
1398
|
+
# Test that flag evaluation handles flag dependencies properly
|
|
1399
|
+
# The flag has a dependency on "beta-feature" which doesn't exist locally
|
|
1400
|
+
# Since the dependency doesn't exist, local evaluation should fail and fall back to remote
|
|
1401
|
+
# Remote returns empty result, so final result is None
|
|
1399
1402
|
feature_flag_match = client.get_feature_flag(
|
|
1400
1403
|
"flag-with-dependencies",
|
|
1401
1404
|
"test-user",
|
|
1402
1405
|
person_properties={"email": "test@example.com"},
|
|
1403
1406
|
)
|
|
1404
|
-
self.
|
|
1405
|
-
self.assertEqual(patch_flags.call_count,
|
|
1407
|
+
self.assertIsNone(feature_flag_match)
|
|
1408
|
+
self.assertEqual(patch_flags.call_count, 1)
|
|
1406
1409
|
self.assertEqual(patch_get.call_count, 0)
|
|
1407
1410
|
|
|
1408
|
-
#
|
|
1409
|
-
mock_log.warning.assert_called_with(
|
|
1410
|
-
"Flag dependency filters are not supported in local evaluation. "
|
|
1411
|
-
"Skipping condition for flag '%s' with dependency on flag '%s'",
|
|
1412
|
-
"flag-with-dependencies",
|
|
1413
|
-
"beta-feature",
|
|
1414
|
-
)
|
|
1415
|
-
|
|
1416
|
-
# Test with email that doesn't match
|
|
1411
|
+
# Test with email that doesn't match (should also fall back to remote due to missing dependency)
|
|
1417
1412
|
feature_flag_match = client.get_feature_flag(
|
|
1418
1413
|
"flag-with-dependencies",
|
|
1419
1414
|
"test-user-2",
|
|
1420
1415
|
person_properties={"email": "test@other.com"},
|
|
1421
1416
|
)
|
|
1422
|
-
self.
|
|
1423
|
-
self.assertEqual(patch_flags.call_count,
|
|
1417
|
+
self.assertIsNone(feature_flag_match)
|
|
1418
|
+
self.assertEqual(patch_flags.call_count, 2) # Called twice now
|
|
1424
1419
|
self.assertEqual(patch_get.call_count, 0)
|
|
1425
1420
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1421
|
+
@mock.patch("posthog.client.flags")
|
|
1422
|
+
@mock.patch("posthog.client.get")
|
|
1423
|
+
def test_flag_dependencies_simple_chain(self, patch_get, patch_flags):
|
|
1424
|
+
"""Test basic flag dependency: flag-b depends on flag-a"""
|
|
1425
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1426
|
+
client.feature_flags = [
|
|
1427
|
+
{
|
|
1428
|
+
"id": 1,
|
|
1429
|
+
"name": "Flag A",
|
|
1430
|
+
"key": "flag-a",
|
|
1431
|
+
"active": True,
|
|
1432
|
+
"filters": {
|
|
1433
|
+
"groups": [
|
|
1434
|
+
{
|
|
1435
|
+
"properties": [
|
|
1436
|
+
{
|
|
1437
|
+
"key": "email",
|
|
1438
|
+
"operator": "icontains",
|
|
1439
|
+
"value": "@example.com",
|
|
1440
|
+
"type": "person",
|
|
1441
|
+
}
|
|
1442
|
+
],
|
|
1443
|
+
"rollout_percentage": 100,
|
|
1444
|
+
}
|
|
1445
|
+
],
|
|
1446
|
+
},
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
"id": 2,
|
|
1450
|
+
"name": "Flag B",
|
|
1451
|
+
"key": "flag-b",
|
|
1452
|
+
"active": True,
|
|
1453
|
+
"filters": {
|
|
1454
|
+
"groups": [
|
|
1455
|
+
{
|
|
1456
|
+
"properties": [
|
|
1457
|
+
{
|
|
1458
|
+
"key": "flag-a",
|
|
1459
|
+
"operator": "flag_evaluates_to",
|
|
1460
|
+
"value": True,
|
|
1461
|
+
"type": "flag",
|
|
1462
|
+
"dependency_chain": ["flag-a"],
|
|
1463
|
+
}
|
|
1464
|
+
],
|
|
1465
|
+
"rollout_percentage": 100,
|
|
1466
|
+
}
|
|
1467
|
+
],
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
]
|
|
1471
|
+
|
|
1472
|
+
# Test when dependency is satisfied
|
|
1473
|
+
result = client.get_feature_flag(
|
|
1474
|
+
"flag-b",
|
|
1475
|
+
"test-user",
|
|
1476
|
+
person_properties={"email": "test@example.com"},
|
|
1477
|
+
)
|
|
1478
|
+
self.assertEqual(result, True)
|
|
1479
|
+
|
|
1480
|
+
# Test when dependency is not satisfied
|
|
1481
|
+
result = client.get_feature_flag(
|
|
1482
|
+
"flag-b",
|
|
1483
|
+
"test-user-2",
|
|
1484
|
+
person_properties={"email": "test@other.com"},
|
|
1485
|
+
)
|
|
1486
|
+
self.assertEqual(result, False)
|
|
1487
|
+
|
|
1488
|
+
@mock.patch("posthog.client.flags")
|
|
1489
|
+
@mock.patch("posthog.client.get")
|
|
1490
|
+
def test_flag_dependencies_circular_dependency(self, patch_get, patch_flags):
|
|
1491
|
+
"""Test circular dependency handling: flag-a depends on flag-b, flag-b depends on flag-a"""
|
|
1492
|
+
# Mock remote flags call to return empty for these flags (fallback returns None)
|
|
1493
|
+
patch_flags.return_value = {"featureFlags": {}}
|
|
1494
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1495
|
+
client.feature_flags = [
|
|
1496
|
+
{
|
|
1497
|
+
"id": 1,
|
|
1498
|
+
"name": "Flag A",
|
|
1499
|
+
"key": "flag-a",
|
|
1500
|
+
"active": True,
|
|
1501
|
+
"filters": {
|
|
1502
|
+
"groups": [
|
|
1503
|
+
{
|
|
1504
|
+
"properties": [
|
|
1505
|
+
{
|
|
1506
|
+
"key": "flag-b",
|
|
1507
|
+
"operator": "flag_evaluates_to",
|
|
1508
|
+
"value": True,
|
|
1509
|
+
"type": "flag",
|
|
1510
|
+
"dependency_chain": [], # Empty chain indicates circular dependency
|
|
1511
|
+
}
|
|
1512
|
+
],
|
|
1513
|
+
"rollout_percentage": 100,
|
|
1514
|
+
}
|
|
1515
|
+
],
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
"id": 2,
|
|
1520
|
+
"name": "Flag B",
|
|
1521
|
+
"key": "flag-b",
|
|
1522
|
+
"active": True,
|
|
1523
|
+
"filters": {
|
|
1524
|
+
"groups": [
|
|
1525
|
+
{
|
|
1526
|
+
"properties": [
|
|
1527
|
+
{
|
|
1528
|
+
"key": "flag-a",
|
|
1529
|
+
"operator": "flag_evaluates_to",
|
|
1530
|
+
"value": True,
|
|
1531
|
+
"type": "flag",
|
|
1532
|
+
"dependency_chain": [], # Empty chain indicates circular dependency
|
|
1533
|
+
}
|
|
1534
|
+
],
|
|
1535
|
+
"rollout_percentage": 100,
|
|
1536
|
+
}
|
|
1537
|
+
],
|
|
1538
|
+
},
|
|
1539
|
+
},
|
|
1540
|
+
]
|
|
1541
|
+
|
|
1542
|
+
# Both flags should fall back to remote evaluation due to circular dependency
|
|
1543
|
+
# Since we're not mocking the remote call, both should return None
|
|
1544
|
+
result_a = client.get_feature_flag("flag-a", "test-user")
|
|
1545
|
+
self.assertIsNone(result_a)
|
|
1546
|
+
|
|
1547
|
+
result_b = client.get_feature_flag("flag-b", "test-user")
|
|
1548
|
+
self.assertIsNone(result_b)
|
|
1549
|
+
|
|
1550
|
+
@mock.patch("posthog.client.flags")
|
|
1551
|
+
@mock.patch("posthog.client.get")
|
|
1552
|
+
def test_flag_dependencies_missing_flag(self, patch_get, patch_flags):
|
|
1553
|
+
"""Test handling of missing flag dependency"""
|
|
1554
|
+
# Mock remote flags call to return empty for this flag (fallback returns None)
|
|
1555
|
+
patch_flags.return_value = {"featureFlags": {}}
|
|
1556
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1557
|
+
client.feature_flags = [
|
|
1558
|
+
{
|
|
1559
|
+
"id": 1,
|
|
1560
|
+
"name": "Flag A",
|
|
1561
|
+
"key": "flag-a",
|
|
1562
|
+
"active": True,
|
|
1563
|
+
"filters": {
|
|
1564
|
+
"groups": [
|
|
1565
|
+
{
|
|
1566
|
+
"properties": [
|
|
1567
|
+
{
|
|
1568
|
+
"key": "non-existent-flag",
|
|
1569
|
+
"operator": "flag_evaluates_to",
|
|
1570
|
+
"value": True,
|
|
1571
|
+
"type": "flag",
|
|
1572
|
+
"dependency_chain": ["non-existent-flag"],
|
|
1573
|
+
}
|
|
1574
|
+
],
|
|
1575
|
+
"rollout_percentage": 100,
|
|
1576
|
+
}
|
|
1577
|
+
],
|
|
1578
|
+
},
|
|
1579
|
+
}
|
|
1580
|
+
]
|
|
1581
|
+
|
|
1582
|
+
# Should fall back to remote evaluation because dependency doesn't exist
|
|
1583
|
+
# Since we're not mocking the remote call, should return None
|
|
1584
|
+
result = client.get_feature_flag("flag-a", "test-user")
|
|
1585
|
+
self.assertIsNone(result)
|
|
1586
|
+
|
|
1587
|
+
@mock.patch("posthog.client.flags")
|
|
1588
|
+
@mock.patch("posthog.client.get")
|
|
1589
|
+
def test_flag_dependencies_complex_chain(self, patch_get, patch_flags):
|
|
1590
|
+
"""Test complex dependency chain: flag-d -> flag-c -> [flag-a, flag-b]"""
|
|
1591
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1592
|
+
client.feature_flags = [
|
|
1593
|
+
{
|
|
1594
|
+
"id": 1,
|
|
1595
|
+
"name": "Flag A",
|
|
1596
|
+
"key": "flag-a",
|
|
1597
|
+
"active": True,
|
|
1598
|
+
"filters": {
|
|
1599
|
+
"groups": [
|
|
1600
|
+
{
|
|
1601
|
+
"properties": [],
|
|
1602
|
+
"rollout_percentage": 100,
|
|
1603
|
+
}
|
|
1604
|
+
],
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
"id": 2,
|
|
1609
|
+
"name": "Flag B",
|
|
1610
|
+
"key": "flag-b",
|
|
1611
|
+
"active": True,
|
|
1612
|
+
"filters": {
|
|
1613
|
+
"groups": [
|
|
1614
|
+
{
|
|
1615
|
+
"properties": [],
|
|
1616
|
+
"rollout_percentage": 100,
|
|
1617
|
+
}
|
|
1618
|
+
],
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
"id": 3,
|
|
1623
|
+
"name": "Flag C",
|
|
1624
|
+
"key": "flag-c",
|
|
1625
|
+
"active": True,
|
|
1626
|
+
"filters": {
|
|
1627
|
+
"groups": [
|
|
1628
|
+
{
|
|
1629
|
+
"properties": [
|
|
1630
|
+
{
|
|
1631
|
+
"key": "flag-a",
|
|
1632
|
+
"operator": "flag_evaluates_to",
|
|
1633
|
+
"value": True,
|
|
1634
|
+
"type": "flag",
|
|
1635
|
+
"dependency_chain": ["flag-a"],
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
"key": "flag-b",
|
|
1639
|
+
"operator": "flag_evaluates_to",
|
|
1640
|
+
"value": True,
|
|
1641
|
+
"type": "flag",
|
|
1642
|
+
"dependency_chain": ["flag-b"],
|
|
1643
|
+
},
|
|
1644
|
+
],
|
|
1645
|
+
"rollout_percentage": 100,
|
|
1646
|
+
}
|
|
1647
|
+
],
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
"id": 4,
|
|
1652
|
+
"name": "Flag D",
|
|
1653
|
+
"key": "flag-d",
|
|
1654
|
+
"active": True,
|
|
1655
|
+
"filters": {
|
|
1656
|
+
"groups": [
|
|
1657
|
+
{
|
|
1658
|
+
"properties": [
|
|
1659
|
+
{
|
|
1660
|
+
"key": "flag-c",
|
|
1661
|
+
"operator": "flag_evaluates_to",
|
|
1662
|
+
"value": True,
|
|
1663
|
+
"type": "flag",
|
|
1664
|
+
"dependency_chain": ["flag-a", "flag-b", "flag-c"],
|
|
1665
|
+
}
|
|
1666
|
+
],
|
|
1667
|
+
"rollout_percentage": 100,
|
|
1668
|
+
}
|
|
1669
|
+
],
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
]
|
|
1673
|
+
|
|
1674
|
+
# All dependencies satisfied - should return True
|
|
1675
|
+
result = client.get_feature_flag("flag-d", "test-user")
|
|
1676
|
+
self.assertEqual(result, True)
|
|
1677
|
+
|
|
1678
|
+
# Make flag-a inactive - should break the chain
|
|
1679
|
+
client.feature_flags[0]["active"] = False
|
|
1680
|
+
result = client.get_feature_flag("flag-d", "test-user")
|
|
1681
|
+
self.assertEqual(result, False)
|
|
1682
|
+
|
|
1683
|
+
@mock.patch("posthog.client.flags")
|
|
1684
|
+
@mock.patch("posthog.client.get")
|
|
1685
|
+
def test_flag_dependencies_mixed_conditions(self, patch_get, patch_flags):
|
|
1686
|
+
"""Test flag dependency mixed with other property conditions"""
|
|
1687
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1688
|
+
client.feature_flags = [
|
|
1689
|
+
{
|
|
1690
|
+
"id": 1,
|
|
1691
|
+
"name": "Base Flag",
|
|
1692
|
+
"key": "base-flag",
|
|
1693
|
+
"active": True,
|
|
1694
|
+
"filters": {
|
|
1695
|
+
"groups": [
|
|
1696
|
+
{
|
|
1697
|
+
"properties": [],
|
|
1698
|
+
"rollout_percentage": 100,
|
|
1699
|
+
}
|
|
1700
|
+
],
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
"id": 2,
|
|
1705
|
+
"name": "Mixed Flag",
|
|
1706
|
+
"key": "mixed-flag",
|
|
1707
|
+
"active": True,
|
|
1708
|
+
"filters": {
|
|
1709
|
+
"groups": [
|
|
1710
|
+
{
|
|
1711
|
+
"properties": [
|
|
1712
|
+
{
|
|
1713
|
+
"key": "base-flag",
|
|
1714
|
+
"operator": "flag_evaluates_to",
|
|
1715
|
+
"value": True,
|
|
1716
|
+
"type": "flag",
|
|
1717
|
+
"dependency_chain": ["base-flag"],
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
"key": "email",
|
|
1721
|
+
"operator": "icontains",
|
|
1722
|
+
"value": "@example.com",
|
|
1723
|
+
"type": "person",
|
|
1724
|
+
},
|
|
1725
|
+
],
|
|
1726
|
+
"rollout_percentage": 100,
|
|
1727
|
+
}
|
|
1728
|
+
],
|
|
1729
|
+
},
|
|
1730
|
+
},
|
|
1731
|
+
]
|
|
1732
|
+
|
|
1733
|
+
# Both flag dependency and email condition satisfied
|
|
1734
|
+
result = client.get_feature_flag(
|
|
1735
|
+
"mixed-flag",
|
|
1736
|
+
"test-user",
|
|
1737
|
+
person_properties={"email": "test@example.com"},
|
|
1738
|
+
)
|
|
1739
|
+
self.assertEqual(result, True)
|
|
1740
|
+
|
|
1741
|
+
# Flag dependency satisfied but email condition not satisfied
|
|
1742
|
+
result = client.get_feature_flag(
|
|
1743
|
+
"mixed-flag",
|
|
1744
|
+
"test-user-2",
|
|
1745
|
+
person_properties={"email": "test@other.com"},
|
|
1746
|
+
)
|
|
1747
|
+
self.assertEqual(result, False)
|
|
1748
|
+
|
|
1749
|
+
# Email condition satisfied but flag dependency not satisfied
|
|
1750
|
+
client.feature_flags[0]["active"] = False
|
|
1751
|
+
result = client.get_feature_flag(
|
|
1752
|
+
"mixed-flag",
|
|
1753
|
+
"test-user-3",
|
|
1754
|
+
person_properties={"email": "test@example.com"},
|
|
1755
|
+
)
|
|
1756
|
+
self.assertEqual(result, False)
|
|
1757
|
+
|
|
1758
|
+
@mock.patch("posthog.client.flags")
|
|
1759
|
+
@mock.patch("posthog.client.get")
|
|
1760
|
+
def test_flag_dependencies_malformed_chain(self, patch_get, patch_flags):
|
|
1761
|
+
"""Test handling of malformed dependency chains"""
|
|
1762
|
+
# Mock remote flags call to return empty for this flag (fallback returns None)
|
|
1763
|
+
patch_flags.return_value = {"featureFlags": {}}
|
|
1764
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
1765
|
+
client.feature_flags = [
|
|
1766
|
+
{
|
|
1767
|
+
"id": 1,
|
|
1768
|
+
"name": "Base Flag",
|
|
1769
|
+
"key": "base-flag",
|
|
1770
|
+
"active": True,
|
|
1771
|
+
"filters": {
|
|
1772
|
+
"groups": [
|
|
1773
|
+
{
|
|
1774
|
+
"properties": [],
|
|
1775
|
+
"rollout_percentage": 100,
|
|
1776
|
+
}
|
|
1777
|
+
],
|
|
1778
|
+
},
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
"id": 2,
|
|
1782
|
+
"name": "Missing Chain Flag",
|
|
1783
|
+
"key": "missing-chain-flag",
|
|
1784
|
+
"active": True,
|
|
1785
|
+
"filters": {
|
|
1786
|
+
"groups": [
|
|
1787
|
+
{
|
|
1788
|
+
"properties": [
|
|
1789
|
+
{
|
|
1790
|
+
"key": "base-flag",
|
|
1791
|
+
"operator": "flag_evaluates_to",
|
|
1792
|
+
"value": True,
|
|
1793
|
+
"type": "flag",
|
|
1794
|
+
# No dependency_chain property - should evaluate as inconclusive
|
|
1795
|
+
}
|
|
1796
|
+
],
|
|
1797
|
+
"rollout_percentage": 100,
|
|
1798
|
+
}
|
|
1799
|
+
],
|
|
1800
|
+
},
|
|
1801
|
+
},
|
|
1802
|
+
]
|
|
1803
|
+
|
|
1804
|
+
# Should fall back to remote evaluation when dependency_chain is missing
|
|
1805
|
+
# Since we're not mocking the remote call, should return None
|
|
1806
|
+
result = client.get_feature_flag("missing-chain-flag", "test-user")
|
|
1807
|
+
self.assertIsNone(result)
|
|
1808
|
+
|
|
1809
|
+
def test_flag_dependencies_without_context_raises_inconclusive(self):
|
|
1810
|
+
"""Test that missing flags_by_key raises InconclusiveMatchError"""
|
|
1811
|
+
from posthoganalytics.feature_flags import (
|
|
1812
|
+
evaluate_flag_dependency,
|
|
1813
|
+
InconclusiveMatchError,
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
property_with_flag_dep = {
|
|
1817
|
+
"key": "some-flag",
|
|
1818
|
+
"operator": "flag_evaluates_to",
|
|
1819
|
+
"value": True,
|
|
1820
|
+
"type": "flag",
|
|
1821
|
+
"dependency_chain": ["some-flag"],
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
# Should raise InconclusiveMatchError when flags_by_key is None
|
|
1825
|
+
with self.assertRaises(InconclusiveMatchError) as cm:
|
|
1826
|
+
evaluate_flag_dependency(
|
|
1827
|
+
property_with_flag_dep,
|
|
1828
|
+
flags_by_key=None, # This should trigger the error
|
|
1829
|
+
evaluation_cache={},
|
|
1830
|
+
distinct_id="test-user",
|
|
1831
|
+
properties={},
|
|
1832
|
+
cohort_properties={},
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
self.assertIn("Cannot evaluate flag dependency", str(cm.exception))
|
|
1836
|
+
self.assertIn("some-flag", str(cm.exception))
|
|
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
|
|
1428
2347
|
|
|
1429
2348
|
@mock.patch("posthog.client.Poller")
|
|
1430
2349
|
@mock.patch("posthog.client.get")
|