posthoganalytics 6.7.0__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.
Files changed (40) hide show
  1. posthoganalytics/__init__.py +84 -7
  2. posthoganalytics/ai/anthropic/__init__.py +10 -0
  3. posthoganalytics/ai/anthropic/anthropic.py +95 -65
  4. posthoganalytics/ai/anthropic/anthropic_async.py +95 -65
  5. posthoganalytics/ai/anthropic/anthropic_converter.py +443 -0
  6. posthoganalytics/ai/gemini/__init__.py +15 -1
  7. posthoganalytics/ai/gemini/gemini.py +66 -71
  8. posthoganalytics/ai/gemini/gemini_async.py +423 -0
  9. posthoganalytics/ai/gemini/gemini_converter.py +652 -0
  10. posthoganalytics/ai/langchain/callbacks.py +58 -13
  11. posthoganalytics/ai/openai/__init__.py +16 -1
  12. posthoganalytics/ai/openai/openai.py +140 -149
  13. posthoganalytics/ai/openai/openai_async.py +127 -82
  14. posthoganalytics/ai/openai/openai_converter.py +741 -0
  15. posthoganalytics/ai/sanitization.py +248 -0
  16. posthoganalytics/ai/types.py +125 -0
  17. posthoganalytics/ai/utils.py +339 -356
  18. posthoganalytics/client.py +345 -97
  19. posthoganalytics/contexts.py +81 -0
  20. posthoganalytics/exception_utils.py +250 -2
  21. posthoganalytics/feature_flags.py +26 -10
  22. posthoganalytics/flag_definition_cache.py +127 -0
  23. posthoganalytics/integrations/django.py +157 -19
  24. posthoganalytics/request.py +203 -23
  25. posthoganalytics/test/test_client.py +250 -22
  26. posthoganalytics/test/test_exception_capture.py +418 -0
  27. posthoganalytics/test/test_feature_flag_result.py +441 -2
  28. posthoganalytics/test/test_feature_flags.py +308 -104
  29. posthoganalytics/test/test_flag_definition_cache.py +612 -0
  30. posthoganalytics/test/test_module.py +0 -8
  31. posthoganalytics/test/test_request.py +536 -0
  32. posthoganalytics/test/test_utils.py +4 -1
  33. posthoganalytics/types.py +40 -0
  34. posthoganalytics/version.py +1 -1
  35. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
  36. posthoganalytics-7.4.3.dist-info/RECORD +57 -0
  37. posthoganalytics-6.7.0.dist-info/RECORD +0 -49
  38. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
  39. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
  40. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ from parameterized import parameterized
9
9
 
10
10
  from posthoganalytics.client import Client
11
11
  from posthoganalytics.contexts import get_context_session_id, new_context, set_context_session
12
- from posthoganalytics.request import APIError
12
+ from posthoganalytics.request import APIError, GetResponse
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
@@ -198,12 +198,6 @@ class TestClient(unittest.TestCase):
198
198
  print(capture_call)
199
199
  self.assertEqual(capture_call[1]["distinct_id"], "distinct_id")
200
200
  self.assertEqual(capture_call[0][0], "$exception")
201
- self.assertEqual(
202
- capture_call[1]["properties"]["$exception_type"], "Exception"
203
- )
204
- self.assertEqual(
205
- capture_call[1]["properties"]["$exception_message"], "test exception"
206
- )
207
201
  self.assertEqual(
208
202
  capture_call[1]["properties"]["$exception_list"][0]["mechanism"][
209
203
  "type"
@@ -415,7 +409,9 @@ class TestClient(unittest.TestCase):
415
409
  )
416
410
  client.feature_flags = [multivariate_flag, basic_flag, false_flag]
417
411
 
418
- msg_uuid = client.capture("python test event", distinct_id="distinct_id")
412
+ msg_uuid = client.capture(
413
+ "python test event", distinct_id="distinct_id", send_feature_flags=True
414
+ )
419
415
  self.assertIsNotNone(msg_uuid)
420
416
  self.assertFalse(self.failed)
421
417
 
@@ -571,6 +567,7 @@ class TestClient(unittest.TestCase):
571
567
  "python test event",
572
568
  distinct_id="distinct_id",
573
569
  properties={"$feature/beta-feature-local": "my-custom-variant"},
570
+ send_feature_flags=True,
574
571
  )
