posthoganalytics 6.4.1__py3-none-any.whl → 6.6.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/client.py +73 -14
- posthoganalytics/test/test_client.py +69 -0
- posthoganalytics/test/test_feature_flags.py +118 -7
- posthoganalytics/types.py +3 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.4.1.dist-info → posthoganalytics-6.6.0.dist-info}/METADATA +1 -1
- {posthoganalytics-6.4.1.dist-info → posthoganalytics-6.6.0.dist-info}/RECORD +10 -10
- {posthoganalytics-6.4.1.dist-info → posthoganalytics-6.6.0.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.4.1.dist-info → posthoganalytics-6.6.0.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.4.1.dist-info → posthoganalytics-6.6.0.dist-info}/top_level.txt +0 -0
posthoganalytics/client.py
CHANGED
|
@@ -44,6 +44,7 @@ from posthoganalytics.types import (
|
|
|
44
44
|
FlagsAndPayloads,
|
|
45
45
|
FlagsResponse,
|
|
46
46
|
FlagValue,
|
|
47
|
+
SendFeatureFlagsOptions,
|
|
47
48
|
normalize_flags_response,
|
|
48
49
|
to_flags_and_payloads,
|
|
49
50
|
to_payloads,
|
|
@@ -87,6 +88,7 @@ def add_context_tags(properties):
|
|
|
87
88
|
current_context = _get_current_context()
|
|
88
89
|
if current_context:
|
|
89
90
|
context_tags = current_context.collect_tags()
|
|
91
|
+
properties["$context_tags"] = set(context_tags.keys())
|
|
90
92
|
# We want explicitly passed properties to override context tags
|
|
91
93
|
context_tags.update(properties)
|
|
92
94
|
properties = context_tags
|
|
@@ -312,6 +314,7 @@ class Client(object):
|
|
|
312
314
|
person_properties=None,
|
|
313
315
|
group_properties=None,
|
|
314
316
|
disable_geoip=None,
|
|
317
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
315
318
|
) -> dict[str, Union[bool, str]]:
|
|
316
319
|
"""
|
|
317
320
|
Get feature flag variants for a user by calling decide.
|
|
@@ -322,12 +325,19 @@ class Client(object):
|
|
|
322
325
|
person_properties: A dictionary of person properties.
|
|
323
326
|
group_properties: A dictionary of group properties.
|
|
324
327
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
328
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
329
|
+
only these flags will be evaluated, improving performance.
|
|
325
330
|
|
|
326
331
|
Category:
|
|
327
332
|
Feature Flags
|
|
328
333
|
"""
|
|
329
334
|
resp_data = self.get_flags_decision(
|
|
330
|
-
distinct_id,
|
|
335
|
+
distinct_id,
|
|
336
|
+
groups,
|
|
337
|
+
person_properties,
|
|
338
|
+
group_properties,
|
|
339
|
+
disable_geoip,
|
|
340
|
+
flag_keys_to_evaluate,
|
|
331
341
|
)
|
|
332
342
|
return to_values(resp_data) or {}
|
|
333
343
|
|
|
@@ -338,6 +348,7 @@ class Client(object):
|
|
|
338
348
|
person_properties=None,
|
|
339
349
|
group_properties=None,
|
|
340
350
|
disable_geoip=None,
|
|
351
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
341
352
|
) -> dict[str, str]:
|
|
342
353
|
"""
|
|
343
354
|
Get feature flag payloads for a user by calling decide.
|
|
@@ -348,6 +359,8 @@ class Client(object):
|
|
|
348
359
|
person_properties: A dictionary of person properties.
|
|
349
360
|
group_properties: A dictionary of group properties.
|
|
350
361
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
362
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
363
|
+
only these flags will be evaluated, improving performance.
|
|
351
364
|
|
|
352
365
|
Examples:
|
|
353
366
|
```python
|
|
@@ -358,7 +371,12 @@ class Client(object):
|
|
|
358
371
|
Feature Flags
|
|
359
372
|
"""
|
|
360
373
|
resp_data = self.get_flags_decision(
|
|
361
|
-
distinct_id,
|
|
374
|
+
distinct_id,
|
|
375
|
+
groups,
|
|
376
|
+
person_properties,
|
|
377
|
+
group_properties,
|
|
378
|
+
disable_geoip,
|
|
379
|
+
flag_keys_to_evaluate,
|
|
362
380
|
)
|
|
363
381
|
return to_payloads(resp_data) or {}
|
|
364
382
|
|
|
@@ -369,6 +387,7 @@ class Client(object):
|
|
|
369
387
|
person_properties=None,
|
|
370
388
|
group_properties=None,
|
|
371
389
|
disable_geoip=None,
|
|
390
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
372
391
|
) -> FlagsAndPayloads:
|
|
373
392
|
"""
|
|
374
393
|
Get feature flags and payloads for a user by calling decide.
|
|
@@ -379,6 +398,8 @@ class Client(object):
|
|
|
379
398
|
person_properties: A dictionary of person properties.
|
|
380
399
|
group_properties: A dictionary of group properties.
|
|
381
400
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
401
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
402
|
+
only these flags will be evaluated, improving performance.
|
|
382
403
|
|
|
383
404
|
Examples:
|
|
384
405
|
```python
|
|
@@ -389,7 +410,12 @@ class Client(object):
|
|
|
389
410
|
Feature Flags
|
|
390
411
|
"""
|
|
391
412
|
resp = self.get_flags_decision(
|
|
392
|
-
distinct_id,
|
|
413
|
+
distinct_id,
|
|
414
|
+
groups,
|
|
415
|
+
person_properties,
|
|
416
|
+
group_properties,
|
|
417
|
+
disable_geoip,
|
|
418
|
+
flag_keys_to_evaluate,
|
|
393
419
|
)
|
|
394
420
|
return to_flags_and_payloads(resp)
|
|
395
421
|
|
|
@@ -400,6 +426,7 @@ class Client(object):
|
|
|
400
426
|
person_properties=None,
|
|
401
427
|
group_properties=None,
|
|
402
428
|
disable_geoip=None,
|
|
429
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
403
430
|
) -> FlagsResponse:
|
|
404
431
|
"""
|
|
405
432
|
Get feature flags decision.
|
|
@@ -410,6 +437,8 @@ class Client(object):
|
|
|
410
437
|
person_properties: A dictionary of person properties.
|
|
411
438
|
group_properties: A dictionary of group properties.
|
|
412
439
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
440
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
441
|
+
only these flags will be evaluated, improving performance.
|
|
413
442
|
|
|
414
443
|
Examples:
|
|
415
444
|
```python
|
|
@@ -440,6 +469,9 @@ class Client(object):
|
|
|
440
469
|
"geoip_disable": disable_geoip,
|
|
441
470
|
}
|
|
442
471
|
|
|
472
|
+
if flag_keys_to_evaluate:
|
|
473
|
+
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
|
|
474
|
+
|
|
443
475
|
resp_data = flags(
|
|
444
476
|
self.api_key,
|
|
445
477
|
self.host,
|
|
@@ -544,6 +576,7 @@ class Client(object):
|
|
|
544
576
|
group_properties=flag_options["group_properties"],
|
|
545
577
|
disable_geoip=disable_geoip,
|
|
546
578
|
only_evaluate_locally=True,
|
|
579
|
+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
|
|
547
580
|
)
|
|
548
581
|
else:
|
|
549
582
|
# Default behavior - use remote evaluation
|
|
@@ -553,6 +586,7 @@ class Client(object):
|
|
|
553
586
|
person_properties=flag_options["person_properties"],
|
|
554
587
|
group_properties=flag_options["group_properties"],
|
|
555
588
|
disable_geoip=disable_geoip,
|
|
589
|
+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
|
|
556
590
|
)
|
|
557
591
|
except Exception as e:
|
|
558
592
|
self.log.exception(
|
|
@@ -585,7 +619,7 @@ class Client(object):
|
|
|
585
619
|
|
|
586
620
|
return self._enqueue(msg, disable_geoip)
|
|
587
621
|
|
|
588
|
-
def _parse_send_feature_flags(self, send_feature_flags) ->
|
|
622
|
+
def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
|
|
589
623
|
"""
|
|
590
624
|
Parse and normalize send_feature_flags parameter into a standard format.
|
|
591
625
|
|
|
@@ -593,8 +627,8 @@ class Client(object):
|
|
|
593
627
|
send_feature_flags: Either bool or SendFeatureFlagsOptions dict
|
|
594
628
|
|
|
595
629
|
Returns:
|
|
596
|
-
|
|
597
|
-
person_properties, group_properties
|
|
630
|
+
SendFeatureFlagsOptions: Normalized options with keys: should_send, only_evaluate_locally,
|
|
631
|
+
person_properties, group_properties, flag_keys_filter
|
|
598
632
|
|
|
599
633
|
Raises:
|
|
600
634
|
TypeError: If send_feature_flags is not bool or dict
|
|
@@ -607,6 +641,7 @@ class Client(object):
|
|
|
607
641
|
),
|
|
608
642
|
"person_properties": send_feature_flags.get("person_properties"),
|
|
609
643
|
"group_properties": send_feature_flags.get("group_properties"),
|
|
644
|
+
"flag_keys_filter": send_feature_flags.get("flag_keys_filter"),
|
|
610
645
|
}
|
|
611
646
|
elif isinstance(send_feature_flags, bool):
|
|
612
647
|
return {
|
|
@@ -614,6 +649,7 @@ class Client(object):
|
|
|
614
649
|
"only_evaluate_locally": None,
|
|
615
650
|
"person_properties": None,
|
|
616
651
|
"group_properties": None,
|
|
652
|
+
"flag_keys_filter": None,
|
|
617
653
|
}
|
|
618
654
|
else:
|
|
619
655
|
raise TypeError(
|
|
@@ -1183,12 +1219,12 @@ class Client(object):
|
|
|
1183
1219
|
self.log.warning(
|
|
1184
1220
|
f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
|
|
1185
1221
|
)
|
|
1186
|
-
# failover to `/
|
|
1222
|
+
# failover to `/flags`
|
|
1187
1223
|
raise InconclusiveMatchError("Flag has unknown group type index")
|
|
1188
1224
|
|
|
1189
1225
|
if group_name not in groups:
|
|
1190
1226
|
# Group flags are never enabled in `groups` aren't passed in
|
|
1191
|
-
# don't failover to `/
|
|
1227
|
+
# don't failover to `/flags`, since response will be the same
|
|
1192
1228
|
if warn_on_unknown_groups:
|
|
1193
1229
|
self.log.warning(
|
|
1194
1230
|
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
|
|
@@ -1316,7 +1352,7 @@ class Client(object):
|
|
|
1316
1352
|
)
|
|
1317
1353
|
elif not only_evaluate_locally:
|
|
1318
1354
|
try:
|
|
1319
|
-
flag_details, request_id = self.
|
|
1355
|
+
flag_details, request_id = self._get_feature_flag_details_from_server(
|
|
1320
1356
|
key,
|
|
1321
1357
|
distinct_id,
|
|
1322
1358
|
groups,
|
|
@@ -1556,7 +1592,7 @@ class Client(object):
|
|
|
1556
1592
|
)
|
|
1557
1593
|
return feature_flag_result.payload if feature_flag_result else None
|
|
1558
1594
|
|
|
1559
|
-
def
|
|
1595
|
+
def _get_feature_flag_details_from_server(
|
|
1560
1596
|
self,
|
|
1561
1597
|
key: str,
|
|
1562
1598
|
distinct_id: ID_TYPES,
|
|
@@ -1566,10 +1602,15 @@ class Client(object):
|
|
|
1566
1602
|
disable_geoip: Optional[bool],
|
|
1567
1603
|
) -> tuple[Optional[FeatureFlag], Optional[str]]:
|
|
1568
1604
|
"""
|
|
1569
|
-
Calls /
|
|
1605
|
+
Calls /flags and returns the flag details and request id
|
|
1570
1606
|
"""
|
|
1571
1607
|
resp_data = self.get_flags_decision(
|
|
1572
|
-
distinct_id,
|
|
1608
|
+
distinct_id,
|
|
1609
|
+
groups,
|
|
1610
|
+
person_properties,
|
|
1611
|
+
group_properties,
|
|
1612
|
+
disable_geoip,
|
|
1613
|
+
flag_keys_to_evaluate=[key],
|
|
1573
1614
|
)
|
|
1574
1615
|
request_id = resp_data.get("requestId")
|
|
1575
1616
|
flags = resp_data.get("flags")
|
|
@@ -1685,6 +1726,7 @@ class Client(object):
|
|
|
1685
1726
|
group_properties=None,
|
|
1686
1727
|
only_evaluate_locally=False,
|
|
1687
1728
|
disable_geoip=None,
|
|
1729
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1688
1730
|
) -> Optional[dict[str, Union[bool, str]]]:
|
|
1689
1731
|
"""
|
|
1690
1732
|
Get all feature flags for a user.
|
|
@@ -1696,6 +1738,8 @@ class Client(object):
|
|
|
1696
1738
|
group_properties: A dictionary of group properties.
|
|
1697
1739
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1698
1740
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1741
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
1742
|
+
only these flags will be evaluated, improving performance.
|
|
1699
1743
|
|
|
1700
1744
|
Examples:
|
|
1701
1745
|
```python
|
|
@@ -1712,6 +1756,7 @@ class Client(object):
|
|
|
1712
1756
|
group_properties=group_properties,
|
|
1713
1757
|
only_evaluate_locally=only_evaluate_locally,
|
|
1714
1758
|
disable_geoip=disable_geoip,
|
|
1759
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1715
1760
|
)
|
|
1716
1761
|
|
|
1717
1762
|
return response["featureFlags"]
|
|
@@ -1725,6 +1770,7 @@ class Client(object):
|
|
|
1725
1770
|
group_properties=None,
|
|
1726
1771
|
only_evaluate_locally=False,
|
|
1727
1772
|
disable_geoip=None,
|
|
1773
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1728
1774
|
) -> FlagsAndPayloads:
|
|
1729
1775
|
"""
|
|
1730
1776
|
Get all feature flags and their payloads for a user.
|
|
@@ -1736,6 +1782,8 @@ class Client(object):
|
|
|
1736
1782
|
group_properties: A dictionary of group properties.
|
|
1737
1783
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1738
1784
|
disable_geoip: Whether to disable GeoIP for this request.
|
|
1785
|
+
flag_keys_to_evaluate: A list of specific flag keys to evaluate. If provided,
|
|
1786
|
+
only these flags will be evaluated, improving performance.
|
|
1739
1787
|
|
|
1740
1788
|
Examples:
|
|
1741
1789
|
```python
|
|
@@ -1759,6 +1807,7 @@ class Client(object):
|
|
|
1759
1807
|
groups=groups,
|
|
1760
1808
|
person_properties=person_properties,
|
|
1761
1809
|
group_properties=group_properties,
|
|
1810
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1762
1811
|
)
|
|
1763
1812
|
|
|
1764
1813
|
if fallback_to_decide and not only_evaluate_locally:
|
|
@@ -1769,6 +1818,7 @@ class Client(object):
|
|
|
1769
1818
|
person_properties=person_properties,
|
|
1770
1819
|
group_properties=group_properties,
|
|
1771
1820
|
disable_geoip=disable_geoip,
|
|
1821
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1772
1822
|
)
|
|
1773
1823
|
return to_flags_and_payloads(decide_response)
|
|
1774
1824
|
except Exception as e:
|
|
@@ -1786,6 +1836,7 @@ class Client(object):
|
|
|
1786
1836
|
person_properties=None,
|
|
1787
1837
|
group_properties=None,
|
|
1788
1838
|
warn_on_unknown_groups=False,
|
|
1839
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1789
1840
|
) -> tuple[FlagsAndPayloads, bool]:
|
|
1790
1841
|
person_properties = person_properties or {}
|
|
1791
1842
|
group_properties = group_properties or {}
|
|
@@ -1798,7 +1849,15 @@ class Client(object):
|
|
|
1798
1849
|
fallback_to_decide = False
|
|
1799
1850
|
# If loading in previous line failed
|
|
1800
1851
|
if self.feature_flags:
|
|
1801
|
-
|
|
1852
|
+
# Filter flags based on flag_keys_to_evaluate if provided
|
|
1853
|
+
flags_to_process = self.feature_flags
|
|
1854
|
+
if flag_keys_to_evaluate:
|
|
1855
|
+
flag_keys_set = set(flag_keys_to_evaluate)
|
|
1856
|
+
flags_to_process = [
|
|
1857
|
+
flag for flag in self.feature_flags if flag["key"] in flag_keys_set
|
|
1858
|
+
]
|
|
1859
|
+
|
|
1860
|
+
for flag in flags_to_process:
|
|
1802
1861
|
try:
|
|
1803
1862
|
flags[flag["key"]] = self._compute_flag_locally(
|
|
1804
1863
|
flag,
|
|
@@ -1814,7 +1873,7 @@ class Client(object):
|
|
|
1814
1873
|
if matched_payload is not None:
|
|
1815
1874
|
payloads[flag["key"]] = matched_payload
|
|
1816
1875
|
except InconclusiveMatchError:
|
|
1817
|
-
# No need to log this, since it's just telling us to fall back to `/
|
|
1876
|
+
# No need to log this, since it's just telling us to fall back to `/flags`
|
|
1818
1877
|
fallback_to_decide = True
|
|
1819
1878
|
except Exception as e:
|
|
1820
1879
|
self.log.exception(
|
|
@@ -13,6 +13,7 @@ from posthoganalytics.request import APIError
|
|
|
13
13
|
from posthoganalytics.test.test_utils import FAKE_TEST_API_KEY
|
|
14
14
|
from posthoganalytics.types import FeatureFlag, LegacyFlagMetadata
|
|
15
15
|
from posthoganalytics.version import VERSION
|
|
16
|
+
from posthoganalytics.contexts import tag
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class TestClient(unittest.TestCase):
|
|
@@ -1741,6 +1742,7 @@ class TestClient(unittest.TestCase):
|
|
|
1741
1742
|
person_properties={"distinct_id": "some_id"},
|
|
1742
1743
|
group_properties={},
|
|
1743
1744
|
geoip_disable=True,
|
|
1745
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1744
1746
|
)
|
|
1745
1747
|
patch_flags.reset_mock()
|
|
1746
1748
|
client.feature_enabled(
|
|
@@ -1755,6 +1757,7 @@ class TestClient(unittest.TestCase):
|
|
|
1755
1757
|
person_properties={"distinct_id": "feature_enabled_distinct_id"},
|
|
1756
1758
|
group_properties={},
|
|
1757
1759
|
geoip_disable=True,
|
|
1760
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1758
1761
|
)
|
|
1759
1762
|
patch_flags.reset_mock()
|
|
1760
1763
|
client.get_all_flags_and_payloads("all_flags_payloads_id")
|
|
@@ -1815,6 +1818,7 @@ class TestClient(unittest.TestCase):
|
|
|
1815
1818
|
"instance": {"$group_key": "app.posthog.com"},
|
|
1816
1819
|
},
|
|
1817
1820
|
geoip_disable=False,
|
|
1821
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1818
1822
|
)
|
|
1819
1823
|
|
|
1820
1824
|
patch_flags.reset_mock()
|
|
@@ -1841,6 +1845,7 @@ class TestClient(unittest.TestCase):
|
|
|
1841
1845
|
"instance": {"$group_key": "app.posthog.com"},
|
|
1842
1846
|
},
|
|
1843
1847
|
geoip_disable=False,
|
|
1848
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1844
1849
|
)
|
|
1845
1850
|
|
|
1846
1851
|
patch_flags.reset_mock()
|
|
@@ -2186,6 +2191,7 @@ class TestClient(unittest.TestCase):
|
|
|
2186
2191
|
"only_evaluate_locally": None,
|
|
2187
2192
|
"person_properties": None,
|
|
2188
2193
|
"group_properties": None,
|
|
2194
|
+
"flag_keys_filter": None,
|
|
2189
2195
|
}
|
|
2190
2196
|
self.assertEqual(result, expected)
|
|
2191
2197
|
|
|
@@ -2196,6 +2202,7 @@ class TestClient(unittest.TestCase):
|
|
|
2196
2202
|
"only_evaluate_locally": None,
|
|
2197
2203
|
"person_properties": None,
|
|
2198
2204
|
"group_properties": None,
|
|
2205
|
+
"flag_keys_filter": None,
|
|
2199
2206
|
}
|
|
2200
2207
|
self.assertEqual(result, expected)
|
|
2201
2208
|
|
|
@@ -2211,6 +2218,7 @@ class TestClient(unittest.TestCase):
|
|
|
2211
2218
|
"only_evaluate_locally": True,
|
|
2212
2219
|
"person_properties": {"plan": "premium"},
|
|
2213
2220
|
"group_properties": {"company": {"type": "enterprise"}},
|
|
2221
|
+
"flag_keys_filter": None,
|
|
2214
2222
|
}
|
|
2215
2223
|
self.assertEqual(result, expected)
|
|
2216
2224
|
|
|
@@ -2222,6 +2230,7 @@ class TestClient(unittest.TestCase):
|
|
|
2222
2230
|
"only_evaluate_locally": None,
|
|
2223
2231
|
"person_properties": {"user_id": "123"},
|
|
2224
2232
|
"group_properties": None,
|
|
2233
|
+
"flag_keys_filter": None,
|
|
2225
2234
|
}
|
|
2226
2235
|
self.assertEqual(result, expected)
|
|
2227
2236
|
|
|
@@ -2232,6 +2241,7 @@ class TestClient(unittest.TestCase):
|
|
|
2232
2241
|
"only_evaluate_locally": None,
|
|
2233
2242
|
"person_properties": None,
|
|
2234
2243
|
"group_properties": None,
|
|
2244
|
+
"flag_keys_filter": None,
|
|
2235
2245
|
}
|
|
2236
2246
|
self.assertEqual(result, expected)
|
|
2237
2247
|
|
|
@@ -2248,6 +2258,53 @@ class TestClient(unittest.TestCase):
|
|
|
2248
2258
|
client._parse_send_feature_flags(None)
|
|
2249
2259
|
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
2250
2260
|
|
|
2261
|
+
@mock.patch("posthog.client.flags")
|
|
2262
|
+
def test_capture_with_send_feature_flags_flag_keys_filter(self, patch_flags):
|
|
2263
|
+
"""Test that SendFeatureFlagsOptions with flag_keys_filter only evaluates specified flags"""
|
|
2264
|
+
# When flag_keys_to_evaluate is provided, the API should only return the requested flags
|
|
2265
|
+
patch_flags.return_value = {
|
|
2266
|
+
"featureFlags": {
|
|
2267
|
+
"flag1": "value1",
|
|
2268
|
+
"flag3": "value3",
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
2273
|
+
client = Client(
|
|
2274
|
+
FAKE_TEST_API_KEY,
|
|
2275
|
+
on_error=self.set_fail,
|
|
2276
|
+
personal_api_key=FAKE_TEST_API_KEY,
|
|
2277
|
+
sync_mode=True,
|
|
2278
|
+
)
|
|
2279
|
+
|
|
2280
|
+
send_options = {
|
|
2281
|
+
"flag_keys_filter": ["flag1", "flag3"],
|
|
2282
|
+
"person_properties": {"subscription": "pro"},
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
msg_uuid = client.capture(
|
|
2286
|
+
"test event", distinct_id="distinct_id", send_feature_flags=send_options
|
|
2287
|
+
)
|
|
2288
|
+
|
|
2289
|
+
self.assertIsNotNone(msg_uuid)
|
|
2290
|
+
self.assertFalse(self.failed)
|
|
2291
|
+
|
|
2292
|
+
# Verify flags() was called with flag_keys_to_evaluate
|
|
2293
|
+
patch_flags.assert_called_once()
|
|
2294
|
+
call_args = patch_flags.call_args[1]
|
|
2295
|
+
self.assertEqual(call_args["flag_keys_to_evaluate"], ["flag1", "flag3"])
|
|
2296
|
+
self.assertEqual(call_args["person_properties"], {"subscription": "pro"})
|
|
2297
|
+
|
|
2298
|
+
# Check the message includes only the filtered flags
|
|
2299
|
+
mock_post.assert_called_once()
|
|
2300
|
+
batch_data = mock_post.call_args[1]["batch"]
|
|
2301
|
+
msg = batch_data[0]
|
|
2302
|
+
|
|
2303
|
+
self.assertEqual(msg["properties"]["$feature/flag1"], "value1")
|
|
2304
|
+
self.assertEqual(msg["properties"]["$feature/flag3"], "value3")
|
|
2305
|
+
# flag2 should not be included since it wasn't requested
|
|
2306
|
+
self.assertNotIn("$feature/flag2", msg["properties"])
|
|
2307
|
+
|
|
2251
2308
|
@mock.patch("posthog.client.batch_post")
|
|
2252
2309
|
def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post):
|
|
2253
2310
|
"""Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string"""
|
|
@@ -2354,3 +2411,15 @@ class TestClient(unittest.TestCase):
|
|
|
2354
2411
|
self.assertEqual(
|
|
2355
2412
|
result["featureFlagPayloads"]["normal-payload-flag"], "normal payload"
|
|
2356
2413
|
)
|
|
2414
|
+
|
|
2415
|
+
def test_context_tags_added(self):
|
|
2416
|
+
with mock.patch("posthog.client.batch_post") as mock_post:
|
|
2417
|
+
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
|
|
2418
|
+
|
|
2419
|
+
with new_context():
|
|
2420
|
+
tag("random_tag", 12345)
|
|
2421
|
+
client.capture("python test event", distinct_id="distinct_id")
|
|
2422
|
+
|
|
2423
|
+
batch_data = mock_post.call_args[1]["batch"]
|
|
2424
|
+
msg = batch_data[0]
|
|
2425
|
+
self.assertEqual(msg["properties"]["$context_tags"], ["random_tag"])
|
|
@@ -215,7 +215,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
215
215
|
)
|
|
216
216
|
self.assertEqual(patch_flags.call_count, 0)
|
|
217
217
|
|
|
218
|
-
# Now group type mappings are gone, so fall back to /
|
|
218
|
+
# Now group type mappings are gone, so fall back to /flags/
|
|
219
219
|
patch_flags.return_value = {
|
|
220
220
|
"featureFlags": {"group-flag": "decide-fallback-value"}
|
|
221
221
|
}
|
|
@@ -311,7 +311,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
311
311
|
)
|
|
312
312
|
self.assertEqual(patch_flags.call_count, 0)
|
|
313
313
|
|
|
314
|
-
# will fall back on `/
|
|
314
|
+
# will fall back on `/flags`, as all properties present for second group, but that group resolves to false
|
|
315
315
|
self.assertEqual(
|
|
316
316
|
client.get_feature_flag(
|
|
317
317
|
"complex-flag",
|
|
@@ -651,7 +651,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
651
651
|
},
|
|
652
652
|
},
|
|
653
653
|
]
|
|
654
|
-
# beta-feature value overridden by /
|
|
654
|
+
# beta-feature value overridden by /flags
|
|
655
655
|
self.assertEqual(
|
|
656
656
|
client.get_all_flags("distinct_id"),
|
|
657
657
|
{
|
|
@@ -725,7 +725,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
725
725
|
},
|
|
726
726
|
},
|
|
727
727
|
]
|
|
728
|
-
# beta-feature value overridden by /
|
|
728
|
+
# beta-feature value overridden by /flags
|
|
729
729
|
self.assertEqual(
|
|
730
730
|
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
|
|
731
731
|
{
|
|
@@ -746,7 +746,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
746
746
|
}
|
|
747
747
|
client = self.client
|
|
748
748
|
client.feature_flags = []
|
|
749
|
-
# beta-feature value overridden by /
|
|
749
|
+
# beta-feature value overridden by /flags
|
|
750
750
|
self.assertEqual(
|
|
751
751
|
client.get_all_flags("distinct_id"),
|
|
752
752
|
{"beta-feature": "variant-1", "beta-feature2": "variant-2"},
|
|
@@ -765,7 +765,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
765
765
|
}
|
|
766
766
|
client = self.client
|
|
767
767
|
client.feature_flags = []
|
|
768
|
-
# beta-feature value overridden by /
|
|
768
|
+
# beta-feature value overridden by /flags
|
|
769
769
|
self.assertEqual(
|
|
770
770
|
client.get_all_flags_and_payloads("distinct_id")["featureFlagPayloads"],
|
|
771
771
|
{"beta-feature": 100, "beta-feature2": 300},
|
|
@@ -5387,4 +5387,115 @@ class TestConsistency(unittest.TestCase):
|
|
|
5387
5387
|
test_cases = ["beta-feature", "BETA-FEATURE", "bEtA-FeAtUrE"]
|
|
5388
5388
|
for case in test_cases:
|
|
5389
5389
|
self.assertFalse(client.feature_enabled(case, "user1"))
|
|
5390
|
-
|
|
5390
|
+
|
|
5391
|
+
@mock.patch("posthog.client.flags")
|
|
5392
|
+
def test_get_all_flags_with_flag_keys_to_evaluate(self, mock_flags):
|
|
5393
|
+
"""Test that get_all_flags with flag_keys_to_evaluate only evaluates specified flags"""
|
|
5394
|
+
mock_flags.return_value = {
|
|
5395
|
+
"featureFlags": {
|
|
5396
|
+
"flag1": "value1",
|
|
5397
|
+
"flag2": True,
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5401
|
+
client = Client(
|
|
5402
|
+
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
|
|
5403
|
+
)
|
|
5404
|
+
|
|
5405
|
+
# Call get_all_flags with flag_keys_to_evaluate
|
|
5406
|
+
result = client.get_all_flags(
|
|
5407
|
+
"user123",
|
|
5408
|
+
flag_keys_to_evaluate=["flag1", "flag2"],
|
|
5409
|
+
person_properties={"region": "USA"},
|
|
5410
|
+
)
|
|
5411
|
+
|
|
5412
|
+
# Verify flags() was called with flag_keys_to_evaluate
|
|
5413
|
+
mock_flags.assert_called_once()
|
|
5414
|
+
call_args = mock_flags.call_args[1]
|
|
5415
|
+
self.assertEqual(call_args["flag_keys_to_evaluate"], ["flag1", "flag2"])
|
|
5416
|
+
self.assertEqual(
|
|
5417
|
+
call_args["person_properties"], {"distinct_id": "user123", "region": "USA"}
|
|
5418
|
+
)
|
|
5419
|
+
|
|
5420
|
+
# Check the result
|
|
5421
|
+
self.assertEqual(result, {"flag1": "value1", "flag2": True})
|
|
5422
|
+
|
|
5423
|
+
@mock.patch("posthog.client.flags")
|
|
5424
|
+
def test_get_all_flags_and_payloads_with_flag_keys_to_evaluate(self, mock_flags):
|
|
5425
|
+
"""Test that get_all_flags_and_payloads with flag_keys_to_evaluate only evaluates specified flags"""
|
|
5426
|
+
mock_flags.return_value = {
|
|
5427
|
+
"featureFlags": {
|
|
5428
|
+
"flag1": "variant1",
|
|
5429
|
+
"flag3": True,
|
|
5430
|
+
},
|
|
5431
|
+
"featureFlagPayloads": {
|
|
5432
|
+
"flag1": {"data": "payload1"},
|
|
5433
|
+
"flag3": {"data": "payload3"},
|
|
5434
|
+
},
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
client = Client(
|
|
5438
|
+
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
|
|
5439
|
+
)
|
|
5440
|
+
|
|
5441
|
+
# Call get_all_flags_and_payloads with flag_keys_to_evaluate
|
|
5442
|
+
result = client.get_all_flags_and_payloads(
|
|
5443
|
+
"user123",
|
|
5444
|
+
flag_keys_to_evaluate=["flag1", "flag3"],
|
|
5445
|
+
person_properties={"subscription": "pro"},
|
|
5446
|
+
)
|
|
5447
|
+
|
|
5448
|
+
# Verify flags() was called with flag_keys_to_evaluate
|
|
5449
|
+
mock_flags.assert_called_once()
|
|
5450
|
+
call_args = mock_flags.call_args[1]
|
|
5451
|
+
self.assertEqual(call_args["flag_keys_to_evaluate"], ["flag1", "flag3"])
|
|
5452
|
+
self.assertEqual(
|
|
5453
|
+
call_args["person_properties"],
|
|
5454
|
+
{"distinct_id": "user123", "subscription": "pro"},
|
|
5455
|
+
)
|
|
5456
|
+
|
|
5457
|
+
# Check the result
|
|
5458
|
+
self.assertEqual(result["featureFlags"], {"flag1": "variant1", "flag3": True})
|
|
5459
|
+
self.assertEqual(
|
|
5460
|
+
result["featureFlagPayloads"],
|
|
5461
|
+
{"flag1": {"data": "payload1"}, "flag3": {"data": "payload3"}},
|
|
5462
|
+
)
|
|
5463
|
+
|
|
5464
|
+
def test_get_all_flags_locally_with_flag_keys_to_evaluate(self):
|
|
5465
|
+
"""Test that local evaluation with flag_keys_to_evaluate only evaluates specified flags"""
|
|
5466
|
+
client = Client(
|
|
5467
|
+
project_api_key=FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY
|
|
5468
|
+
)
|
|
5469
|
+
|
|
5470
|
+
# Set up multiple flags
|
|
5471
|
+
client.feature_flags = [
|
|
5472
|
+
{
|
|
5473
|
+
"id": 1,
|
|
5474
|
+
"key": "flag1",
|
|
5475
|
+
"active": True,
|
|
5476
|
+
"filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
|
|
5477
|
+
},
|
|
5478
|
+
{
|
|
5479
|
+
"id": 2,
|
|
5480
|
+
"key": "flag2",
|
|
5481
|
+
"active": True,
|
|
5482
|
+
"filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
|
|
5483
|
+
},
|
|
5484
|
+
{
|
|
5485
|
+
"id": 3,
|
|
5486
|
+
"key": "flag3",
|
|
5487
|
+
"active": True,
|
|
5488
|
+
"filters": {"groups": [{"properties": [], "rollout_percentage": 100}]},
|
|
5489
|
+
},
|
|
5490
|
+
]
|
|
5491
|
+
|
|
5492
|
+
# Call get_all_flags with flag_keys_to_evaluate
|
|
5493
|
+
result = client.get_all_flags(
|
|
5494
|
+
"user123",
|
|
5495
|
+
flag_keys_to_evaluate=["flag1", "flag3"],
|
|
5496
|
+
only_evaluate_locally=True,
|
|
5497
|
+
)
|
|
5498
|
+
|
|
5499
|
+
# Should only return flag1 and flag3
|
|
5500
|
+
self.assertEqual(result, {"flag1": True, "flag3": True})
|
|
5501
|
+
self.assertNotIn("flag2", result)
|
posthoganalytics/types.py
CHANGED
|
@@ -9,6 +9,7 @@ FlagValue = Union[bool, str]
|
|
|
9
9
|
BeforeSendCallback = Callable[[dict[str, Any]], Optional[dict[str, Any]]]
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
# Type alias for the send_feature_flags parameter
|
|
12
13
|
class SendFeatureFlagsOptions(TypedDict, total=False):
|
|
13
14
|
"""Options for sending feature flags with capture events.
|
|
14
15
|
|
|
@@ -22,9 +23,11 @@ class SendFeatureFlagsOptions(TypedDict, total=False):
|
|
|
22
23
|
Format: { group_type_name: { group_properties } }
|
|
23
24
|
"""
|
|
24
25
|
|
|
26
|
+
should_send: bool
|
|
25
27
|
only_evaluate_locally: Optional[bool]
|
|
26
28
|
person_properties: Optional[dict[str, Any]]
|
|
27
29
|
group_properties: Optional[dict[str, dict[str, Any]]]
|
|
30
|
+
flag_keys_filter: Optional[list[str]]
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
@dataclass(frozen=True)
|
posthoganalytics/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
posthoganalytics/__init__.py,sha256=66HkeJ1fkzbKC2ggl3F164oajFeiGm8v84kJR0Yf5BI,25987
|
|
2
2
|
posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
|
|
3
|
-
posthoganalytics/client.py,sha256=
|
|
3
|
+
posthoganalytics/client.py,sha256=aouVWhA4LglnwJK2NYDUDjCHdT0Boebn7TpI05WEs9U,71291
|
|
4
4
|
posthoganalytics/consumer.py,sha256=CiNbJBdyW9jER3ZYCKbX-JFmEDXlE1lbDy1MSl43-a0,4617
|
|
5
5
|
posthoganalytics/contexts.py,sha256=LFSFIYpUFWKTBnGMjV9n1aYHWbAzz5zLJGr2qG34PoE,9405
|
|
6
6
|
posthoganalytics/exception_capture.py,sha256=1VHBfffrXXrkK0PT8iVgKPpj_R1pGAzG5f3Qw0WF79w,1783
|
|
@@ -9,9 +9,9 @@ posthoganalytics/feature_flags.py,sha256=O_kXmw3goB2E9XMBosdPeBAuo9MsnsH8PyNWq95
|
|
|
9
9
|
posthoganalytics/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
|
|
10
10
|
posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
posthoganalytics/request.py,sha256=Bsl2c5WwONKPQzwWMmKPX5VgOlwSiIcSNfhXgoz62Y8,6186
|
|
12
|
-
posthoganalytics/types.py,sha256=
|
|
12
|
+
posthoganalytics/types.py,sha256=Dl3aFGX9XUR0wMmK12r2s5Hjan9jL4HpQ9GHpVcEq5U,10207
|
|
13
13
|
posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
|
|
14
|
-
posthoganalytics/version.py,sha256=
|
|
14
|
+
posthoganalytics/version.py,sha256=0bIh40DOpniOO55hJUyuzw6FrrEfbuBGzAq2e_jniew,87
|
|
15
15
|
posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
posthoganalytics/ai/utils.py,sha256=92RlL395wjL5V9FstS8BeebwMtaz6DP6zS9miCNla9M,21106
|
|
17
17
|
posthoganalytics/ai/anthropic/__init__.py,sha256=fFhDOiRzTXzGQlgnrRDL-4yKC8EYIl8NW4a2QNR6xRU,368
|
|
@@ -30,20 +30,20 @@ posthoganalytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
30
30
|
posthoganalytics/integrations/django.py,sha256=KYtBr7CkiZQynRc2TCWWYHe-J3ie8iSUa42WPshYZdc,6795
|
|
31
31
|
posthoganalytics/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
|
|
32
32
|
posthoganalytics/test/test_before_send.py,sha256=A1_UVMewhHAvO39rZDWfS606vG_X-q0KNXvh5DAKiB8,7930
|
|
33
|
-
posthoganalytics/test/test_client.py,sha256=
|
|
33
|
+
posthoganalytics/test/test_client.py,sha256=rb_y0HbaxDbS5P4WrG0ked1ZEYUK0G7ooxljRkmIONI,94495
|
|
34
34
|
posthoganalytics/test/test_consumer.py,sha256=HGMfU9PzQ5ZAe_R3kHnZNsMvD7jUjHL-gie0isrvMMk,7107
|
|
35
35
|
posthoganalytics/test/test_contexts.py,sha256=c--hNUIEf6SHQ7H9vdPhU1oLCN0SnD4wDbFr-eLPHDo,7013
|
|
36
36
|
posthoganalytics/test/test_exception_capture.py,sha256=al37Kg6wjzL_IBCFUUXRvkP6nVrqS6IZRCOKSo29Nh8,1063
|
|
37
37
|
posthoganalytics/test/test_feature_flag.py,sha256=9RQwB5eCvVAGrrO7UnR3Z1OidP_YoL4iBl3A83fuAig,6824
|
|
38
38
|
posthoganalytics/test/test_feature_flag_result.py,sha256=z2OgD97r85LKMqCnoCqAs74WjUMucayAtC3qWaITGCA,15898
|
|
39
|
-
posthoganalytics/test/test_feature_flags.py,sha256=
|
|
39
|
+
posthoganalytics/test/test_feature_flags.py,sha256=qfRDlvKVJNgkLHtt0tWIFrPQCvIUxXc6dqli9yPs85Q,175974
|
|
40
40
|
posthoganalytics/test/test_module.py,sha256=viqaAWA_uHt8r20fHIeME6IQkeXmQ8ZyrJTtPGQAb1E,1070
|
|
41
41
|
posthoganalytics/test/test_request.py,sha256=Zc0VbkjpVmj8mKokQm9rzdgTr0b1U44vvMYSkB_IQLs,4467
|
|
42
42
|
posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
|
|
43
43
|
posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
|
|
44
44
|
posthoganalytics/test/test_utils.py,sha256=sqUTbfweVcxxFRd3WDMFXqPMyU6DvzOBeAOc68Py9aw,9620
|
|
45
|
-
posthoganalytics-6.
|
|
46
|
-
posthoganalytics-6.
|
|
47
|
-
posthoganalytics-6.
|
|
48
|
-
posthoganalytics-6.
|
|
49
|
-
posthoganalytics-6.
|
|
45
|
+
posthoganalytics-6.6.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
|
|
46
|
+
posthoganalytics-6.6.0.dist-info/METADATA,sha256=8OGLBUt9B-iYFGi9QjGCga5VOnNay8DCo-e8qp9Hhzs,6024
|
|
47
|
+
posthoganalytics-6.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
48
|
+
posthoganalytics-6.6.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
|
|
49
|
+
posthoganalytics-6.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|