@@ -751,6 +751,186 @@ def test_basic_capture_with_feature_flags_switched_off_doesnt_send_them(
751751
752752 self .assertEqual (patch_flags .call_count , 0 )
753753
754+ @mock .patch ("posthog.client.flags" )
755+ def test_capture_with_send_feature_flags_options_only_evaluate_locally_true (
756+ self , patch_flags
757+ ):
758+ """Test that SendFeatureFlagsOptions with only_evaluate_locally=True uses local evaluation"""
759+ with mock .patch ("posthog.client.batch_post" ) as mock_post :
760+ client = Client (
761+ FAKE_TEST_API_KEY ,
762+ on_error = self .set_fail ,
763+ personal_api_key = FAKE_TEST_API_KEY ,
764+ sync_mode = True ,
765+ )
766+
767+ # Set up local flags
768+ client .feature_flags = [
769+ {
770+ "id" : 1 ,
771+ "key" : "local-flag" ,
772+ "active" : True ,
773+ "filters" : {
774+ "groups" : [
775+ {
776+ "properties" : [{"key" : "region" , "value" : "US" }],
777+ "rollout_percentage" : 100 ,
778+ }
779+ ],
780+ },
781+ }
782+ ]
783+
784+ send_options = {
785+ "only_evaluate_locally" : True ,
786+ "person_properties" : {"region" : "US" },
787+ }
788+
789+ msg_uuid = client .capture (
790+ "test event" , distinct_id = "distinct_id" , send_feature_flags = send_options
791+ )
792+
793+ self .assertIsNotNone (msg_uuid )
794+ self .assertFalse (self .failed )
795+
796+ # Verify flags() was not called (no remote evaluation)
797+ patch_flags .assert_not_called ()
798+
799+ # Check the message includes the local flag
800+ mock_post .assert_called_once ()
801+ batch_data = mock_post .call_args [1 ]["batch" ]
802+ msg = batch_data [0 ]
803+
804+ self .assertEqual (msg ["properties" ]["$feature/local-flag" ], True )
805+ self .assertEqual (msg ["properties" ]["$active_feature_flags" ], ["local-flag" ])
806+
807+ @mock .patch ("posthog.client.flags" )
808+ def test_capture_with_send_feature_flags_options_only_evaluate_locally_false (
809+ self , patch_flags
810+ ):
811+ """Test that SendFeatureFlagsOptions with only_evaluate_locally=False forces remote evaluation"""
812+ patch_flags .return_value = {"featureFlags" : {"remote-flag" : "remote-value" }}
813+
814+ with mock .patch ("posthog.client.batch_post" ) as mock_post :
815+ client = Client (
816+ FAKE_TEST_API_KEY ,
817+ on_error = self .set_fail ,
818+ personal_api_key = FAKE_TEST_API_KEY ,
819+ sync_mode = True ,
820+ )
821+
822+ send_options = {
823+ "only_evaluate_locally" : False ,
824+ "person_properties" : {"plan" : "premium" },
825+ "group_properties" : {"company" : {"type" : "enterprise" }},
826+ }
827+
828+ msg_uuid = client .capture (
829+ "test event" ,
830+ distinct_id = "distinct_id" ,
831+ groups = {"company" : "acme" },
832+ send_feature_flags = send_options ,
833+ )
834+
835+ self .assertIsNotNone (msg_uuid )
836+ self .assertFalse (self .failed )
837+
838+ # Verify flags() was called with the correct properties
839+ patch_flags .assert_called_once ()
840+ call_args = patch_flags .call_args [1 ]
841+ self .assertEqual (call_args ["person_properties" ], {"plan" : "premium" })
842+ self .assertEqual (
843+ call_args ["group_properties" ], {"company" : {"type" : "enterprise" }}
844+ )
845+
846+ # Check the message includes the remote flag
847+ mock_post .assert_called_once ()
848+ batch_data = mock_post .call_args [1 ]["batch" ]
849+ msg = batch_data [0 ]
850+
851+ self .assertEqual (msg ["properties" ]["$feature/remote-flag" ], "remote-value" )
852+
853+ @mock .patch ("posthog.client.flags" )
854+ def test_capture_with_send_feature_flags_options_default_behavior (
855+ self , patch_flags
856+ ):
857+ """Test that SendFeatureFlagsOptions without only_evaluate_locally defaults to remote evaluation"""
858+ patch_flags .return_value = {"featureFlags" : {"default-flag" : "default-value" }}
859+
860+ with mock .patch ("posthog.client.batch_post" ) as mock_post :
861+ client = Client (
862+ FAKE_TEST_API_KEY ,
863+ on_error = self .set_fail ,
864+ personal_api_key = FAKE_TEST_API_KEY ,
865+ sync_mode = True ,
866+ )
867+
868+ send_options = {
869+ "person_properties" : {"subscription" : "pro" },
870+ }
871+
872+ msg_uuid = client .capture (
873+ "test event" , distinct_id = "distinct_id" , send_feature_flags = send_options
874+ )
875+
876+ self .assertIsNotNone (msg_uuid )
877+ self .assertFalse (self .failed )
878+
879+ # Verify flags() was called (default to remote evaluation)
880+ patch_flags .assert_called_once ()
881+ call_args = patch_flags .call_args [1 ]
882+ self .assertEqual (call_args ["person_properties" ], {"subscription" : "pro" })
883+
884+ # Check the message includes the flag
885+ mock_post .assert_called_once ()
886+ batch_data = mock_post .call_args [1 ]["batch" ]
887+ msg = batch_data [0 ]
888+
889+ self .assertEqual (
890+ msg ["properties" ]["$feature/default-flag" ], "default-value"
891+ )
892+
893+ @mock .patch ("posthog.client.flags" )
894+ def test_capture_exception_with_send_feature_flags_options (self , patch_flags ):
895+ """Test that capture_exception also supports SendFeatureFlagsOptions"""
896+ patch_flags .return_value = {"featureFlags" : {"exception-flag" : True }}
897+
898+ with mock .patch ("posthog.client.batch_post" ) as mock_post :
899+ client = Client (
900+ FAKE_TEST_API_KEY ,
901+ on_error = self .set_fail ,
902+ personal_api_key = FAKE_TEST_API_KEY ,
903+ sync_mode = True ,
904+ )
905+
906+ send_options = {
907+ "only_evaluate_locally" : False ,
908+ "person_properties" : {"user_type" : "admin" },
909+ }
910+
911+ try :
912+ raise ValueError ("Test exception" )
913+ except ValueError as e :
914+ msg_uuid = client .capture_exception (
915+ e , distinct_id = "distinct_id" , send_feature_flags = send_options
916+ )
917+
918+ self .assertIsNotNone (msg_uuid )
919+ self .assertFalse (self .failed )
920+
921+ # Verify flags() was called with the correct properties
922+ patch_flags .assert_called_once ()
923+ call_args = patch_flags .call_args [1 ]
924+ self .assertEqual (call_args ["person_properties" ], {"user_type" : "admin" })
925+
926+ # Check the message includes the flag
927+ mock_post .assert_called_once ()
928+ batch_data = mock_post .call_args [1 ]["batch" ]
929+ msg = batch_data [0 ]
930+
931+ self .assertEqual (msg ["event" ], "$exception" )
932+ self .assertEqual (msg ["properties" ]["$feature/exception-flag" ], True )
933+
754934 def test_stringifies_distinct_id (self ):
755935 # A large number that loses precision in node:
756936 # node -e "console.log(157963456373623802 + 1)" > 157963456373623800
@@ -1591,7 +1771,7 @@ def test_disable_geoip_default_on_decide(self, patch_flags):
15911771
15921772 @mock .patch ("posthog.client.Poller" )
15931773 @mock .patch ("posthog.client.get" )
1594- def test_call_identify_fails (self , patch_get , patch_poll ):
1774+ def test_call_identify_fails (self , patch_get , patch_poller ):
15951775 def raise_effect ():
15961776 raise Exception ("http exception" )
15971777
@@ -1993,3 +2173,76 @@ def test_get_remote_config_payload_requires_personal_api_key(self):
19932173 result = client .get_remote_config_payload ("test-flag" )
19942174
19952175 self .assertIsNone (result )
2176+
2177+ def test_parse_send_feature_flags_method (self ):
2178+ """Test the _parse_send_feature_flags helper method"""
2179+ client = Client (FAKE_TEST_API_KEY , sync_mode = True )
2180+
2181+ # Test boolean True
2182+ result = client ._parse_send_feature_flags (True )
2183+ expected = {
2184+ "should_send" : True ,
2185+ "only_evaluate_locally" : None ,
2186+ "person_properties" : None ,
2187+ "group_properties" : None ,
2188+ }
2189+ self .assertEqual (result , expected )
2190+
2191+ # Test boolean False
2192+ result = client ._parse_send_feature_flags (False )
2193+ expected = {
2194+ "should_send" : False ,
2195+ "only_evaluate_locally" : None ,
2196+ "person_properties" : None ,
2197+ "group_properties" : None ,
2198+ }
2199+ self .assertEqual (result , expected )
2200+
2201+ # Test options dict with all fields
2202+ options = {
2203+ "only_evaluate_locally" : True ,
2204+ "person_properties" : {"plan" : "premium" },
2205+ "group_properties" : {"company" : {"type" : "enterprise" }},
2206+ }
2207+ result = client ._parse_send_feature_flags (options )
2208+ expected = {
2209+ "should_send" : True ,
2210+ "only_evaluate_locally" : True ,
2211+ "person_properties" : {"plan" : "premium" },
2212+ "group_properties" : {"company" : {"type" : "enterprise" }},
2213+ }
2214+ self .assertEqual (result , expected )
2215+
2216+ # Test options dict with partial fields
2217+ options = {"person_properties" : {"user_id" : "123" }}
2218+ result = client ._parse_send_feature_flags (options )
2219+ expected = {
2220+ "should_send" : True ,
2221+ "only_evaluate_locally" : None ,
2222+ "person_properties" : {"user_id" : "123" },
2223+ "group_properties" : None ,
2224+ }
2225+ self .assertEqual (result , expected )
2226+
2227+ # Test empty dict
2228+ result = client ._parse_send_feature_flags ({})
2229+ expected = {
2230+ "should_send" : True ,
2231+ "only_evaluate_locally" : None ,
2232+ "person_properties" : None ,
2233+ "group_properties" : None ,
2234+ }
2235+ self .assertEqual (result , expected )
2236+
2237+ # Test invalid types
2238+ with self .assertRaises (TypeError ) as cm :
2239+ client ._parse_send_feature_flags ("invalid" )
2240+ self .assertIn ("Invalid type for send_feature_flags" , str (cm .exception ))
2241+
2242+ with self .assertRaises (TypeError ) as cm :
2243+ client ._parse_send_feature_flags (123 )
2244+ self .assertIn ("Invalid type for send_feature_flags" , str (cm .exception ))
2245+
2246+ with self .assertRaises (TypeError ) as cm :
2247+ client ._parse_send_feature_flags (None )
2248+ self .assertIn ("Invalid type for send_feature_flags" , str (cm .exception ))
0 commit comments