575
572
  self.assertIsNotNone(msg_uuid)
576
573
  self.assertFalse(self.failed)
@@ -752,6 +749,178 @@ class TestClient(unittest.TestCase):
752
749
 
753
750
  self.assertEqual(patch_flags.call_count, 0)
754
751
 
752
+ @mock.patch("posthog.client.flags")
753
+ def test_capture_with_send_feature_flags_false_and_local_evaluation_doesnt_send_flags(
754
+ self, patch_flags
755
+ ):
756
+ """Test that send_feature_flags=False with local evaluation enabled does NOT send flags"""
757
+ patch_flags.return_value = {"featureFlags": {"beta-feature": "remote-variant"}}
758
+
759
+ multivariate_flag = {
760
+ "id": 1,
761
+ "name": "Beta Feature",
762
+ "key": "beta-feature-local",
763
+ "active": True,
764
+ "rollout_percentage": 100,
765
+ "filters": {
766
+ "groups": [
767
+ {
768
+ "rollout_percentage": 100,
769
+ },
770
+ ],
771
+ "multivariate": {
772
+ "variants": [
773
+ {
774
+ "key": "first-variant",
775
+ "name": "First Variant",
776
+ "rollout_percentage": 50,
777
+ },
778
+ {
779
+ "key": "second-variant",
780
+ "name": "Second Variant",
781
+ "rollout_percentage": 50,
782
+ },
783
+ ]
784
+ },
785
+ },
786
+ }
787
+ simple_flag = {
788
+ "id": 2,
789
+ "name": "Simple Flag",
790
+ "key": "simple-flag",
791
+ "active": True,
792
+ "filters": {
793
+ "groups": [
794
+ {
795
+ "rollout_percentage": 100,
796
+ }
797
+ ],
798
+ },
799
+ }
800
+
801
+ with mock.patch("posthog.client.batch_post") as mock_post:
802
+ client = Client(
803
+ FAKE_TEST_API_KEY,
804
+ on_error=self.set_fail,
805
+ personal_api_key=FAKE_TEST_API_KEY,
806
+ sync_mode=True,
807
+ )
808
+ client.feature_flags = [multivariate_flag, simple_flag]
809
+
810
+ msg_uuid = client.capture(
811
+ "python test event",
812
+ distinct_id="distinct_id",
813
+ send_feature_flags=False,
814
+ )
815
+ self.assertIsNotNone(msg_uuid)
816
+ self.assertFalse(self.failed)
817
+
818
+ # Get the enqueued message from the mock
819
+ mock_post.assert_called_once()
820
+ batch_data = mock_post.call_args[1]["batch"]
821
+ msg = batch_data[0]
822
+
823
+ self.assertEqual(msg["event"], "python test event")
824
+ self.assertEqual(msg["distinct_id"], "distinct_id")
825
+
826
+ # CRITICAL: Verify local flags are NOT included in the event
827
+ self.assertNotIn("$feature/beta-feature-local", msg["properties"])
828
+ self.assertNotIn("$feature/simple-flag", msg["properties"])
829
+ self.assertNotIn("$active_feature_flags", msg["properties"])
830
+
831
+ # CRITICAL: Verify the /flags API was NOT called
832
+ self.assertEqual(patch_flags.call_count, 0)
833
+
834
+ @mock.patch("posthog.client.flags")
835
+ def test_capture_with_send_feature_flags_true_and_local_evaluation_uses_local_flags(
836
+ self, patch_flags
837
+ ):
838
+ """Test that send_feature_flags=True with local evaluation enabled uses local flags without API call"""
839
+ patch_flags.return_value = {"featureFlags": {"remote-flag": "remote-variant"}}
840
+
841
+ multivariate_flag = {
842
+ "id": 1,
843
+ "name": "Beta Feature",
844
+ "key": "beta-feature-local",
845
+ "active": True,
846
+ "rollout_percentage": 100,
847
+ "filters": {
848
+ "groups": [
849
+ {
850
+ "rollout_percentage": 100,
851
+ },
852
+ ],
853
+ "multivariate": {
854
+ "variants": [
855
+ {
856
+ "key": "first-variant",
857
+ "name": "First Variant",
858
+ "rollout_percentage": 50,
859
+ },
860
+ {
861
+ "key": "second-variant",
862
+ "name": "Second Variant",
863
+ "rollout_percentage": 50,
864
+ },
865
+ ]
866
+ },
867
+ },
868
+ }
869
+ simple_flag = {
870
+ "id": 2,
871
+ "name": "Simple Flag",
872
+ "key": "simple-flag",
873
+ "active": True,
874
+ "filters": {
875
+ "groups": [
876
+ {
877
+ "rollout_percentage": 100,
878
+ }
879
+ ],
880
+ },
881
+ }
882
+
883
+ with mock.patch("posthog.client.batch_post") as mock_post:
884
+ client = Client(
885
+ FAKE_TEST_API_KEY,
886
+ on_error=self.set_fail,
887
+ personal_api_key=FAKE_TEST_API_KEY,
888
+ sync_mode=True,
889
+ )
890
+ client.feature_flags = [multivariate_flag, simple_flag]
891
+
892
+ msg_uuid = client.capture(
893
+ "python test event",
894
+ distinct_id="distinct_id",
895
+ send_feature_flags=True,
896
+ )
897
+ self.assertIsNotNone(msg_uuid)
898
+ self.assertFalse(self.failed)
899
+
900
+ # Get the enqueued message from the mock
901
+ mock_post.assert_called_once()
902
+ batch_data = mock_post.call_args[1]["batch"]
903
+ msg = batch_data[0]
904
+
905
+ self.assertEqual(msg["event"], "python test event")
906
+ self.assertEqual(msg["distinct_id"], "distinct_id")
907
+
908
+ # Verify local flags are included in the event
909
+ self.assertIn("$feature/beta-feature-local", msg["properties"])
910
+ self.assertIn("$feature/simple-flag", msg["properties"])
911
+ self.assertEqual(msg["properties"]["$feature/simple-flag"], True)
912
+
913
+ # Verify active feature flags are set correctly
914
+ active_flags = msg["properties"]["$active_feature_flags"]
915
+ self.assertIn("beta-feature-local", active_flags)
916
+ self.assertIn("simple-flag", active_flags)
917
+
918
+ # The remote flag should NOT be included since we used local evaluation
919
+ self.assertNotIn("$feature/remote-flag", msg["properties"])
920
+
921
+ # CRITICAL: Verify the /flags API was NOT called
922
+ self.assertEqual(patch_flags.call_count, 0)
923
+
755
924
  @mock.patch("posthog.client.flags")
