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.
@@ -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": "exact",
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 doesn't fail when encountering a flag dependency
1396
- # The flag should evaluate based on other conditions (email contains @example.com)
1397
- # Since flag dependencies aren't implemented, it should skip the flag condition
1398
- # and evaluate based on the email condition only
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.assertEqual(feature_flag_match, True)
1405
- self.assertEqual(patch_flags.call_count, 0)
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
- # Verify warning was logged for flag dependency
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.assertEqual(feature_flag_match, False)
1423
- self.assertEqual(patch_flags.call_count, 0)
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
- # Verify warning was logged again for the second evaluation
1427
- self.assertEqual(mock_log.warning.call_count, 2)
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")