20
20
from .config_manager import AuthDatafilePollingConfigManager
21
21
from .config_manager import PollingConfigManager
22
22
from .config_manager import StaticConfigManager
23
+ from .decision .decide_option import DecideOption
24
+ from .decision .decision import Decision
25
+ from .decision .decision_message import DecisionMessage
23
26
from .error_handler import NoOpErrorHandler as noop_error_handler
24
27
from .event import event_factory , user_event_factory
25
28
from .event .event_processor import ForwardingEventProcessor
26
29
from .event_dispatcher import EventDispatcher as default_event_dispatcher
27
30
from .helpers import enums , validator
31
+ from .helpers .enums import DecisionSources
28
32
from .notification_center import NotificationCenter
29
33
from .optimizely_config import OptimizelyConfigService
30
34
from .user_context import UserContext
@@ -34,18 +38,19 @@ class Optimizely(object):
34
38
""" Class encapsulating all SDK functionality. """
35
39
36
40
def __init__ (
37
- self ,
38
- datafile = None ,
39
- event_dispatcher = None ,
40
- logger = None ,
41
- error_handler = None ,
42
- skip_json_validation = False ,
43
- user_profile_service = None ,
44
- sdk_key = None ,
45
- config_manager = None ,
46
- notification_center = None ,
47
- event_processor = None ,
48
- datafile_access_token = None ,
41
+ self ,
42
+ datafile = None ,
43
+ event_dispatcher = None ,
44
+ logger = None ,
45
+ error_handler = None ,
46
+ skip_json_validation = False ,
47
+ user_profile_service = None ,
48
+ sdk_key = None ,
49
+ config_manager = None ,
50
+ notification_center = None ,
51
+ event_processor = None ,
52
+ datafile_access_token = None ,
53
+ default_decisions = None
49
54
):
50
55
""" Optimizely init method for managing Custom projects.
51
56
@@ -69,6 +74,7 @@ def __init__(
69
74
which simply forwards events to the event dispatcher.
70
75
To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor.
71
76
datafile_access_token: Optional string used to fetch authenticated datafile for a secure project environment.
77
+ default_decisions: Optional list of decide options used with the decide APIs.
72
78
"""
73
79
self .logger_name = '.' .join ([__name__ , self .__class__ .__name__ ])
74
80
self .is_valid = True
@@ -80,6 +86,7 @@ def __init__(
80
86
self .event_processor = event_processor or ForwardingEventProcessor (
81
87
self .event_dispatcher , logger = self .logger , notification_center = self .notification_center ,
82
88
)
89
+ self .default_decisions = default_decisions or []
83
90
84
91
try :
85
92
self ._validate_instantiation_options ()
@@ -192,7 +199,7 @@ def _send_impression_event(self, project_config, experiment, variation, flag_key
192
199
)
193
200
194
201
def _get_feature_variable_for_type (
195
- self , project_config , feature_key , variable_key , variable_type , user_id , attributes ,
202
+ self , project_config , feature_key , variable_key , variable_type , user_id , attributes ,
196
203
):
197
204
""" Helper method to determine value for a certain variable attached to a feature flag based on type of variable.
198
205
@@ -296,7 +303,7 @@ def _get_feature_variable_for_type(
296
303
return actual_value
297
304
298
305
def _get_all_feature_variables_for_type (
299
- self , project_config , feature_key , user_id , attributes ,
306
+ self , project_config , feature_key , user_id , attributes ,
300
307
):
301
308
""" Helper method to determine value for all variables attached to a feature flag.
302
309
@@ -935,3 +942,136 @@ def create_user_context(self, user_id, attributes=None):
935
942
936
943
user_context = UserContext (self , user_id , attributes )
937
944
return user_context
945
+
946
+ def decide (self , user_context , key , decide_options = None ):
947
+ # raising on user context as it is internal and not provided directly by the user.
948
+ if not isinstance (user_context , UserContext ):
949
+ raise
950
+
951
+ reasons = []
952
+
953
+ # check if SDK is ready
954
+ if not self .is_valid :
955
+ self .logger .error (enums .Errors .INVALID_PROJECT_CONFIG .format ('decide' ))
956
+ reasons .append (DecisionMessage .SDK_NOT_READY )
957
+ return Decision (flag_key = key , user_context = user_context , reasons = reasons )
958
+
959
+ # validate that key is a string
960
+ if not isinstance (key , string_types ):
961
+ self .logger .error ('Key parameter is invalid' )
962
+ reasons .append (DecisionMessage .FLAG_KEY_INVALID .format (key ))
963
+ return Decision .new (flag_key = key , user_context = user_context , reasons = reasons )
964
+
965
+ # validate that key maps to a feature flag
966
+ config = self .project_config
967
+ feature_flag = config .get_feature_flag_from_key (key )
968
+ if feature_flag is None :
969
+ self .logger .error ("No feature flag was found for key '#{key}'." )
970
+ reasons .push (DecisionMessage .FLAG_KEY_INVALID .format (key ))
971
+ return Decision (flag_key = key , user_context = user_context , reasons = reasons )
972
+
973
+ # merge decide_options and default_decide_options
974
+ if isinstance (decide_options , list ):
975
+ decide_options += self .default_decisions
976
+ else :
977
+ self .logger .debug ('Provided decide options is not an array. Using default decide options.' )
978
+ decide_options = self .default_decisions
979
+
980
+ # Create Optimizely Decision Result.
981
+ user_id = user_context .user_id
982
+ attributes = user_context .user_attributes
983
+ variation_key = None
984
+ feature_enabled = False
985
+ rule_key = None
986
+ flag_key = key
987
+ all_variables = {}
988
+ decision_event_dispatched = False
989
+ experiment = None
990
+ decision_source = DecisionSources .ROLLOUT
991
+ source_info = {}
992
+
993
+ decision = self .decision_service .get_variation_for_feature (config , feature_flag , user_id , attributes ,
994
+ decide_options , reasons )
995
+
996
+ # Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
997
+ if decision .source == enums .DecisionSources .FEATURE_TEST :
998
+ experiment = decision .experiment
999
+ rule_key = experiment ['key' ]
1000
+ variation = decision ['variation' ]
1001
+ variation_key = variation ['key' ]
1002
+ feature_enabled = variation ['featureEnabled' ]
1003
+ decision_source = decision .source
1004
+ source_info ["variation" ] = variation
1005
+ source_info ["experiment" ] = experiment
1006
+
1007
+ if DecideOption .DISABLE_DECISION_EVENT not in decide_options :
1008
+ if decision_source == DecisionSources .FEATURE_TEST or config .send_flag_decisions :
1009
+ self ._send_impression_event (config , experiment , variation_key or '' , flag_key , rule_key or '' ,
1010
+ feature_enabled , decision_source ,
1011
+ user_id , attributes )
1012
+ decision_event_dispatched = True
1013
+
1014
+ # Generate all variables map if decide options doesn't include excludeVariables
1015
+ if DecideOption .EXCLUDE_VARIABLES not in decide_options :
1016
+ for v in feature_flag ['variables' ]:
1017
+ project_config = self .config_manager .get_config ()
1018
+ all_variables [v ['key' ]] = self ._get_feature_variable_for_type (project_config , feature_flag ['key' ],
1019
+ v ['key' ], v ['type' ], user_id , attributes )
1020
+
1021
+ # Send notification
1022
+ self .notification_center .send_notifications (
1023
+ enums .NotificationTypes .DECISION ,
1024
+ enums .DecisionNotificationTypes .FEATURE ,
1025
+ user_id ,
1026
+ attributes or {},
1027
+ {
1028
+ 'feature_key' : key ,
1029
+ 'feature_enabled' : feature_enabled ,
1030
+ 'source' : decision .source ,
1031
+ 'source_info' : source_info ,
1032
+ },
1033
+ )
1034
+
1035
+ include_reasons = []
1036
+ if DecideOption .INCLUDE_REASONS in decide_options :
1037
+ include_reasons = reasons
1038
+
1039
+ return Decision (variation_key = variation_key , enabled = feature_enabled , variables = all_variables ,
1040
+ rule_key = rule_key ,
1041
+ flag_key = flag_key , user_context = user_context , reasons = include_reasons )
1042
+
1043
+ def decide_all (self , user_context , decide_options = None ):
1044
+ # raising on user context as it is internal and not provided directly by the user.
1045
+ if not isinstance (user_context , UserContext ):
1046
+ raise
1047
+
1048
+ # check if SDK is ready
1049
+ if not self .is_valid :
1050
+ self .logger .error (enums .Errors .INVALID_PROJECT_CONFIG .format ('decide_all' ))
1051
+ return {}
1052
+
1053
+ keys = []
1054
+ for f in self .project_config :
1055
+ keys .append (f ['key' ])
1056
+
1057
+ return self .decide_for_keys (user_context , keys , decide_options )
1058
+
1059
+ def decide_for_keys (self , user_context , keys , decide_options = []):
1060
+ # raising on user context as it is internal and not provided directly by the user.
1061
+ if not isinstance (user_context , UserContext ):
1062
+ raise
1063
+
1064
+ # check if SDK is ready
1065
+ if not self .is_valid :
1066
+ self .logger .error (enums .Errors .INVALID_PROJECT_CONFIG .format ('decide_for_keys' ))
1067
+ return {}
1068
+
1069
+ enabled_flags_only = DecideOption .ENABLED_FLAGS_ONLY in decide_options
1070
+ decisions = {}
1071
+ for key in keys :
1072
+ decision = self .decide (user_context , key , decide_options )
1073
+ if enabled_flags_only and not decision .enabled :
1074
+ continue
1075
+ decisions [key ] = decision
1076
+
1077
+ return decisions
0 commit comments