756
925
  def test_capture_with_send_feature_flags_options_only_evaluate_locally_true(
757
926
  self, patch_flags
@@ -2095,13 +2264,21 @@ class TestClient(unittest.TestCase):
2095
2264
  self, patch_get, patch_poller
2096
2265
  ):
2097
2266
  """Test that when enable_local_evaluation=False, the poller is not started"""
2098
- patch_get.return_value = {
2099
- "flags": [
2100
- {"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
2101
- ],
2102
- "group_type_mapping": {},
2103
- "cohorts": {},
2104
- }
2267
+ patch_get.return_value = GetResponse(
2268
+ data={
2269
+ "flags": [
2270
+ {
2271
+ "id": 1,
2272
+ "name": "Beta Feature",
2273
+ "key": "beta-feature",
2274
+ "active": True,
2275
+ }
2276
+ ],
2277
+ "group_type_mapping": {},
2278
+ "cohorts": {},
2279
+ },
2280
+ etag='"test-etag"',
2281
+ )
2105
2282
 
2106
2283
  client = Client(
2107
2284
  FAKE_TEST_API_KEY,
@@ -2123,13 +2300,21 @@ class TestClient(unittest.TestCase):
2123
2300
  @mock.patch("posthog.client.get")
2124
2301
  def test_enable_local_evaluation_true_starts_poller(self, patch_get, patch_poller):
2125
2302
  """Test that when enable_local_evaluation=True (default), the poller is started"""
2126
- patch_get.return_value = {
2127
- "flags": [
2128
- {"id": 1, "name": "Beta Feature", "key": "beta-feature", "active": True}
2129
- ],
2130
- "group_type_mapping": {},
2131
- "cohorts": {},
2132
- }
2303
+ patch_get.return_value = GetResponse(
2304
+ data={
2305
+ "flags": [
2306
+ {
2307
+ "id": 1,
2308
+ "name": "Beta Feature",
2309
+ "key": "beta-feature",
2310
+ "active": True,
2311
+ }
2312
+ ],
2313
+ "group_type_mapping": {},
2314
+ "cohorts": {},
2315
+ },
2316
+ etag='"test-etag"',
2317
+ )
2133
2318
 
2134
2319
  client = Client(
2135
2320
  FAKE_TEST_API_KEY,
@@ -2423,3 +2608,46 @@ class TestClient(unittest.TestCase):
2423
2608
  batch_data = mock_post.call_args[1]["batch"]
2424
2609
  msg = batch_data[0]
2425
2610
  self.assertEqual(msg["properties"]["$context_tags"], ["random_tag"])
2611
+
2612
+ @mock.patch(
2613
+ "posthog.client.Client._enqueue", side_effect=Exception("Unexpected error")
2614
+ )
2615
+ def test_methods_handle_exceptions(self, mock_enqueue):
2616
+ """Test that all decorated methods handle exceptions gracefully."""
2617
+ client = Client("test-key")
2618
+
2619
+ test_cases = [
2620
+ ("capture", ["test_event"], {}),
2621
+ ("set", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2622
+ ("set_once", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2623
+ ("group_identify", ["group-type", "group-key"], {}),
2624
+ ("alias", ["some-id", "new-id"], {}),
2625
+ ]
2626
+
2627
+ for method_name, args, kwargs in test_cases:
2628
+ with self.subTest(method=method_name):
2629
+ method = getattr(client, method_name)
2630
+ result = method(*args, **kwargs)
2631
+ self.assertEqual(result, None)
2632
+
2633
+ @mock.patch(
2634
+ "posthog.client.Client._enqueue", side_effect=Exception("Expected error")
2635
+ )
2636
+ def test_debug_flag_re_raises_exceptions(self, mock_enqueue):
2637
+ """Test that methods re-raise exceptions when debug=True."""
2638
+ client = Client("test-key", debug=True)
2639
+
2640
+ test_cases = [
2641
+ ("capture", ["test_event"], {}),
2642
+ ("set", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2643
+ ("set_once", [], {"distinct_id": "some-id", "properties": {"a": "b"}}),
2644
+ ("group_identify", ["group-type", "group-key"], {}),
2645
+ ("alias", ["some-id", "new-id"], {}),
2646
+ ]
2647
+
2648
+ for method_name, args, kwargs in test_cases:
2649
+ with self.subTest(method=method_name):
2650
+ method = getattr(client, method_name)
2651
+ with self.assertRaises(Exception) as cm:
2652
+ method(*args, **kwargs)
2653
+ self.assertEqual(str(cm.exception), "Expected error")