posthoganalytics 6.5.0__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 +72 -14
- posthoganalytics/test/test_client.py +56 -0
- posthoganalytics/test/test_feature_flags.py +118 -7
- posthoganalytics/types.py +3 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.5.0.dist-info → posthoganalytics-6.6.0.dist-info}/METADATA +1 -1
- {posthoganalytics-6.5.0.dist-info → posthoganalytics-6.6.0.dist-info}/RECORD +10 -10
- {posthoganalytics-6.5.0.dist-info → posthoganalytics-6.6.0.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.5.0.dist-info → posthoganalytics-6.6.0.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.5.0.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,
|
|
@@ -313,6 +314,7 @@ class Client(object):
|
|
|
313
314
|
person_properties=None,
|
|
314
315
|
group_properties=None,
|
|
315
316
|
disable_geoip=None,
|
|
317
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
316
318
|
) -> dict[str, Union[bool, str]]:
|
|
317
319
|
"""
|
|
318
320
|
Get feature flag variants for a user by calling decide.
|
|
@@ -323,12 +325,19 @@ class Client(object):
|
|
|
323
325
|
person_properties: A dictionary of person properties.
|
|
324
326
|
group_properties: A dictionary of group properties.
|
|
325
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.
|
|
326
330
|
|
|
327
331
|
Category:
|
|
328
332
|
Feature Flags
|
|
329
333
|
"""
|
|
330
334
|
resp_data = self.get_flags_decision(
|
|
331
|
-
distinct_id,
|
|
335
|
+
distinct_id,
|
|
336
|
+
groups,
|
|
337
|
+
person_properties,
|
|
338
|
+
group_properties,
|
|
339
|
+
disable_geoip,
|
|
340
|
+
flag_keys_to_evaluate,
|
|
332
341
|
)
|
|
333
342
|
return to_values(resp_data) or {}
|
|
334
343
|
|
|
@@ -339,6 +348,7 @@ class Client(object):
|
|
|
339
348
|
person_properties=None,
|
|
340
349
|
group_properties=None,
|
|
341
350
|
disable_geoip=None,
|
|
351
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
342
352
|
) -> dict[str, str]:
|
|
343
353
|
"""
|
|
344
354
|
Get feature flag payloads for a user by calling decide.
|
|
@@ -349,6 +359,8 @@ class Client(object):
|
|
|
349
359
|
person_properties: A dictionary of person properties.
|
|
350
360
|
group_properties: A dictionary of group properties.
|
|
351
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.
|
|
352
364
|
|
|
353
365
|
Examples:
|
|
354
366
|
```python
|
|
@@ -359,7 +371,12 @@ class Client(object):
|
|
|
359
371
|
Feature Flags
|
|
360
372
|
"""
|
|
361
373
|
resp_data = self.get_flags_decision(
|
|
362
|
-
distinct_id,
|
|
374
|
+
distinct_id,
|
|
375
|
+
groups,
|
|
376
|
+
person_properties,
|
|
377
|
+
group_properties,
|
|
378
|
+
disable_geoip,
|
|
379
|
+
flag_keys_to_evaluate,
|
|
363
380
|
)
|
|
364
381
|
return to_payloads(resp_data) or {}
|
|
365
382
|
|
|
@@ -370,6 +387,7 @@ class Client(object):
|
|
|
370
387
|
person_properties=None,
|
|
371
388
|
group_properties=None,
|
|
372
389
|
disable_geoip=None,
|
|
390
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
373
391
|
) -> FlagsAndPayloads:
|
|
374
392
|
"""
|
|
375
393
|
Get feature flags and payloads for a user by calling decide.
|
|
@@ -380,6 +398,8 @@ class Client(object):
|
|
|
380
398
|
person_properties: A dictionary of person properties.
|
|
381
399
|
group_properties: A dictionary of group properties.
|
|
382
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.
|
|
383
403
|
|
|
384
404
|
Examples:
|
|
385
405
|
```python
|
|
@@ -390,7 +410,12 @@ class Client(object):
|
|
|
390
410
|
Feature Flags
|
|
391
411
|
"""
|
|
392
412
|
resp = self.get_flags_decision(
|
|
393
|
-
distinct_id,
|
|
413
|
+
distinct_id,
|
|
414
|
+
groups,
|
|
415
|
+
person_properties,
|
|
416
|
+
group_properties,
|
|
417
|
+
disable_geoip,
|
|
418
|
+
flag_keys_to_evaluate,
|
|
394
419
|
)
|
|
395
420
|
return to_flags_and_payloads(resp)
|
|
396
421
|
|
|
@@ -401,6 +426,7 @@ class Client(object):
|
|
|
401
426
|
person_properties=None,
|
|
402
427
|
group_properties=None,
|
|
403
428
|
disable_geoip=None,
|
|
429
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
404
430
|
) -> FlagsResponse:
|
|
405
431
|
"""
|
|
406
432
|
Get feature flags decision.
|
|
@@ -411,6 +437,8 @@ class Client(object):
|
|
|
411
437
|
person_properties: A dictionary of person properties.
|
|
412
438
|
group_properties: A dictionary of group properties.
|
|
413
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.
|
|
414
442
|
|
|
415
443
|
Examples:
|
|
416
444
|
```python
|
|
@@ -441,6 +469,9 @@ class Client(object):
|
|
|
441
469
|
"geoip_disable": disable_geoip,
|
|
442
470
|
}
|
|
443
471
|
|
|
472
|
+
if flag_keys_to_evaluate:
|
|
473
|
+
request_data["flag_keys_to_evaluate"] = flag_keys_to_evaluate
|
|
474
|
+
|
|
444
475
|
resp_data = flags(
|
|
445
476
|
self.api_key,
|
|
446
477
|
self.host,
|
|
@@ -545,6 +576,7 @@ class Client(object):
|
|
|
545
576
|
group_properties=flag_options["group_properties"],
|
|
546
577
|
disable_geoip=disable_geoip,
|
|
547
578
|
only_evaluate_locally=True,
|
|
579
|
+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
|
|
548
580
|
)
|
|
549
581
|
else:
|
|
550
582
|
# Default behavior - use remote evaluation
|
|
@@ -554,6 +586,7 @@ class Client(object):
|
|
|
554
586
|
person_properties=flag_options["person_properties"],
|
|
555
587
|
group_properties=flag_options["group_properties"],
|
|
556
588
|
disable_geoip=disable_geoip,
|
|
589
|
+
flag_keys_to_evaluate=flag_options["flag_keys_filter"],
|
|
557
590
|
)
|
|
558
591
|
except Exception as e:
|
|
559
592
|
self.log.exception(
|
|
@@ -586,7 +619,7 @@ class Client(object):
|
|
|
586
619
|
|
|
587
620
|
return self._enqueue(msg, disable_geoip)
|
|
588
621
|
|
|
589
|
-
def _parse_send_feature_flags(self, send_feature_flags) ->
|
|
622
|
+
def _parse_send_feature_flags(self, send_feature_flags) -> SendFeatureFlagsOptions:
|
|
590
623
|
"""
|
|
591
624
|
Parse and normalize send_feature_flags parameter into a standard format.
|
|
592
625
|
|
|
@@ -594,8 +627,8 @@ class Client(object):
|
|
|
594
627
|
send_feature_flags: Either bool or SendFeatureFlagsOptions dict
|
|
595
628
|
|
|
596
629
|
Returns:
|
|
597
|
-
|
|
598
|
-
person_properties, group_properties
|
|
630
|
+
SendFeatureFlagsOptions: Normalized options with keys: should_send, only_evaluate_locally,
|
|
631
|
+
person_properties, group_properties, flag_keys_filter
|
|
599
632
|
|
|
600
633
|
Raises:
|
|
601
634
|
TypeError: If send_feature_flags is not bool or dict
|
|
@@ -608,6 +641,7 @@ class Client(object):
|
|
|
608
641
|
),
|
|
609
642
|
"person_properties": send_feature_flags.get("person_properties"),
|
|
610
643
|
"group_properties": send_feature_flags.get("group_properties"),
|
|
644
|
+
"flag_keys_filter": send_feature_flags.get("flag_keys_filter"),
|
|
611
645
|
}
|
|
612
646
|
elif isinstance(send_feature_flags, bool):
|
|
613
647
|
return {
|
|
@@ -615,6 +649,7 @@ class Client(object):
|
|
|
615
649
|
"only_evaluate_locally": None,
|
|
616
650
|
"person_properties": None,
|
|
617
651
|
"group_properties": None,
|
|
652
|
+
"flag_keys_filter": None,
|
|
618
653
|
}
|
|
619
654
|
else:
|
|
620
655
|
raise TypeError(
|
|
@@ -1184,12 +1219,12 @@ class Client(object):
|
|
|
1184
1219
|
self.log.warning(
|
|
1185
1220
|
f"[FEATURE FLAGS] Unknown group type index {aggregation_group_type_index} for feature flag {feature_flag['key']}"
|
|
1186
1221
|
)
|
|
1187
|
-
# failover to `/
|
|
1222
|
+
# failover to `/flags`
|
|
1188
1223
|
raise InconclusiveMatchError("Flag has unknown group type index")
|
|
1189
1224
|
|
|
1190
1225
|
if group_name not in groups:
|
|
1191
1226
|
# Group flags are never enabled in `groups` aren't passed in
|
|
1192
|
-
# don't failover to `/
|
|
1227
|
+
# don't failover to `/flags`, since response will be the same
|
|
1193
1228
|
if warn_on_unknown_groups:
|
|
1194
1229
|
self.log.warning(
|
|
1195
1230
|
f"[FEATURE FLAGS] Can't compute group feature flag: {feature_flag['key']} without group names passed in"
|
|
@@ -1317,7 +1352,7 @@ class Client(object):
|
|
|
1317
1352
|
)
|
|
1318
1353
|
elif not only_evaluate_locally:
|
|
1319
1354
|
try:
|
|
1320
|
-
flag_details, request_id = self.
|
|
1355
|
+
flag_details, request_id = self._get_feature_flag_details_from_server(
|
|
1321
1356
|
key,
|
|
1322
1357
|
distinct_id,
|
|
1323
1358
|
groups,
|
|
@@ -1557,7 +1592,7 @@ class Client(object):
|
|
|
1557
1592
|
)
|
|
1558
1593
|
return feature_flag_result.payload if feature_flag_result else None
|
|
1559
1594
|
|
|
1560
|
-
def
|
|
1595
|
+
def _get_feature_flag_details_from_server(
|
|
1561
1596
|
self,
|
|
1562
1597
|
key: str,
|
|
1563
1598
|
distinct_id: ID_TYPES,
|
|
@@ -1567,10 +1602,15 @@ class Client(object):
|
|
|
1567
1602
|
disable_geoip: Optional[bool],
|
|
1568
1603
|
) -> tuple[Optional[FeatureFlag], Optional[str]]:
|
|
1569
1604
|
"""
|
|
1570
|
-
Calls /
|
|
1605
|
+
Calls /flags and returns the flag details and request id
|
|
1571
1606
|
"""
|
|
1572
1607
|
resp_data = self.get_flags_decision(
|
|
1573
|
-
distinct_id,
|
|
1608
|
+
distinct_id,
|
|
1609
|
+
groups,
|
|
1610
|
+
person_properties,
|
|
1611
|
+
group_properties,
|
|
1612
|
+
disable_geoip,
|
|
1613
|
+
flag_keys_to_evaluate=[key],
|
|
1574
1614
|
)
|
|
1575
1615
|
request_id = resp_data.get("requestId")
|
|
1576
1616
|
flags = resp_data.get("flags")
|
|
@@ -1686,6 +1726,7 @@ class Client(object):
|
|
|
1686
1726
|
group_properties=None,
|
|
1687
1727
|
only_evaluate_locally=False,
|
|
1688
1728
|
disable_geoip=None,
|
|
1729
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1689
1730
|
) -> Optional[dict[str, Union[bool, str]]]:
|
|
1690
1731
|
"""
|
|
1691
1732
|
Get all feature flags for a user.
|
|
@@ -1697,6 +1738,8 @@ class Client(object):
|
|
|
1697
1738
|
group_properties: A dictionary of group properties.
|
|
1698
1739
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1699
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.
|
|
1700
1743
|
|
|
1701
1744
|
Examples:
|
|
1702
1745
|
```python
|
|
@@ -1713,6 +1756,7 @@ class Client(object):
|
|
|
1713
1756
|
group_properties=group_properties,
|
|
1714
1757
|
only_evaluate_locally=only_evaluate_locally,
|
|
1715
1758
|
disable_geoip=disable_geoip,
|
|
1759
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1716
1760
|
)
|
|
1717
1761
|
|
|
1718
1762
|
return response["featureFlags"]
|
|
@@ -1726,6 +1770,7 @@ class Client(object):
|
|
|
1726
1770
|
group_properties=None,
|
|
1727
1771
|
only_evaluate_locally=False,
|
|
1728
1772
|
disable_geoip=None,
|
|
1773
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1729
1774
|
) -> FlagsAndPayloads:
|
|
1730
1775
|
"""
|
|
1731
1776
|
Get all feature flags and their payloads for a user.
|
|
@@ -1737,6 +1782,8 @@ class Client(object):
|
|
|
1737
1782
|
group_properties: A dictionary of group properties.
|
|
1738
1783
|
only_evaluate_locally: Whether to only evaluate locally.
|
|
1739
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.
|
|
1740
1787
|
|
|
1741
1788
|
Examples:
|
|
1742
1789
|
```python
|
|
@@ -1760,6 +1807,7 @@ class Client(object):
|
|
|
1760
1807
|
groups=groups,
|
|
1761
1808
|
person_properties=person_properties,
|
|
1762
1809
|
group_properties=group_properties,
|
|
1810
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1763
1811
|
)
|
|
1764
1812
|
|
|
1765
1813
|
if fallback_to_decide and not only_evaluate_locally:
|
|
@@ -1770,6 +1818,7 @@ class Client(object):
|
|
|
1770
1818
|
person_properties=person_properties,
|
|
1771
1819
|
group_properties=group_properties,
|
|
1772
1820
|
disable_geoip=disable_geoip,
|
|
1821
|
+
flag_keys_to_evaluate=flag_keys_to_evaluate,
|
|
1773
1822
|
)
|
|
1774
1823
|
return to_flags_and_payloads(decide_response)
|
|
1775
1824
|
except Exception as e:
|
|
@@ -1787,6 +1836,7 @@ class Client(object):
|
|
|
1787
1836
|
person_properties=None,
|
|
1788
1837
|
group_properties=None,
|
|
1789
1838
|
warn_on_unknown_groups=False,
|
|
1839
|
+
flag_keys_to_evaluate: Optional[list[str]] = None,
|
|
1790
1840
|
) -> tuple[FlagsAndPayloads, bool]:
|
|
1791
1841
|
person_properties = person_properties or {}
|
|
1792
1842
|
group_properties = group_properties or {}
|
|
@@ -1799,7 +1849,15 @@ class Client(object):
|
|
|
1799
1849
|
fallback_to_decide = False
|
|
1800
1850
|
# If loading in previous line failed
|
|
1801
1851
|
if self.feature_flags:
|
|
1802
|
-
|
|
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:
|
|
1803
1861
|
try:
|
|
1804
1862
|
flags[flag["key"]] = self._compute_flag_locally(
|
|
1805
1863
|
flag,
|
|
@@ -1815,7 +1873,7 @@ class Client(object):
|
|
|
1815
1873
|
if matched_payload is not None:
|
|
1816
1874
|
payloads[flag["key"]] = matched_payload
|
|
1817
1875
|
except InconclusiveMatchError:
|
|
1818
|
-
# 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`
|
|
1819
1877
|
fallback_to_decide = True
|
|
1820
1878
|
except Exception as e:
|
|
1821
1879
|
self.log.exception(
|
|
@@ -1742,6 +1742,7 @@ class TestClient(unittest.TestCase):
|
|
|
1742
1742
|
person_properties={"distinct_id": "some_id"},
|
|
1743
1743
|
group_properties={},
|
|
1744
1744
|
geoip_disable=True,
|
|
1745
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1745
1746
|
)
|
|
1746
1747
|
patch_flags.reset_mock()
|
|
1747
1748
|
client.feature_enabled(
|
|
@@ -1756,6 +1757,7 @@ class TestClient(unittest.TestCase):
|
|
|
1756
1757
|
person_properties={"distinct_id": "feature_enabled_distinct_id"},
|
|
1757
1758
|
group_properties={},
|
|
1758
1759
|
geoip_disable=True,
|
|
1760
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1759
1761
|
)
|
|
1760
1762
|
patch_flags.reset_mock()
|
|
1761
1763
|
client.get_all_flags_and_payloads("all_flags_payloads_id")
|
|
@@ -1816,6 +1818,7 @@ class TestClient(unittest.TestCase):
|
|
|
1816
1818
|
"instance": {"$group_key": "app.posthog.com"},
|
|
1817
1819
|
},
|
|
1818
1820
|
geoip_disable=False,
|
|
1821
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1819
1822
|
)
|
|
1820
1823
|
|
|
1821
1824
|
patch_flags.reset_mock()
|
|
@@ -1842,6 +1845,7 @@ class TestClient(unittest.TestCase):
|
|
|
1842
1845
|
"instance": {"$group_key": "app.posthog.com"},
|
|
1843
1846
|
},
|
|
1844
1847
|
geoip_disable=False,
|
|
1848
|
+
flag_keys_to_evaluate=["random_key"],
|
|
1845
1849
|
)
|
|
1846
1850
|
|
|
1847
1851
|
patch_flags.reset_mock()
|
|
@@ -2187,6 +2191,7 @@ class TestClient(unittest.TestCase):
|
|
|
2187
2191
|
"only_evaluate_locally": None,
|
|
2188
2192
|
"person_properties": None,
|
|
2189
2193
|
"group_properties": None,
|
|
2194
|
+
"flag_keys_filter": None,
|
|
2190
2195
|
}
|
|
2191
2196
|
self.assertEqual(result, expected)
|
|
2192
2197
|
|
|
@@ -2197,6 +2202,7 @@ class TestClient(unittest.TestCase):
|
|
|
2197
2202
|
"only_evaluate_locally": None,
|
|
2198
2203
|
"person_properties": None,
|
|
2199
2204
|
"group_properties": None,
|
|
2205
|
+
"flag_keys_filter": None,
|
|
2200
2206
|
}
|
|
2201
2207
|
self.assertEqual(result, expected)
|
|
2202
2208
|
|
|
@@ -2212,6 +2218,7 @@ class TestClient(unittest.TestCase):
|
|
|
2212
2218
|
"only_evaluate_locally": True,
|
|
2213
2219
|
"person_properties": {"plan": "premium"},
|
|
2214
2220
|
"group_properties": {"company": {"type": "enterprise"}},
|
|
2221
|
+
"flag_keys_filter": None,
|
|
2215
2222
|
}
|
|
2216
2223
|
self.assertEqual(result, expected)
|
|
2217
2224
|
|
|
@@ -2223,6 +2230,7 @@ class TestClient(unittest.TestCase):
|
|
|
2223
2230
|
"only_evaluate_locally": None,
|
|
2224
2231
|
"person_properties": {"user_id": "123"},
|
|
2225
2232
|
"group_properties": None,
|
|
2233
|
+
"flag_keys_filter": None,
|
|
2226
2234
|
}
|
|
2227
2235
|
self.assertEqual(result, expected)
|
|
2228
2236
|
|
|
@@ -2233,6 +2241,7 @@ class TestClient(unittest.TestCase):
|
|
|
2233
2241
|
"only_evaluate_locally": None,
|
|
2234
2242
|
"person_properties": None,
|
|
2235
2243
|
"group_properties": None,
|
|
2244
|
+
"flag_keys_filter": None,
|
|
2236
2245
|
}
|
|
2237
2246
|
self.assertEqual(result, expected)
|
|
2238
2247
|
|
|
@@ -2249,6 +2258,53 @@ class TestClient(unittest.TestCase):
|
|
|
2249
2258
|
client._parse_send_feature_flags(None)
|
|
2250
2259
|
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
2251
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
|
+
|
|
2252
2308
|
@mock.patch("posthog.client.batch_post")
|
|
2253
2309
|
def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post):
|
|
2254
2310
|
"""Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string"""
|
|
@@ -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
|