posthoganalytics 6.7.5__py3-none-any.whl → 7.4.3__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/__init__.py +84 -7
- posthoganalytics/ai/anthropic/anthropic_async.py +30 -67
- posthoganalytics/ai/anthropic/anthropic_converter.py +40 -0
- posthoganalytics/ai/gemini/__init__.py +3 -0
- posthoganalytics/ai/gemini/gemini.py +1 -1
- posthoganalytics/ai/gemini/gemini_async.py +423 -0
- posthoganalytics/ai/gemini/gemini_converter.py +160 -24
- posthoganalytics/ai/langchain/callbacks.py +55 -11
- posthoganalytics/ai/openai/openai.py +27 -2
- posthoganalytics/ai/openai/openai_async.py +49 -5
- posthoganalytics/ai/openai/openai_converter.py +130 -0
- posthoganalytics/ai/sanitization.py +27 -5
- posthoganalytics/ai/types.py +1 -0
- posthoganalytics/ai/utils.py +32 -2
- posthoganalytics/client.py +338 -90
- posthoganalytics/contexts.py +81 -0
- posthoganalytics/exception_utils.py +250 -2
- posthoganalytics/feature_flags.py +26 -10
- posthoganalytics/flag_definition_cache.py +127 -0
- posthoganalytics/integrations/django.py +149 -50
- posthoganalytics/request.py +203 -23
- posthoganalytics/test/test_client.py +250 -22
- posthoganalytics/test/test_exception_capture.py +418 -0
- posthoganalytics/test/test_feature_flag_result.py +441 -2
- posthoganalytics/test/test_feature_flags.py +306 -102
- posthoganalytics/test/test_flag_definition_cache.py +612 -0
- posthoganalytics/test/test_module.py +0 -8
- posthoganalytics/test/test_request.py +536 -0
- posthoganalytics/test/test_utils.py +4 -1
- posthoganalytics/types.py +40 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
- posthoganalytics-7.4.3.dist-info/RECORD +57 -0
- posthoganalytics-6.7.5.dist-info/RECORD +0 -54
- {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,7 @@ from posthoganalytics.feature_flags import (
|
|
|
11
11
|
match_property,
|
|
12
12
|
relative_date_parse_for_feature_flag_matching,
|
|
13
13
|
)
|
|
14
|
-
from posthoganalytics.request import APIError
|
|
14
|
+
from posthoganalytics.request import APIError, GetResponse
|
|
15
15
|
from posthoganalytics.test.test_utils import FAKE_TEST_API_KEY
|
|
16
16
|
|
|
17
17
|
|
|
@@ -2348,23 +2348,27 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
2348
2348
|
@mock.patch("posthog.client.Poller")
|
|
2349
2349
|
@mock.patch("posthog.client.get")
|
|
2350
2350
|
def test_load_feature_flags(self, patch_get, patch_poll):
|
|
2351
|
-
patch_get.return_value =
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2351
|
+
patch_get.return_value = GetResponse(
|
|
2352
|
+
data={
|
|
2353
|
+
"flags": [
|
|
2354
|
+
{
|
|
2355
|
+
"id": 1,
|
|
2356
|
+
"name": "Beta Feature",
|
|
2357
|
+
"key": "beta-feature",
|
|
2358
|
+
"active": True,
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
"id": 2,
|
|
2362
|
+
"name": "Alpha Feature",
|
|
2363
|
+
"key": "alpha-feature",
|
|
2364
|
+
"active": False,
|
|
2365
|
+
},
|
|
2366
|
+
],
|
|
2367
|
+
"group_type_mapping": {"0": "company"},
|
|
2368
|
+
"cohorts": {},
|
|
2369
|
+
},
|
|
2370
|
+
etag='"abc123"',
|
|
2371
|
+
)
|
|
2368
2372
|
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2369
2373
|
with freeze_time("2020-01-01T12:01:00.0000Z"):
|
|
2370
2374
|
client.load_feature_flags()
|
|
@@ -2375,6 +2379,139 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
2375
2379
|
client._last_feature_flag_poll.isoformat(), "2020-01-01T12:01:00+00:00"
|
|
2376
2380
|
)
|
|
2377
2381
|
self.assertEqual(patch_poll.call_count, 1)
|
|
2382
|
+
# Verify ETag is stored
|
|
2383
|
+
self.assertEqual(client._flags_etag, '"abc123"')
|
|
2384
|
+
|
|
2385
|
+
@mock.patch("posthog.client.Poller")
|
|
2386
|
+
@mock.patch("posthog.client.get")
|
|
2387
|
+
def test_load_feature_flags_sends_etag_on_subsequent_requests(
|
|
2388
|
+
self, patch_get, patch_poll
|
|
2389
|
+
):
|
|
2390
|
+
"""Test that the ETag is sent in If-None-Match header on subsequent requests"""
|
|
2391
|
+
patch_get.return_value = GetResponse(
|
|
2392
|
+
data={
|
|
2393
|
+
"flags": [{"id": 1, "key": "beta-feature", "active": True}],
|
|
2394
|
+
"group_type_mapping": {},
|
|
2395
|
+
"cohorts": {},
|
|
2396
|
+
},
|
|
2397
|
+
etag='"initial-etag"',
|
|
2398
|
+
)
|
|
2399
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2400
|
+
client.load_feature_flags()
|
|
2401
|
+
|
|
2402
|
+
# First call should have no etag
|
|
2403
|
+
first_call_kwargs = patch_get.call_args_list[0][1]
|
|
2404
|
+
self.assertIsNone(first_call_kwargs.get("etag"))
|
|
2405
|
+
|
|
2406
|
+
# Simulate second call
|
|
2407
|
+
client._load_feature_flags()
|
|
2408
|
+
|
|
2409
|
+
# Second call should have the etag
|
|
2410
|
+
second_call_kwargs = patch_get.call_args_list[1][1]
|
|
2411
|
+
self.assertEqual(second_call_kwargs.get("etag"), '"initial-etag"')
|
|
2412
|
+
|
|
2413
|
+
@mock.patch("posthog.client.Poller")
|
|
2414
|
+
@mock.patch("posthog.client.get")
|
|
2415
|
+
def test_load_feature_flags_304_not_modified(self, patch_get, patch_poll):
|
|
2416
|
+
"""Test that 304 Not Modified responses skip flag processing"""
|
|
2417
|
+
# First response with flags
|
|
2418
|
+
initial_response = GetResponse(
|
|
2419
|
+
data={
|
|
2420
|
+
"flags": [{"id": 1, "key": "beta-feature", "active": True}],
|
|
2421
|
+
"group_type_mapping": {"0": "company"},
|
|
2422
|
+
"cohorts": {},
|
|
2423
|
+
},
|
|
2424
|
+
etag='"test-etag"',
|
|
2425
|
+
)
|
|
2426
|
+
# Second response is 304 Not Modified
|
|
2427
|
+
not_modified_response = GetResponse(
|
|
2428
|
+
data=None,
|
|
2429
|
+
etag='"test-etag"',
|
|
2430
|
+
not_modified=True,
|
|
2431
|
+
)
|
|
2432
|
+
patch_get.side_effect = [initial_response, not_modified_response]
|
|
2433
|
+
|
|
2434
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2435
|
+
client.load_feature_flags()
|
|
2436
|
+
|
|
2437
|
+
# Verify initial flags are loaded
|
|
2438
|
+
self.assertEqual(len(client.feature_flags), 1)
|
|
2439
|
+
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
|
2440
|
+
self.assertEqual(client.group_type_mapping, {"0": "company"})
|
|
2441
|
+
|
|
2442
|
+
# Second call with 304
|
|
2443
|
+
client._load_feature_flags()
|
|
2444
|
+
|
|
2445
|
+
# Flags should still be the same (not cleared)
|
|
2446
|
+
self.assertEqual(len(client.feature_flags), 1)
|
|
2447
|
+
self.assertEqual(client.feature_flags[0]["key"], "beta-feature")
|
|
2448
|
+
self.assertEqual(client.group_type_mapping, {"0": "company"})
|
|
2449
|
+
|
|
2450
|
+
@mock.patch("posthog.client.Poller")
|
|
2451
|
+
@mock.patch("posthog.client.get")
|
|
2452
|
+
def test_load_feature_flags_etag_updated_on_new_response(
|
|
2453
|
+
self, patch_get, patch_poll
|
|
2454
|
+
):
|
|
2455
|
+
"""Test that ETag is updated when flags change"""
|
|
2456
|
+
patch_get.side_effect = [
|
|
2457
|
+
GetResponse(
|
|
2458
|
+
data={
|
|
2459
|
+
"flags": [{"id": 1, "key": "flag-v1", "active": True}],
|
|
2460
|
+
"group_type_mapping": {},
|
|
2461
|
+
"cohorts": {},
|
|
2462
|
+
},
|
|
2463
|
+
etag='"etag-v1"',
|
|
2464
|
+
),
|
|
2465
|
+
GetResponse(
|
|
2466
|
+
data={
|
|
2467
|
+
"flags": [{"id": 1, "key": "flag-v2", "active": True}],
|
|
2468
|
+
"group_type_mapping": {},
|
|
2469
|
+
"cohorts": {},
|
|
2470
|
+
},
|
|
2471
|
+
etag='"etag-v2"',
|
|
2472
|
+
),
|
|
2473
|
+
]
|
|
2474
|
+
|
|
2475
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2476
|
+
client.load_feature_flags()
|
|
2477
|
+
self.assertEqual(client._flags_etag, '"etag-v1"')
|
|
2478
|
+
|
|
2479
|
+
client._load_feature_flags()
|
|
2480
|
+
self.assertEqual(client._flags_etag, '"etag-v2"')
|
|
2481
|
+
self.assertEqual(client.feature_flags[0]["key"], "flag-v2")
|
|
2482
|
+
|
|
2483
|
+
@mock.patch("posthog.client.Poller")
|
|
2484
|
+
@mock.patch("posthog.client.get")
|
|
2485
|
+
def test_load_feature_flags_clears_etag_when_server_stops_sending(
|
|
2486
|
+
self, patch_get, patch_poll
|
|
2487
|
+
):
|
|
2488
|
+
"""Test that ETag is cleared when server stops sending it"""
|
|
2489
|
+
patch_get.side_effect = [
|
|
2490
|
+
GetResponse(
|
|
2491
|
+
data={
|
|
2492
|
+
"flags": [{"id": 1, "key": "flag-v1", "active": True}],
|
|
2493
|
+
"group_type_mapping": {},
|
|
2494
|
+
"cohorts": {},
|
|
2495
|
+
},
|
|
2496
|
+
etag='"etag-v1"',
|
|
2497
|
+
),
|
|
2498
|
+
GetResponse(
|
|
2499
|
+
data={
|
|
2500
|
+
"flags": [{"id": 1, "key": "flag-v2", "active": True}],
|
|
2501
|
+
"group_type_mapping": {},
|
|
2502
|
+
"cohorts": {},
|
|
2503
|
+
},
|
|
2504
|
+
etag=None, # Server stopped sending ETag
|
|
2505
|
+
),
|
|
2506
|
+
]
|
|
2507
|
+
|
|
2508
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2509
|
+
client.load_feature_flags()
|
|
2510
|
+
self.assertEqual(client._flags_etag, '"etag-v1"')
|
|
2511
|
+
|
|
2512
|
+
client._load_feature_flags()
|
|
2513
|
+
self.assertIsNone(client._flags_etag)
|
|
2514
|
+
self.assertEqual(client.feature_flags[0]["key"], "flag-v2")
|
|
2378
2515
|
|
|
2379
2516
|
def test_load_feature_flags_wrong_key(self):
|
|
2380
2517
|
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
@@ -2804,73 +2941,61 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
2804
2941
|
self.assertEqual(patch_flags.call_count, 0)
|
|
2805
2942
|
|
|
2806
2943
|
@mock.patch("posthog.client.flags")
|
|
2807
|
-
def
|
|
2808
|
-
patch_flags.return_value = {"featureFlags": {"
|
|
2944
|
+
def test_conditions_evaluated_in_order(self, patch_flags):
|
|
2945
|
+
patch_flags.return_value = {"featureFlags": {"order-test": "server-variant"}}
|
|
2809
2946
|
client = Client(FAKE_TEST_API_KEY, personal_api_key="test")
|
|
2810
2947
|
client.feature_flags = [
|
|
2811
2948
|
{
|
|
2812
2949
|
"id": 1,
|
|
2813
|
-
"name": "
|
|
2814
|
-
"key": "
|
|
2950
|
+
"name": "Order Test Flag",
|
|
2951
|
+
"key": "order-test",
|
|
2815
2952
|
"active": True,
|
|
2816
|
-
"rollout_percentage": 100,
|
|
2817
2953
|
"filters": {
|
|
2818
2954
|
"groups": [
|
|
2819
2955
|
{
|
|
2820
2956
|
"rollout_percentage": 100,
|
|
2821
|
-
# The override applies even if the first condition matches all and gives everyone their default group
|
|
2822
2957
|
},
|
|
2823
2958
|
{
|
|
2824
2959
|
"properties": [
|
|
2825
2960
|
{
|
|
2826
2961
|
"key": "email",
|
|
2827
2962
|
"type": "person",
|
|
2828
|
-
"value": "
|
|
2829
|
-
"operator": "
|
|
2963
|
+
"value": "@vip.com",
|
|
2964
|
+
"operator": "icontains",
|
|
2830
2965
|
}
|
|
2831
2966
|
],
|
|
2832
2967
|
"rollout_percentage": 100,
|
|
2833
|
-
"variant": "
|
|
2968
|
+
"variant": "vip-variant",
|
|
2834
2969
|
},
|
|
2835
|
-
{"rollout_percentage": 50, "variant": "third-variant"},
|
|
2836
2970
|
],
|
|
2837
2971
|
"multivariate": {
|
|
2838
2972
|
"variants": [
|
|
2839
2973
|
{
|
|
2840
|
-
"key": "
|
|
2841
|
-
"name": "
|
|
2842
|
-
"rollout_percentage":
|
|
2843
|
-
},
|
|
2844
|
-
{
|
|
2845
|
-
"key": "second-variant",
|
|
2846
|
-
"name": "Second Variant",
|
|
2847
|
-
"rollout_percentage": 25,
|
|
2974
|
+
"key": "control",
|
|
2975
|
+
"name": "Control",
|
|
2976
|
+
"rollout_percentage": 100,
|
|
2848
2977
|
},
|
|
2849
2978
|
{
|
|
2850
|
-
"key": "
|
|
2851
|
-
"name": "
|
|
2852
|
-
"rollout_percentage":
|
|
2979
|
+
"key": "vip-variant",
|
|
2980
|
+
"name": "VIP Variant",
|
|
2981
|
+
"rollout_percentage": 0,
|
|
2853
2982
|
},
|
|
2854
2983
|
]
|
|
2855
2984
|
},
|
|
2856
2985
|
},
|
|
2857
2986
|
}
|
|
2858
2987
|
]
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
"
|
|
2866
|
-
)
|
|
2867
|
-
self.assertEqual(
|
|
2868
|
-
client.get_feature_flag("beta-feature", "example_id"), "third-variant"
|
|
2869
|
-
)
|
|
2870
|
-
self.assertEqual(
|
|
2871
|
-
client.get_feature_flag("beta-feature", "another_id"), "second-variant"
|
|
2988
|
+
|
|
2989
|
+
# Even though user@vip.com would match the second condition with variant override,
|
|
2990
|
+
# they should match the first condition and get control
|
|
2991
|
+
result = client.get_feature_flag(
|
|
2992
|
+
"order-test",
|
|
2993
|
+
"user123",
|
|
2994
|
+
person_properties={"email": "user@vip.com"},
|
|
2872
2995
|
)
|
|
2873
|
-
|
|
2996
|
+
self.assertEqual(result, "control")
|
|
2997
|
+
|
|
2998
|
+
# server not called because this can be evaluated locally
|
|
2874
2999
|
self.assertEqual(patch_flags.call_count, 0)
|
|
2875
3000
|
|
|
2876
3001
|
@mock.patch("posthog.client.flags")
|
|
@@ -2937,6 +3062,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
2937
3062
|
"some-distinct-id",
|
|
2938
3063
|
match_value=True,
|
|
2939
3064
|
person_properties={"region": "USA"},
|
|
3065
|
+
send_feature_flag_events=True,
|
|
2940
3066
|
),
|
|
2941
3067
|
300,
|
|
2942
3068
|
)
|
|
@@ -3025,6 +3151,75 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
3025
3151
|
)
|
|
3026
3152
|
self.assertEqual(patch_flags.call_count, 0)
|
|
3027
3153
|
|
|
3154
|
+
@mock.patch("posthog.client.flags")
|
|
3155
|
+
@mock.patch("posthog.client.get")
|
|
3156
|
+
def test_fallback_to_api_when_flag_has_static_cohort_in_multi_condition(
|
|
3157
|
+
self, patch_get, patch_flags
|
|
3158
|
+
):
|
|
3159
|
+
"""
|
|
3160
|
+
When a flag has multiple conditions and one contains a static cohort,
|
|
3161
|
+
the SDK should fallback to API for the entire flag, not just skip that
|
|
3162
|
+
condition and evaluate the next one locally.
|
|
3163
|
+
|
|
3164
|
+
This prevents returning wrong variants when later conditions could match
|
|
3165
|
+
locally but the user is actually in the static cohort.
|
|
3166
|
+
"""
|
|
3167
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
3168
|
+
|
|
3169
|
+
# Mock the local flags response - cohort 999 is NOT in cohorts map (static cohort)
|
|
3170
|
+
client.feature_flags = [
|
|
3171
|
+
{
|
|
3172
|
+
"id": 1,
|
|
3173
|
+
"key": "multi-condition-flag",
|
|
3174
|
+
"active": True,
|
|
3175
|
+
"filters": {
|
|
3176
|
+
"groups": [
|
|
3177
|
+
{
|
|
3178
|
+
"properties": [
|
|
3179
|
+
{"key": "id", "value": 999, "type": "cohort"}
|
|
3180
|
+
],
|
|
3181
|
+
"rollout_percentage": 100,
|
|
3182
|
+
"variant": "set-1",
|
|
3183
|
+
},
|
|
3184
|
+
{
|
|
3185
|
+
"properties": [
|
|
3186
|
+
{
|
|
3187
|
+
"key": "$geoip_country_code",
|
|
3188
|
+
"operator": "exact",
|
|
3189
|
+
"value": ["DE"],
|
|
3190
|
+
"type": "person",
|
|
3191
|
+
}
|
|
3192
|
+
],
|
|
3193
|
+
"rollout_percentage": 100,
|
|
3194
|
+
"variant": "set-8",
|
|
3195
|
+
},
|
|
3196
|
+
],
|
|
3197
|
+
"multivariate": {
|
|
3198
|
+
"variants": [
|
|
3199
|
+
{"key": "set-1", "rollout_percentage": 50},
|
|
3200
|
+
{"key": "set-8", "rollout_percentage": 50},
|
|
3201
|
+
]
|
|
3202
|
+
},
|
|
3203
|
+
},
|
|
3204
|
+
}
|
|
3205
|
+
]
|
|
3206
|
+
client.cohorts = {} # Note: cohort 999 is NOT here - it's a static cohort
|
|
3207
|
+
|
|
3208
|
+
# Mock the API response - user is in the static cohort
|
|
3209
|
+
patch_flags.return_value = {"featureFlags": {"multi-condition-flag": "set-1"}}
|
|
3210
|
+
|
|
3211
|
+
result = client.get_feature_flag(
|
|
3212
|
+
"multi-condition-flag",
|
|
3213
|
+
"test-distinct-id",
|
|
3214
|
+
person_properties={"$geoip_country_code": "DE"},
|
|
3215
|
+
)
|
|
3216
|
+
|
|
3217
|
+
# Should return the API result (set-1), not local evaluation (set-8)
|
|
3218
|
+
self.assertEqual(result, "set-1")
|
|
3219
|
+
|
|
3220
|
+
# Verify API was called (fallback occurred)
|
|
3221
|
+
self.assertEqual(patch_flags.call_count, 1)
|
|
3222
|
+
|
|
3028
3223
|
|
|
3029
3224
|
class TestMatchProperties(unittest.TestCase):
|
|
3030
3225
|
def property(self, key, value, operator=None):
|
|
@@ -3802,6 +3997,7 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3802
3997
|
},
|
|
3803
3998
|
},
|
|
3804
3999
|
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
|
|
4000
|
+
"evaluatedAt": 1234567890,
|
|
3805
4001
|
}
|
|
3806
4002
|
client = Client(FAKE_TEST_API_KEY)
|
|
3807
4003
|
|
|
@@ -3821,6 +4017,7 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3821
4017
|
"$feature_flag_id": 23,
|
|
3822
4018
|
"$feature_flag_version": 42,
|
|
3823
4019
|
"$feature_flag_request_id": "18043bf7-9cf6-44cd-b959-9662ee20d371",
|
|
4020
|
+
"$feature_flag_evaluated_at": 1234567890,
|
|
3824
4021
|
},
|
|
3825
4022
|
groups={},
|
|
3826
4023
|
disable_geoip=None,
|
|
@@ -3855,7 +4052,9 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3855
4052
|
|
|
3856
4053
|
self.assertEqual(
|
|
3857
4054
|
client.get_feature_flag_payload(
|
|
3858
|
-
"decide-flag-with-payload",
|
|
4055
|
+
"decide-flag-with-payload",
|
|
4056
|
+
"some-distinct-id",
|
|
4057
|
+
send_feature_flag_events=True,
|
|
3859
4058
|
),
|
|
3860
4059
|
{"foo": "bar"},
|
|
3861
4060
|
)
|
|
@@ -3931,9 +4130,10 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3931
4130
|
|
|
3932
4131
|
@mock.patch.object(Client, "capture")
|
|
3933
4132
|
@mock.patch("posthog.client.flags")
|
|
3934
|
-
def
|
|
4133
|
+
def test_get_feature_flag_payload_does_not_send_feature_flag_called_events(
|
|
3935
4134
|
self, patch_flags, patch_capture
|
|
3936
4135
|
):
|
|
4136
|
+
"""Test that get_feature_flag_payload does NOT send $feature_flag_called events"""
|
|
3937
4137
|
patch_flags.return_value = {
|
|
3938
4138
|
"featureFlags": {"person-flag": True},
|
|
3939
4139
|
"featureFlagPayloads": {"person-flag": 300},
|
|
@@ -3955,68 +4155,72 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3955
4155
|
"rollout_percentage": 100,
|
|
3956
4156
|
}
|
|
3957
4157
|
],
|
|
4158
|
+
"payloads": {"true": '"payload"'},
|
|
3958
4159
|
},
|
|
3959
4160
|
}
|
|
3960
4161
|
]
|
|
3961
4162
|
|
|
3962
|
-
|
|
3963
|
-
client.get_feature_flag_payload(
|
|
4163
|
+
payload = client.get_feature_flag_payload(
|
|
3964
4164
|
key="person-flag",
|
|
3965
4165
|
distinct_id="some-distinct-id",
|
|
3966
4166
|
person_properties={"region": "USA", "name": "Aloha"},
|
|
3967
4167
|
)
|
|
4168
|
+
self.assertIsNotNone(payload)
|
|
4169
|
+
self.assertEqual(patch_capture.call_count, 0)
|
|
3968
4170
|
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
"$feature/person-flag": True,
|
|
3979
|
-
},
|
|
3980
|
-
groups={},
|
|
3981
|
-
disable_geoip=None,
|
|
3982
|
-
)
|
|
3983
|
-
|
|
3984
|
-
# Reset mocks for further tests
|
|
3985
|
-
patch_capture.reset_mock()
|
|
3986
|
-
patch_flags.reset_mock()
|
|
4171
|
+
@mock.patch("posthog.client.flags")
|
|
4172
|
+
def test_fallback_to_api_in_get_feature_flag_payload_when_flag_has_static_cohort(
|
|
4173
|
+
self, patch_flags
|
|
4174
|
+
):
|
|
4175
|
+
"""
|
|
4176
|
+
Test that get_feature_flag_payload falls back to API when evaluating
|
|
4177
|
+
a flag with static cohorts, similar to get_feature_flag behavior.
|
|
4178
|
+
"""
|
|
4179
|
+
client = Client(FAKE_TEST_API_KEY, personal_api_key=FAKE_TEST_API_KEY)
|
|
3987
4180
|
|
|
3988
|
-
#
|
|
3989
|
-
client.
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
4181
|
+
# Mock the local flags response - cohort 999 is NOT in cohorts map (static cohort)
|
|
4182
|
+
client.feature_flags = [
|
|
4183
|
+
{
|
|
4184
|
+
"id": 1,
|
|
4185
|
+
"name": "Multi-condition Flag",
|
|
4186
|
+
"key": "multi-condition-flag",
|
|
4187
|
+
"active": True,
|
|
4188
|
+
"filters": {
|
|
4189
|
+
"groups": [
|
|
4190
|
+
{
|
|
4191
|
+
"properties": [
|
|
4192
|
+
{"key": "id", "value": 999, "type": "cohort"}
|
|
4193
|
+
],
|
|
4194
|
+
"rollout_percentage": 100,
|
|
4195
|
+
"variant": "variant-1",
|
|
4196
|
+
}
|
|
4197
|
+
],
|
|
4198
|
+
"multivariate": {
|
|
4199
|
+
"variants": [{"key": "variant-1", "rollout_percentage": 100}]
|
|
4200
|
+
},
|
|
4201
|
+
"payloads": {"variant-1": '{"message": "local-payload"}'},
|
|
4202
|
+
},
|
|
4203
|
+
}
|
|
4204
|
+
]
|
|
4205
|
+
client.cohorts = {} # Note: cohort 999 is NOT here - it's a static cohort
|
|
3994
4206
|
|
|
3995
|
-
|
|
3996
|
-
|
|
4207
|
+
# Mock the API response - user is in the static cohort
|
|
4208
|
+
patch_flags.return_value = {
|
|
4209
|
+
"featureFlags": {"multi-condition-flag": "variant-1"},
|
|
4210
|
+
"featureFlagPayloads": {"multi-condition-flag": '{"message": "from-api"}'},
|
|
4211
|
+
}
|
|
3997
4212
|
|
|
3998
|
-
# Call get_feature_flag_payload
|
|
3999
|
-
client.get_feature_flag_payload(
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
person_properties={"region": "USA", "name": "Aloha"},
|
|
4213
|
+
# Call get_feature_flag_payload without match_value to trigger evaluation
|
|
4214
|
+
result = client.get_feature_flag_payload(
|
|
4215
|
+
"multi-condition-flag",
|
|
4216
|
+
"test-distinct-id",
|
|
4003
4217
|
)
|
|
4004
4218
|
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
"$feature_flag_called",
|
|
4008
|
-
distinct_id="some-distinct-id2",
|
|
4009
|
-
properties={
|
|
4010
|
-
"$feature_flag": "person-flag",
|
|
4011
|
-
"$feature_flag_response": True,
|
|
4012
|
-
"locally_evaluated": True,
|
|
4013
|
-
"$feature/person-flag": True,
|
|
4014
|
-
},
|
|
4015
|
-
groups={},
|
|
4016
|
-
disable_geoip=None,
|
|
4017
|
-
)
|
|
4219
|
+
# Should return the API payload, not local payload
|
|
4220
|
+
self.assertEqual(result, {"message": "from-api"})
|
|
4018
4221
|
|
|
4019
|
-
|
|
4222
|
+
# Verify API was called (fallback occurred)
|
|
4223
|
+
self.assertEqual(patch_flags.call_count, 1)
|
|
4020
4224
|
|
|
4021
4225
|
@mock.patch.object(Client, "capture")
|
|
4022
4226
|
@mock.patch("posthog.client.flags")
|