posthoganalytics 7.0.1__py3-none-any.whl → 7.4.1__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 +10 -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 +87 -21
- posthoganalytics/ai/openai/openai.py +27 -2
- posthoganalytics/ai/openai/openai_async.py +27 -2
- posthoganalytics/ai/openai/openai_converter.py +6 -0
- posthoganalytics/ai/sanitization.py +27 -5
- posthoganalytics/ai/utils.py +2 -2
- posthoganalytics/client.py +224 -58
- posthoganalytics/exception_utils.py +49 -4
- posthoganalytics/flag_definition_cache.py +127 -0
- posthoganalytics/request.py +203 -23
- posthoganalytics/test/test_client.py +207 -22
- posthoganalytics/test/test_exception_capture.py +45 -1
- posthoganalytics/test/test_feature_flag_result.py +441 -2
- posthoganalytics/test/test_feature_flags.py +166 -73
- posthoganalytics/test/test_flag_definition_cache.py +612 -0
- 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-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/METADATA +2 -1
- {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/RECORD +28 -25
- {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/WHEEL +0 -0
- {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-7.0.1.dist-info → posthoganalytics-7.4.1.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)
|
|
@@ -2925,6 +3062,7 @@ class TestLocalEvaluation(unittest.TestCase):
|
|
|
2925
3062
|
"some-distinct-id",
|
|
2926
3063
|
match_value=True,
|
|
2927
3064
|
person_properties={"region": "USA"},
|
|
3065
|
+
send_feature_flag_events=True,
|
|
2928
3066
|
),
|
|
2929
3067
|
300,
|
|
2930
3068
|
)
|
|
@@ -3859,6 +3997,7 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3859
3997
|
},
|
|
3860
3998
|
},
|
|
3861
3999
|
"requestId": "18043bf7-9cf6-44cd-b959-9662ee20d371",
|
|
4000
|
+
"evaluatedAt": 1234567890,
|
|
3862
4001
|
}
|
|
3863
4002
|
client = Client(FAKE_TEST_API_KEY)
|
|
3864
4003
|
|
|
@@ -3878,6 +4017,7 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3878
4017
|
"$feature_flag_id": 23,
|
|
3879
4018
|
"$feature_flag_version": 42,
|
|
3880
4019
|
"$feature_flag_request_id": "18043bf7-9cf6-44cd-b959-9662ee20d371",
|
|
4020
|
+
"$feature_flag_evaluated_at": 1234567890,
|
|
3881
4021
|
},
|
|
3882
4022
|
groups={},
|
|
3883
4023
|
disable_geoip=None,
|
|
@@ -3912,7 +4052,9 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3912
4052
|
|
|
3913
4053
|
self.assertEqual(
|
|
3914
4054
|
client.get_feature_flag_payload(
|
|
3915
|
-
"decide-flag-with-payload",
|
|
4055
|
+
"decide-flag-with-payload",
|
|
4056
|
+
"some-distinct-id",
|
|
4057
|
+
send_feature_flag_events=True,
|
|
3916
4058
|
),
|
|
3917
4059
|
{"foo": "bar"},
|
|
3918
4060
|
)
|
|
@@ -3988,9 +4130,10 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
3988
4130
|
|
|
3989
4131
|
@mock.patch.object(Client, "capture")
|
|
3990
4132
|
@mock.patch("posthog.client.flags")
|
|
3991
|
-
def
|
|
4133
|
+
def test_get_feature_flag_payload_does_not_send_feature_flag_called_events(
|
|
3992
4134
|
self, patch_flags, patch_capture
|
|
3993
4135
|
):
|
|
4136
|
+
"""Test that get_feature_flag_payload does NOT send $feature_flag_called events"""
|
|
3994
4137
|
patch_flags.return_value = {
|
|
3995
4138
|
"featureFlags": {"person-flag": True},
|
|
3996
4139
|
"featureFlagPayloads": {"person-flag": 300},
|
|
@@ -4012,68 +4155,18 @@ class TestCaptureCalls(unittest.TestCase):
|
|
|
4012
4155
|
"rollout_percentage": 100,
|
|
4013
4156
|
}
|
|
4014
4157
|
],
|
|
4158
|
+
"payloads": {"true": '"payload"'},
|
|
4015
4159
|
},
|
|
4016
4160
|
}
|
|
4017
4161
|
]
|
|
4018
4162
|
|
|
4019
|
-
|
|
4020
|
-
client.get_feature_flag_payload(
|
|
4021
|
-
key="person-flag",
|
|
4022
|
-
distinct_id="some-distinct-id",
|
|
4023
|
-
person_properties={"region": "USA", "name": "Aloha"},
|
|
4024
|
-
)
|
|
4025
|
-
|
|
4026
|
-
# Assert that capture was called once, with the correct parameters
|
|
4027
|
-
self.assertEqual(patch_capture.call_count, 1)
|
|
4028
|
-
patch_capture.assert_called_with(
|
|
4029
|
-
"$feature_flag_called",
|
|
4030
|
-
distinct_id="some-distinct-id",
|
|
4031
|
-
properties={
|
|
4032
|
-
"$feature_flag": "person-flag",
|
|
4033
|
-
"$feature_flag_response": True,
|
|
4034
|
-
"locally_evaluated": True,
|
|
4035
|
-
"$feature/person-flag": True,
|
|
4036
|
-
},
|
|
4037
|
-
groups={},
|
|
4038
|
-
disable_geoip=None,
|
|
4039
|
-
)
|
|
4040
|
-
|
|
4041
|
-
# Reset mocks for further tests
|
|
4042
|
-
patch_capture.reset_mock()
|
|
4043
|
-
patch_flags.reset_mock()
|
|
4044
|
-
|
|
4045
|
-
# Call get_feature_flag_payload again for the same user; capture should not be called again because we've already reported an event for this distinct_id + flag
|
|
4046
|
-
client.get_feature_flag_payload(
|
|
4163
|
+
payload = client.get_feature_flag_payload(
|
|
4047
4164
|
key="person-flag",
|
|
4048
4165
|
distinct_id="some-distinct-id",
|
|
4049
4166
|
person_properties={"region": "USA", "name": "Aloha"},
|
|
4050
4167
|
)
|
|
4051
|
-
|
|
4168
|
+
self.assertIsNotNone(payload)
|
|
4052
4169
|
self.assertEqual(patch_capture.call_count, 0)
|
|
4053
|
-
patch_capture.reset_mock()
|
|
4054
|
-
|
|
4055
|
-
# Call get_feature_flag_payload for a different user; capture should be called
|
|
4056
|
-
client.get_feature_flag_payload(
|
|
4057
|
-
key="person-flag",
|
|
4058
|
-
distinct_id="some-distinct-id2",
|
|
4059
|
-
person_properties={"region": "USA", "name": "Aloha"},
|
|
4060
|
-
)
|
|
4061
|
-
|
|
4062
|
-
self.assertEqual(patch_capture.call_count, 1)
|
|
4063
|
-
patch_capture.assert_called_with(
|
|
4064
|
-
"$feature_flag_called",
|
|
4065
|
-
distinct_id="some-distinct-id2",
|
|
4066
|
-
properties={
|
|
4067
|
-
"$feature_flag": "person-flag",
|
|
4068
|
-
"$feature_flag_response": True,
|
|
4069
|
-
"locally_evaluated": True,
|
|
4070
|
-
"$feature/person-flag": True,
|
|
4071
|
-
},
|
|
4072
|
-
groups={},
|
|
4073
|
-
disable_geoip=None,
|
|
4074
|
-
)
|
|
4075
|
-
|
|
4076
|
-
patch_capture.reset_mock()
|
|
4077
4170
|
|
|
4078
4171
|
@mock.patch("posthog.client.flags")
|
|
4079
4172
|
def test_fallback_to_api_in_get_feature_flag_payload_when_flag_has_static_cohort(
|