Skip to content

Commit 67b9f83

Browse files
feat: add decide api (#309)
Added new Apis to support the decide feature. Introduced a new OptimizelyUserContext class through create_user_context class api. This creates an optimizely instance with memoized user context and exposes the following APIs 1. set_attribute 2. decide 3. decide_all 4. decide_for_keys 5. track_event
1 parent be63510 commit 67b9f83

17 files changed

+2564
-614
lines changed

optimizely/bucketer.py

+32-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2017, 2019-2020 Optimizely
1+
# Copyright 2016-2017, 2019-2021 Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -71,13 +71,13 @@ def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocatio
7171
traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations.
7272
7373
Returns:
74-
Entity ID which may represent experiment or variation.
74+
Entity ID which may represent experiment or variation and
7575
"""
76-
7776
bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id)
7877
bucketing_number = self._generate_bucket_value(bucketing_key)
78+
message = 'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id)
7979
project_config.logger.debug(
80-
'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id)
80+
message
8181
)
8282

8383
for traffic_allocation in traffic_allocations:
@@ -97,41 +97,57 @@ def bucket(self, project_config, experiment, user_id, bucketing_id):
9797
bucketing_id: ID to be used for bucketing the user.
9898
9999
Returns:
100-
Variation in which user with ID user_id will be put in. None if no variation.
100+
Variation in which user with ID user_id will be put in. None if no variation
101+
and array of log messages representing decision making.
102+
*/.
101103
"""
102-
104+
decide_reasons = []
103105
if not experiment:
104-
return None
106+
return None, decide_reasons
105107

106108
# Determine if experiment is in a mutually exclusive group.
107109
# This will not affect evaluation of rollout rules.
108110
if experiment.groupPolicy in GROUP_POLICIES:
109111
group = project_config.get_group(experiment.groupId)
110112

111113
if not group:
112-
return None
114+
return None, decide_reasons
113115

114116
user_experiment_id = self.find_bucket(
115117
project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
116118
)
119+
117120
if not user_experiment_id:
118-
project_config.logger.info('User "%s" is in no experiment.' % user_id)
119-
return None
121+
message = 'User "%s" is in no experiment.' % user_id
122+
project_config.logger.info(message)
123+
decide_reasons.append(message)
124+
return None, decide_reasons
120125

121126
if user_experiment_id != experiment.id:
127+
message = 'User "%s" is not in experiment "%s" of group %s.' \
128+
% (user_id, experiment.key, experiment.groupId)
122129
project_config.logger.info(
123-
'User "%s" is not in experiment "%s" of group %s.' % (user_id, experiment.key, experiment.groupId)
130+
message
124131
)
125-
return None
132+
decide_reasons.append(message)
133+
return None, decide_reasons
126134

135+
message = 'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId)
127136
project_config.logger.info(
128-
'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId)
137+
message
129138
)
139+
decide_reasons.append(message)
130140

131141
# Bucket user if not in white-list and in group (if any)
132-
variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation)
142+
variation_id = self.find_bucket(project_config, bucketing_id,
143+
experiment.id, experiment.trafficAllocation)
133144
if variation_id:
134145
variation = project_config.get_variation_from_id(experiment.key, variation_id)
135-
return variation
146+
return variation, decide_reasons
136147

137-
return None
148+
else:
149+
message = 'Bucketed into an empty traffic range. Returning nil.'
150+
project_config.logger.info(message)
151+
decide_reasons.append(message)
152+
153+
return None, decide_reasons

optimizely/decision/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright 2021, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2021, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
15+
class OptimizelyDecideOption(object):
16+
DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT'
17+
ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY'
18+
IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE'
19+
INCLUDE_REASONS = 'INCLUDE_REASONS'
20+
EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES'
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright 2021, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
15+
class OptimizelyDecision(object):
16+
def __init__(self, variation_key=None, enabled=None,
17+
variables=None, rule_key=None, flag_key=None, user_context=None, reasons=None):
18+
self.variation_key = variation_key
19+
self.enabled = enabled or False
20+
self.variables = variables or {}
21+
self.rule_key = rule_key
22+
self.flag_key = flag_key
23+
self.user_context = user_context
24+
self.reasons = reasons or []
25+
26+
def as_json(self):
27+
return {
28+
'variation_key': self.variation_key,
29+
'enabled': self.enabled,
30+
'variables': self.variables,
31+
'rule_key': self.rule_key,
32+
'flag_key': self.flag_key,
33+
'user_context': self.user_context.as_json(),
34+
'reasons': self.reasons
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2021, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
15+
class OptimizelyDecisionMessage(object):
16+
SDK_NOT_READY = 'Optimizely SDK not configured properly yet.'
17+
FLAG_KEY_INVALID = 'No flag was found for key "{}".'
18+
VARIABLE_VALUE_INVALID = 'Variable value for key "{}" is invalid or wrong type.'

0 commit comments

Comments
 (0)