1818from posthog .exception_utils import exc_info_from_error , exceptions_from_error_tuple , handle_in_app
1919from posthog .feature_flags import InconclusiveMatchError , match_feature_flag_properties
2020from posthog .poller import Poller
21- from posthog .request import DEFAULT_HOST , APIError , batch_post , decide , determine_server_host , get , remote_config , DecideResponse
21+ from posthog .request import (
22+ DEFAULT_HOST ,
23+ APIError ,
24+ batch_post ,
25+ decide ,
26+ determine_server_host ,
27+ get ,
28+ remote_config ,
29+ DecideResponse ,
30+ )
2231from posthog .utils import SizeLimitedDict , clean , guess_timezone , remove_trailing_slash
2332from posthog .version import VERSION
24- from posthog .types import FlagsAndPayloads , FlagValue , to_values , to_payloads , to_flags_and_payloads , normalize_decide_response
33+ from posthog .types import (
34+ FlagsAndPayloads ,
35+ FeatureFlag ,
36+ FlagValue ,
37+ FlagMetadata ,
38+ to_values ,
39+ to_payloads ,
40+ to_flags_and_payloads ,
41+ normalize_decide_response ,
42+ )
43+
2544try :
2645 import queue
2746except ImportError :
@@ -217,10 +236,10 @@ def feature_flags(self, flags):
217236 Set the local evaluation feature flags.
218237 """
219238 self ._feature_flags = flags or []
220- self .feature_flags_by_key = {
221- flag [ "key" ]: flag for flag in self . _feature_flags if flag . get ( "key" ) is not None
222- }
223- assert self . feature_flags_by_key is not None , "feature_flags_by_key should be initialized when feature_flags is set"
239+ self .feature_flags_by_key = {flag [ "key" ]: flag for flag in self . _feature_flags if flag . get ( "key" ) is not None }
240+ assert (
241+ self . feature_flags_by_key is not None
242+ ) , "feature_flags_by_key should be initialized when feature_flags is set"
224243
225244 def identify (self , distinct_id = None , properties = None , context = None , timestamp = None , uuid = None , disable_geoip = None ):
226245 if context is not None :
@@ -271,7 +290,9 @@ def get_feature_flags_and_payloads(
271290 resp = self .get_decide (distinct_id , groups , person_properties , group_properties , disable_geoip )
272291 return to_flags_and_payloads (resp )
273292
274- def get_decide (self , distinct_id , groups = None , person_properties = None , group_properties = None , disable_geoip = None ) -> DecideResponse :
293+ def get_decide (
294+ self , distinct_id , groups = None , person_properties = None , group_properties = None , disable_geoip = None
295+ ) -> DecideResponse :
275296 require ("distinct_id" , distinct_id , ID_TYPES )
276297
277298 if disable_geoip is None :
@@ -802,7 +823,7 @@ def get_feature_flag(
802823 ) -> FlagValue | None :
803824 """
804825 Get a feature flag value for a key by evaluating locally or remotely
805- depending on whether local evaluation is enabled and the flag can be
826+ depending on whether local evaluation is enabled and the flag can be
806827 locally evaluated.
807828
808829 This also captures the $feature_flag_called event unless send_feature_flag_events is False.
@@ -820,50 +841,51 @@ def get_feature_flag(
820841
821842 response = self ._locally_evaluate_flag (key , distinct_id , groups , person_properties , group_properties )
822843
844+ flag_details = None
845+ request_id = None
846+
823847 flag_was_locally_evaluated = response is not None
824848 if not flag_was_locally_evaluated and not only_evaluate_locally :
825849 try :
826- feature_flags = self .get_feature_variants (
827- distinct_id ,
828- groups = groups ,
829- person_properties = person_properties ,
830- group_properties = group_properties ,
831- disable_geoip = disable_geoip ,
850+ flag_details , request_id = self ._get_feature_flag_details_from_decide (
851+ key , distinct_id , groups , person_properties , group_properties , disable_geoip
832852 )
833- response = feature_flags .get (key )
834- if response is None :
835- response = False
853+ response = flag_details .get_value () if flag_details else False
836854 self .log .debug (f"Successfully computed flag remotely: #{ key } -> #{ response } " )
837855 except Exception as e :
838856 self .log .exception (f"[FEATURE FLAGS] Unable to get flag remotely: { e } " )
839857
840- feature_flag_reported_key = f"{ key } _{ str (response )} "
841- if (
842- feature_flag_reported_key not in self .distinct_ids_feature_flags_reported [distinct_id ]
843- and send_feature_flag_events # noqa: W503
844- ):
845- self .capture (
858+ if send_feature_flag_events :
859+ self ._capture_feature_flag_called (
846860 distinct_id ,
847- "$feature_flag_called" ,
848- {
849- "$feature_flag" : key ,
850- "$feature_flag_response" : response ,
851- "locally_evaluated" : flag_was_locally_evaluated ,
852- f"$feature/{ key } " : response ,
853- },
854- groups = groups ,
855- disable_geoip = disable_geoip ,
861+ key ,
862+ response ,
863+ None ,
864+ flag_was_locally_evaluated ,
865+ groups ,
866+ disable_geoip ,
867+ request_id ,
868+ flag_details ,
856869 )
857- self . distinct_ids_feature_flags_reported [ distinct_id ]. add ( feature_flag_reported_key )
870+
858871 return response
859872
860- def _locally_evaluate_flag (self , key : str , distinct_id : str , groups : dict [str , str ], person_properties : dict [str , str ], group_properties : dict [str , str ]) -> FlagValue | None :
873+ def _locally_evaluate_flag (
874+ self ,
875+ key : str ,
876+ distinct_id : str ,
877+ groups : dict [str , str ],
878+ person_properties : dict [str , str ],
879+ group_properties : dict [str , str ],
880+ ) -> FlagValue | None :
861881 if self .feature_flags is None and self .personal_api_key :
862882 self .load_feature_flags ()
863883 response = None
864884
865885 if self .feature_flags :
866- assert self .feature_flags_by_key is not None , "feature_flags_by_key should be initialized when feature_flags is set"
886+ assert (
887+ self .feature_flags_by_key is not None
888+ ), "feature_flags_by_key should be initialized when feature_flags is set"
867889 # Local evaluation
868890 flag = self .feature_flags_by_key .get (key )
869891 if flag :
@@ -906,49 +928,101 @@ def get_feature_flag_payload(
906928
907929 response = None
908930 payload = None
931+ flag_details = None
932+ request_id = None
909933
910934 if match_value is not None :
911935 payload = self ._compute_payload_locally (key , match_value )
912936
913937 flag_was_locally_evaluated = payload is not None
914938 if not flag_was_locally_evaluated and not only_evaluate_locally :
915939 try :
916- responses_and_payloads = self .get_feature_flags_and_payloads (
917- distinct_id , groups , person_properties , group_properties , disable_geoip
940+ flag_details , request_id = self ._get_feature_flag_details_from_decide (
941+ key , distinct_id , groups , person_properties , group_properties , disable_geoip
918942 )
919- featureFlags = responses_and_payloads ["featureFlags" ]
920- if featureFlags is not None :
921- response = featureFlags .get (key , None )
922-
923- featureFlagPayloads = responses_and_payloads ["featureFlagPayloads" ]
924- if featureFlagPayloads is not None :
925- payload = featureFlagPayloads .get (str (key ), None )
943+ payload = flag_details .metadata .payload if flag_details else None
944+ response = flag_details .get_value () if flag_details else False
926945 except Exception as e :
927946 self .log .exception (f"[FEATURE FLAGS] Unable to get feature flags and payloads: { e } " )
928947
948+ if send_feature_flag_events :
949+ self ._capture_feature_flag_called (
950+ distinct_id ,
951+ key ,
952+ response ,
953+ payload ,
954+ flag_was_locally_evaluated ,
955+ groups ,
956+ disable_geoip ,
957+ request_id ,
958+ flag_details ,
959+ )
960+
961+ return payload
962+
963+ def _get_feature_flag_details_from_decide (
964+ self ,
965+ key : str ,
966+ distinct_id : str ,
967+ groups : dict [str , str ],
968+ person_properties : dict [str , str ],
969+ group_properties : dict [str , str ],
970+ disable_geoip : bool | None ,
971+ ) -> tuple [FeatureFlag | None , str ]:
972+ """
973+ Calls /decide and returns the flag details and request id
974+ """
975+ resp_data = self .get_decide (distinct_id , groups , person_properties , group_properties , disable_geoip )
976+ request_id = resp_data .get ("requestId" )
977+ flags = resp_data .get ("flags" )
978+ flag_details = flags .get (key )
979+ return flag_details , request_id
980+
981+ def _capture_feature_flag_called (
982+ self ,
983+ distinct_id : str ,
984+ key : str ,
985+ response : FlagValue ,
986+ payload : str | None ,
987+ flag_was_locally_evaluated : bool ,
988+ groups : dict [str , str ],
989+ disable_geoip : bool | None ,
990+ request_id : str | None ,
991+ flag_details : FeatureFlag | None ,
992+ ):
929993 feature_flag_reported_key = f"{ key } _{ str (response )} "
930994
931- if (
932- feature_flag_reported_key not in self .distinct_ids_feature_flags_reported [distinct_id ]
933- and send_feature_flag_events # noqa: W503
934- ):
995+ if feature_flag_reported_key not in self .distinct_ids_feature_flags_reported [distinct_id ]:
996+ properties = {
997+ "$feature_flag" : key ,
998+ "$feature_flag_response" : response ,
999+ "locally_evaluated" : flag_was_locally_evaluated ,
1000+ f"$feature/{ key } " : response ,
1001+ }
1002+
1003+ if payload :
1004+ properties ["$feature_flag_payload" ] = payload
1005+
1006+ if request_id :
1007+ properties ["$feature_flag_request_id" ] = request_id
1008+ if isinstance (flag_details , FeatureFlag ):
1009+ if flag_details .reason and flag_details .reason .description :
1010+ properties ["$feature_flag_reason" ] = flag_details .reason .description
1011+ if isinstance (flag_details .metadata , FlagMetadata ):
1012+ if flag_details .metadata .version :
1013+ properties ["$feature_flag_version" ] = flag_details .metadata .version
1014+ if flag_details .metadata .id :
1015+ properties ["$feature_flag_id" ] = flag_details .metadata .id
1016+
9351017 self .capture (
9361018 distinct_id ,
9371019 "$feature_flag_called" ,
938- {
939- "$feature_flag" : key ,
940- "$feature_flag_response" : response ,
941- "$feature_flag_payload" : payload ,
942- "locally_evaluated" : flag_was_locally_evaluated ,
943- f"$feature/{ key } " : response ,
944- },
1020+ properties ,
9451021 groups = groups ,
9461022 disable_geoip = disable_geoip ,
9471023 )
9481024 self .distinct_ids_feature_flags_reported [distinct_id ].add (feature_flag_reported_key )
9491025
950- return payload
951-
9521026 def get_remote_config_payload (self , key : str ):
9531027 if self .disabled :
9541028 return None
@@ -1042,7 +1116,6 @@ def get_all_flags_and_payloads(
10421116
10431117 return response
10441118
1045-
10461119 def _get_all_flags_and_payloads_locally (
10471120 self , distinct_id , * , groups = {}, person_properties = {}, group_properties = {}, warn_on_unknown_groups = False
10481121 ) -> tuple [FlagsAndPayloads , bool ]:
0 commit comments