optimizely-sdk 3.5.0 → 3.6.0
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.
- checksums.yaml +4 -4
- data/lib/optimizely.rb +4 -3
- data/lib/optimizely/audience.rb +20 -11
- data/lib/optimizely/bucketer.rb +3 -8
- data/lib/optimizely/config/datafile_project_config.rb +2 -0
- data/lib/optimizely/custom_attribute_condition_evaluator.rb +133 -37
- data/lib/optimizely/decision_service.rb +30 -29
- data/lib/optimizely/exceptions.rb +16 -0
- data/lib/optimizely/helpers/constants.rb +12 -2
- data/lib/optimizely/optimizely_config.rb +2 -1
- data/lib/optimizely/project_config.rb +3 -1
- data/lib/optimizely/semantic_version.rb +166 -0
- data/lib/optimizely/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d8204dce32c85bea56820607c0268b363fb86e7506ce685a2675645b4ef581b
|
4
|
+
data.tar.gz: dce7cd63fde5e24e179a74f8f14c6a22ed389028faac38a373af105683b596f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cc88f9802b38a2eb29354507303c6dcdcf6a402f721d24689f1e42a52a79319568974cf3584c6bf94398ff5f1410aacf49a1adf7aa27ef9105f3e7b23d39e3d
|
7
|
+
data.tar.gz: a6f0a7f052bb851ce760c4c88e0fe0dea18c14b42d5bcf457ac9cfefe3eaa2027f1b847c2ab0c133233618502cfe9207b1e82a5b1d65d0d4a678b58f5ea0ff88
|
data/lib/optimizely.rb
CHANGED
@@ -806,15 +806,16 @@ module Optimizely
|
|
806
806
|
"Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
|
807
807
|
else
|
808
808
|
@logger.log(Logger::DEBUG,
|
809
|
-
"Variable
|
809
|
+
"Variable value is not defined. Returning the default variable value '#{variable_value}' for variable '#{variable['key']}'.")
|
810
|
+
|
810
811
|
end
|
811
812
|
else
|
812
813
|
@logger.log(Logger::DEBUG,
|
813
|
-
"Feature '#{feature_flag_key}' for
|
814
|
+
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'. Returning the default variable value '#{variable_value}'.")
|
814
815
|
end
|
815
816
|
else
|
816
817
|
@logger.log(Logger::INFO,
|
817
|
-
"User '#{user_id}' was not bucketed into
|
818
|
+
"User '#{user_id}' was not bucketed into experiment or rollout for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
|
818
819
|
end
|
819
820
|
variable_value
|
820
821
|
end
|
data/lib/optimizely/audience.rb
CHANGED
@@ -24,23 +24,32 @@ module Optimizely
|
|
24
24
|
module Audience
|
25
25
|
module_function
|
26
26
|
|
27
|
-
def
|
28
|
-
# Determine for given experiment if user satisfies the
|
27
|
+
def user_meets_audience_conditions?(config, experiment, attributes, logger, logging_hash = nil, logging_key = nil)
|
28
|
+
# Determine for given experiment/rollout rule if user satisfies the audience conditions.
|
29
29
|
#
|
30
30
|
# config - Representation of the Optimizely project config.
|
31
|
-
# experiment - Experiment
|
31
|
+
# experiment - Experiment/Rollout rule in which user is to be bucketed.
|
32
32
|
# attributes - Hash representing user attributes which will be used in determining if
|
33
33
|
# the audience conditions are met.
|
34
|
+
# logger - Provides a logger instance.
|
35
|
+
# logging_hash - Optional string representing logs hash inside Helpers::Constants.
|
36
|
+
# This defaults to 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'.
|
37
|
+
# logging_key - Optional string to be logged as an identifier of experiment under evaluation.
|
38
|
+
# This defaults to experiment['key'].
|
34
39
|
#
|
35
40
|
# Returns boolean representing if user satisfies audience conditions for the audiences or not.
|
41
|
+
logging_hash ||= 'EXPERIMENT_AUDIENCE_EVALUATION_LOGS'
|
42
|
+
logging_key ||= experiment['key']
|
43
|
+
|
44
|
+
logs_hash = Object.const_get "Optimizely::Helpers::Constants::#{logging_hash}"
|
36
45
|
|
37
46
|
audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
|
38
47
|
|
39
48
|
logger.log(
|
40
49
|
Logger::DEBUG,
|
41
50
|
format(
|
42
|
-
|
43
|
-
|
51
|
+
logs_hash['EVALUATING_AUDIENCES_COMBINED'],
|
52
|
+
logging_key,
|
44
53
|
audience_conditions
|
45
54
|
)
|
46
55
|
)
|
@@ -50,8 +59,8 @@ module Optimizely
|
|
50
59
|
logger.log(
|
51
60
|
Logger::INFO,
|
52
61
|
format(
|
53
|
-
|
54
|
-
|
62
|
+
logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
|
63
|
+
logging_key,
|
55
64
|
'TRUE'
|
56
65
|
)
|
57
66
|
)
|
@@ -74,7 +83,7 @@ module Optimizely
|
|
74
83
|
logger.log(
|
75
84
|
Logger::DEBUG,
|
76
85
|
format(
|
77
|
-
|
86
|
+
logs_hash['EVALUATING_AUDIENCE'],
|
78
87
|
audience_id,
|
79
88
|
audience_conditions
|
80
89
|
)
|
@@ -85,7 +94,7 @@ module Optimizely
|
|
85
94
|
result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase
|
86
95
|
logger.log(
|
87
96
|
Logger::DEBUG,
|
88
|
-
format(
|
97
|
+
format(logs_hash['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str)
|
89
98
|
)
|
90
99
|
result
|
91
100
|
end
|
@@ -97,8 +106,8 @@ module Optimizely
|
|
97
106
|
logger.log(
|
98
107
|
Logger::INFO,
|
99
108
|
format(
|
100
|
-
|
101
|
-
|
109
|
+
logs_hash['AUDIENCE_EVALUATION_RESULT_COMBINED'],
|
110
|
+
logging_key,
|
102
111
|
eval_result.to_s.upcase
|
103
112
|
)
|
104
113
|
)
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-2017, 2019 Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, 2019-2020 Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -39,7 +39,7 @@ module Optimizely
|
|
39
39
|
# Determines ID of variation to be shown for a given experiment key and user ID.
|
40
40
|
#
|
41
41
|
# project_config - Instance of ProjectConfig
|
42
|
-
# experiment - Experiment for which visitor is to be bucketed.
|
42
|
+
# experiment - Experiment or Rollout rule for which visitor is to be bucketed.
|
43
43
|
# bucketing_id - String A customer-assigned value used to generate the bucketing key
|
44
44
|
# user_id - String ID for user.
|
45
45
|
#
|
@@ -47,6 +47,7 @@ module Optimizely
|
|
47
47
|
return nil if experiment.nil?
|
48
48
|
|
49
49
|
# check if experiment is in a group; if so, check if user is bucketed into specified experiment
|
50
|
+
# this will not affect evaluation of rollout rules.
|
50
51
|
experiment_id = experiment['id']
|
51
52
|
experiment_key = experiment['key']
|
52
53
|
group_id = experiment['groupId']
|
@@ -82,11 +83,6 @@ module Optimizely
|
|
82
83
|
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
|
83
84
|
if variation_id && variation_id != ''
|
84
85
|
variation = project_config.get_variation_from_id(experiment_key, variation_id)
|
85
|
-
variation_key = variation ? variation['key'] : nil
|
86
|
-
@logger.log(
|
87
|
-
Logger::INFO,
|
88
|
-
"User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
|
89
|
-
)
|
90
86
|
return variation
|
91
87
|
end
|
92
88
|
|
@@ -98,7 +94,6 @@ module Optimizely
|
|
98
94
|
)
|
99
95
|
end
|
100
96
|
|
101
|
-
@logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
|
102
97
|
nil
|
103
98
|
end
|
104
99
|
|
@@ -24,6 +24,7 @@ module Optimizely
|
|
24
24
|
RUNNING_EXPERIMENT_STATUS = ['Running'].freeze
|
25
25
|
RESERVED_ATTRIBUTE_PREFIX = '$opt_'
|
26
26
|
|
27
|
+
attr_reader :datafile
|
27
28
|
attr_reader :account_id
|
28
29
|
attr_reader :attributes
|
29
30
|
attr_reader :audiences
|
@@ -62,6 +63,7 @@ module Optimizely
|
|
62
63
|
|
63
64
|
config = JSON.parse(datafile)
|
64
65
|
|
66
|
+
@datafile = datafile
|
65
67
|
@error_handler = error_handler
|
66
68
|
@logger = logger
|
67
69
|
@version = config['version']
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2019, Optimizely and contributors
|
4
|
+
# Copyright 2019-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -15,8 +15,10 @@
|
|
15
15
|
# See the License for the specific language governing permissions and
|
16
16
|
# limitations under the License.
|
17
17
|
#
|
18
|
+
require_relative 'exceptions'
|
18
19
|
require_relative 'helpers/constants'
|
19
20
|
require_relative 'helpers/validator'
|
21
|
+
require_relative 'semantic_version'
|
20
22
|
|
21
23
|
module Optimizely
|
22
24
|
class CustomAttributeConditionEvaluator
|
@@ -26,15 +28,29 @@ module Optimizely
|
|
26
28
|
EXACT_MATCH_TYPE = 'exact'
|
27
29
|
EXISTS_MATCH_TYPE = 'exists'
|
28
30
|
GREATER_THAN_MATCH_TYPE = 'gt'
|
31
|
+
GREATER_EQUAL_MATCH_TYPE = 'ge'
|
29
32
|
LESS_THAN_MATCH_TYPE = 'lt'
|
33
|
+
LESS_EQUAL_MATCH_TYPE = 'le'
|
30
34
|
SUBSTRING_MATCH_TYPE = 'substring'
|
35
|
+
SEMVER_EQ = 'semver_eq'
|
36
|
+
SEMVER_GE = 'semver_ge'
|
37
|
+
SEMVER_GT = 'semver_gt'
|
38
|
+
SEMVER_LE = 'semver_le'
|
39
|
+
SEMVER_LT = 'semver_lt'
|
31
40
|
|
32
41
|
EVALUATORS_BY_MATCH_TYPE = {
|
33
42
|
EXACT_MATCH_TYPE => :exact_evaluator,
|
34
43
|
EXISTS_MATCH_TYPE => :exists_evaluator,
|
35
44
|
GREATER_THAN_MATCH_TYPE => :greater_than_evaluator,
|
45
|
+
GREATER_EQUAL_MATCH_TYPE => :greater_than_or_equal_evaluator,
|
36
46
|
LESS_THAN_MATCH_TYPE => :less_than_evaluator,
|
37
|
-
|
47
|
+
LESS_EQUAL_MATCH_TYPE => :less_than_or_equal_evaluator,
|
48
|
+
SUBSTRING_MATCH_TYPE => :substring_evaluator,
|
49
|
+
SEMVER_EQ => :semver_equal_evaluator,
|
50
|
+
SEMVER_GE => :semver_greater_than_or_equal_evaluator,
|
51
|
+
SEMVER_GT => :semver_greater_than_evaluator,
|
52
|
+
SEMVER_LE => :semver_less_than_or_equal_evaluator,
|
53
|
+
SEMVER_LT => :semver_less_than_evaluator
|
38
54
|
}.freeze
|
39
55
|
|
40
56
|
attr_reader :user_attributes
|
@@ -95,7 +111,35 @@ module Optimizely
|
|
95
111
|
return nil
|
96
112
|
end
|
97
113
|
|
98
|
-
|
114
|
+
begin
|
115
|
+
send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition)
|
116
|
+
rescue InvalidAttributeType
|
117
|
+
condition_name = leaf_condition['name']
|
118
|
+
user_value = @user_attributes[condition_name]
|
119
|
+
|
120
|
+
@logger.log(
|
121
|
+
Logger::WARN,
|
122
|
+
format(
|
123
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
|
124
|
+
leaf_condition,
|
125
|
+
user_value.class,
|
126
|
+
condition_name
|
127
|
+
)
|
128
|
+
)
|
129
|
+
return nil
|
130
|
+
rescue InvalidSemanticVersion
|
131
|
+
condition_name = leaf_condition['name']
|
132
|
+
|
133
|
+
@logger.log(
|
134
|
+
Logger::WARN,
|
135
|
+
format(
|
136
|
+
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INVALID_SEMANTIC_VERSION'],
|
137
|
+
leaf_condition,
|
138
|
+
condition_name
|
139
|
+
)
|
140
|
+
)
|
141
|
+
return nil
|
142
|
+
end
|
99
143
|
end
|
100
144
|
|
101
145
|
def exact_evaluator(condition)
|
@@ -122,16 +166,7 @@ module Optimizely
|
|
122
166
|
|
123
167
|
if !value_type_valid_for_exact_conditions?(user_provided_value) ||
|
124
168
|
!Helpers::Validator.same_types?(condition_value, user_provided_value)
|
125
|
-
|
126
|
-
Logger::WARN,
|
127
|
-
format(
|
128
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
|
129
|
-
condition,
|
130
|
-
user_provided_value.class,
|
131
|
-
condition['name']
|
132
|
-
)
|
133
|
-
)
|
134
|
-
return nil
|
169
|
+
raise InvalidAttributeType
|
135
170
|
end
|
136
171
|
|
137
172
|
if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value)
|
@@ -173,6 +208,20 @@ module Optimizely
|
|
173
208
|
user_provided_value > condition_value
|
174
209
|
end
|
175
210
|
|
211
|
+
def greater_than_or_equal_evaluator(condition)
|
212
|
+
# Evaluate the given greater than or equal match condition for the given user attributes.
|
213
|
+
# Returns boolean true if the user attribute value is greater than or equal to the condition value,
|
214
|
+
# false if the user attribute value is less than the condition value,
|
215
|
+
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
216
|
+
|
217
|
+
condition_value = condition['value']
|
218
|
+
user_provided_value = @user_attributes[condition['name']]
|
219
|
+
|
220
|
+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
221
|
+
|
222
|
+
user_provided_value >= condition_value
|
223
|
+
end
|
224
|
+
|
176
225
|
def less_than_evaluator(condition)
|
177
226
|
# Evaluate the given less than match condition for the given user attributes.
|
178
227
|
# Returns boolean true if the user attribute value is less than the condition value,
|
@@ -187,6 +236,20 @@ module Optimizely
|
|
187
236
|
user_provided_value < condition_value
|
188
237
|
end
|
189
238
|
|
239
|
+
def less_than_or_equal_evaluator(condition)
|
240
|
+
# Evaluate the given less than or equal match condition for the given user attributes.
|
241
|
+
# Returns boolean true if the user attribute value is less than or equal to the condition value,
|
242
|
+
# false if the user attribute value is greater than the condition value,
|
243
|
+
# nil if the condition value isn't a number or the user attribute value isn't a number.
|
244
|
+
|
245
|
+
condition_value = condition['value']
|
246
|
+
user_provided_value = @user_attributes[condition['name']]
|
247
|
+
|
248
|
+
return nil unless valid_numeric_values?(user_provided_value, condition_value, condition)
|
249
|
+
|
250
|
+
user_provided_value <= condition_value
|
251
|
+
end
|
252
|
+
|
190
253
|
def substring_evaluator(condition)
|
191
254
|
# Evaluate the given substring match condition for the given user attributes.
|
192
255
|
# Returns boolean true if the condition value is a substring of the user attribute value,
|
@@ -204,22 +267,66 @@ module Optimizely
|
|
204
267
|
return nil
|
205
268
|
end
|
206
269
|
|
207
|
-
unless user_provided_value.is_a?(String)
|
208
|
-
@logger.log(
|
209
|
-
Logger::WARN,
|
210
|
-
format(
|
211
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
|
212
|
-
condition,
|
213
|
-
user_provided_value.class,
|
214
|
-
condition['name']
|
215
|
-
)
|
216
|
-
)
|
217
|
-
return nil
|
218
|
-
end
|
270
|
+
raise InvalidAttributeType unless user_provided_value.is_a?(String)
|
219
271
|
|
220
272
|
user_provided_value.include? condition_value
|
221
273
|
end
|
222
274
|
|
275
|
+
def semver_equal_evaluator(condition)
|
276
|
+
# Evaluate the given semantic version equal match target version for the user version.
|
277
|
+
# Returns boolean true if the user version is equal to the target version,
|
278
|
+
# false if the user version is not equal to the target version
|
279
|
+
|
280
|
+
target_version = condition['value']
|
281
|
+
user_version = @user_attributes[condition['name']]
|
282
|
+
|
283
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).zero?
|
284
|
+
end
|
285
|
+
|
286
|
+
def semver_greater_than_evaluator(condition)
|
287
|
+
# Evaluate the given semantic version greater than match target version for the user version.
|
288
|
+
# Returns boolean true if the user version is greater than the target version,
|
289
|
+
# false if the user version is less than or equal to the target version
|
290
|
+
|
291
|
+
target_version = condition['value']
|
292
|
+
user_version = @user_attributes[condition['name']]
|
293
|
+
|
294
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).positive?
|
295
|
+
end
|
296
|
+
|
297
|
+
def semver_greater_than_or_equal_evaluator(condition)
|
298
|
+
# Evaluate the given semantic version greater than or equal to match target version for the user version.
|
299
|
+
# Returns boolean true if the user version is greater than or equal to the target version,
|
300
|
+
# false if the user version is less than the target version
|
301
|
+
|
302
|
+
target_version = condition['value']
|
303
|
+
user_version = @user_attributes[condition['name']]
|
304
|
+
|
305
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) >= 0
|
306
|
+
end
|
307
|
+
|
308
|
+
def semver_less_than_evaluator(condition)
|
309
|
+
# Evaluate the given semantic version less than match target version for the user version.
|
310
|
+
# Returns boolean true if the user version is less than the target version,
|
311
|
+
# false if the user version is greater than or equal to the target version
|
312
|
+
|
313
|
+
target_version = condition['value']
|
314
|
+
user_version = @user_attributes[condition['name']]
|
315
|
+
|
316
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version).negative?
|
317
|
+
end
|
318
|
+
|
319
|
+
def semver_less_than_or_equal_evaluator(condition)
|
320
|
+
# Evaluate the given semantic version less than or equal to match target version for the user version.
|
321
|
+
# Returns boolean true if the user version is less than or equal to the target version,
|
322
|
+
# false if the user version is greater than the target version
|
323
|
+
|
324
|
+
target_version = condition['value']
|
325
|
+
user_version = @user_attributes[condition['name']]
|
326
|
+
|
327
|
+
SemanticVersion.compare_user_version_with_target_version(target_version, user_version) <= 0
|
328
|
+
end
|
329
|
+
|
223
330
|
private
|
224
331
|
|
225
332
|
def valid_numeric_values?(user_value, condition_value, condition)
|
@@ -234,18 +341,7 @@ module Optimizely
|
|
234
341
|
return false
|
235
342
|
end
|
236
343
|
|
237
|
-
unless user_value.is_a?(Numeric)
|
238
|
-
@logger.log(
|
239
|
-
Logger::WARN,
|
240
|
-
format(
|
241
|
-
Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'],
|
242
|
-
condition,
|
243
|
-
user_value.class,
|
244
|
-
condition['name']
|
245
|
-
)
|
246
|
-
)
|
247
|
-
return false
|
248
|
-
end
|
344
|
+
raise InvalidAttributeType unless user_value.is_a?(Numeric)
|
249
345
|
|
250
346
|
unless Helpers::Validator.finite_number?(user_value)
|
251
347
|
@logger.log(
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2017-
|
4
|
+
# Copyright 2017-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -94,7 +94,7 @@ module Optimizely
|
|
94
94
|
end
|
95
95
|
|
96
96
|
# Check audience conditions
|
97
|
-
unless Audience.
|
97
|
+
unless Audience.user_meets_audience_conditions?(project_config, experiment, attributes, @logger)
|
98
98
|
@logger.log(
|
99
99
|
Logger::INFO,
|
100
100
|
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
|
@@ -106,6 +106,16 @@ module Optimizely
|
|
106
106
|
variation = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
|
107
107
|
variation_id = variation ? variation['id'] : nil
|
108
108
|
|
109
|
+
if variation_id
|
110
|
+
variation_key = variation['key']
|
111
|
+
@logger.log(
|
112
|
+
Logger::INFO,
|
113
|
+
"User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
|
114
|
+
)
|
115
|
+
else
|
116
|
+
@logger.log(Logger::INFO, "User '#{user_id}' is in no variation.")
|
117
|
+
end
|
118
|
+
|
109
119
|
# Persist bucketing decision
|
110
120
|
save_user_profile(user_profile, experiment_id, variation_id)
|
111
121
|
variation_id
|
@@ -125,21 +135,9 @@ module Optimizely
|
|
125
135
|
decision = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes)
|
126
136
|
return decision unless decision.nil?
|
127
137
|
|
128
|
-
feature_flag_key = feature_flag['key']
|
129
138
|
decision = get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes)
|
130
|
-
if decision
|
131
|
-
@logger.log(
|
132
|
-
Logger::INFO,
|
133
|
-
"User '#{user_id}' is bucketed into a rollout for feature flag '#{feature_flag_key}'."
|
134
|
-
)
|
135
|
-
return decision
|
136
|
-
end
|
137
|
-
@logger.log(
|
138
|
-
Logger::INFO,
|
139
|
-
"User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
|
140
|
-
)
|
141
139
|
|
142
|
-
|
140
|
+
decision
|
143
141
|
end
|
144
142
|
|
145
143
|
def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil)
|
@@ -178,10 +176,7 @@ module Optimizely
|
|
178
176
|
next unless variation_id
|
179
177
|
|
180
178
|
variation = project_config.variation_id_map[experiment_key][variation_id]
|
181
|
-
|
182
|
-
Logger::INFO,
|
183
|
-
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
|
184
|
-
)
|
179
|
+
|
185
180
|
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST'])
|
186
181
|
end
|
187
182
|
|
@@ -231,20 +226,23 @@ module Optimizely
|
|
231
226
|
# Go through each experiment in order and try to get the variation for the user
|
232
227
|
number_of_rules.times do |index|
|
233
228
|
rollout_rule = rollout_rules[index]
|
234
|
-
|
235
|
-
audience = project_config.get_audience_from_id(audience_id)
|
236
|
-
audience_name = audience['name']
|
229
|
+
logging_key = index + 1
|
237
230
|
|
238
231
|
# Check that user meets audience conditions for targeting rule
|
239
|
-
unless Audience.
|
232
|
+
unless Audience.user_meets_audience_conditions?(project_config, rollout_rule, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
|
240
233
|
@logger.log(
|
241
234
|
Logger::DEBUG,
|
242
|
-
"User '#{user_id}' does not meet the conditions
|
235
|
+
"User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
|
243
236
|
)
|
244
237
|
# move onto the next targeting rule
|
245
238
|
next
|
246
239
|
end
|
247
240
|
|
241
|
+
@logger.log(
|
242
|
+
Logger::DEBUG,
|
243
|
+
"User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
|
244
|
+
)
|
245
|
+
|
248
246
|
# Evaluate if user satisfies the traffic allocation for this rollout rule
|
249
247
|
variation = @bucketer.bucket(project_config, rollout_rule, bucketing_id, user_id)
|
250
248
|
return Decision.new(rollout_rule, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
|
@@ -254,17 +252,20 @@ module Optimizely
|
|
254
252
|
|
255
253
|
# get last rule which is the everyone else rule
|
256
254
|
everyone_else_experiment = rollout_rules[number_of_rules]
|
255
|
+
logging_key = 'Everyone Else'
|
257
256
|
# Check that user meets audience conditions for last rule
|
258
|
-
unless Audience.
|
259
|
-
audience_id = everyone_else_experiment['audienceIds'][0]
|
260
|
-
audience = project_config.get_audience_from_id(audience_id)
|
261
|
-
audience_name = audience['name']
|
257
|
+
unless Audience.user_meets_audience_conditions?(project_config, everyone_else_experiment, attributes, @logger, 'ROLLOUT_AUDIENCE_EVALUATION_LOGS', logging_key)
|
262
258
|
@logger.log(
|
263
259
|
Logger::DEBUG,
|
264
|
-
"User '#{user_id}' does not meet the conditions
|
260
|
+
"User '#{user_id}' does not meet the audience conditions for targeting rule '#{logging_key}'."
|
265
261
|
)
|
266
262
|
return nil
|
267
263
|
end
|
264
|
+
|
265
|
+
@logger.log(
|
266
|
+
Logger::DEBUG,
|
267
|
+
"User '#{user_id}' meets the audience conditions for targeting rule '#{logging_key}'."
|
268
|
+
)
|
268
269
|
variation = @bucketer.bucket(project_config, everyone_else_experiment, bucketing_id, user_id)
|
269
270
|
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCES['ROLLOUT']) unless variation.nil?
|
270
271
|
|
@@ -120,4 +120,20 @@ module Optimizely
|
|
120
120
|
super("Optimizely instance is not valid. Failing '#{aborted_method}'.")
|
121
121
|
end
|
122
122
|
end
|
123
|
+
|
124
|
+
class InvalidAttributeType < Error
|
125
|
+
# Raised when an attribute is not provided in expected type.
|
126
|
+
|
127
|
+
def initialize(msg = 'Provided attribute value is not in the expected data type.')
|
128
|
+
super
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class InvalidSemanticVersion < Error
|
133
|
+
# Raised when an invalid value is provided as semantic version.
|
134
|
+
|
135
|
+
def initialize(msg = 'Provided semantic version is invalid.')
|
136
|
+
super
|
137
|
+
end
|
138
|
+
end
|
123
139
|
end
|
@@ -335,11 +335,11 @@ module Optimizely
|
|
335
335
|
|
336
336
|
AUDIENCE_EVALUATION_LOGS = {
|
337
337
|
'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.",
|
338
|
-
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
|
339
338
|
'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
|
340
|
-
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.",
|
341
339
|
'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
|
342
340
|
"for user attribute '%s' is not in the range [-2^53, +2^53].",
|
341
|
+
'INVALID_SEMANTIC_VERSION' => 'Audience condition %s evaluated as UNKNOWN because an invalid semantic version ' \
|
342
|
+
"was passed for user attribute '%s'.",
|
343
343
|
'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
|
344
344
|
"was passed for user attribute '%s'.",
|
345
345
|
'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \
|
@@ -354,6 +354,16 @@ module Optimizely
|
|
354
354
|
'to upgrade to a newer release of the Optimizely SDK.'
|
355
355
|
}.freeze
|
356
356
|
|
357
|
+
EXPERIMENT_AUDIENCE_EVALUATION_LOGS = {
|
358
|
+
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
|
359
|
+
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s."
|
360
|
+
}.merge(AUDIENCE_EVALUATION_LOGS).freeze
|
361
|
+
|
362
|
+
ROLLOUT_AUDIENCE_EVALUATION_LOGS = {
|
363
|
+
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for rule '%s' collectively evaluated to %s.",
|
364
|
+
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for rule '%s': %s."
|
365
|
+
}.merge(AUDIENCE_EVALUATION_LOGS).freeze
|
366
|
+
|
357
367
|
DECISION_NOTIFICATION_TYPES = {
|
358
368
|
'AB_TEST' => 'ab-test',
|
359
369
|
'FEATURE' => 'feature',
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2019, Optimizely and contributors
|
3
|
+
# Copyright 2019-2020, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -25,6 +25,7 @@ module Optimizely
|
|
25
25
|
experiments_map_object = experiments_map
|
26
26
|
features_map = get_features_map(experiments_map_object)
|
27
27
|
{
|
28
|
+
'datafile' => @project_config.datafile,
|
28
29
|
'experimentsMap' => experiments_map_object,
|
29
30
|
'featuresMap' => features_map,
|
30
31
|
'revision' => @project_config.revision
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2016-
|
3
|
+
# Copyright 2016-2020, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -20,6 +20,8 @@ module Optimizely
|
|
20
20
|
# ProjectConfig is an interface capturing the experiment, variation and feature definitions.
|
21
21
|
# The default implementation of ProjectConfig can be found in DatafileProjectConfig.
|
22
22
|
|
23
|
+
def datafile; end
|
24
|
+
|
23
25
|
def account_id; end
|
24
26
|
|
25
27
|
def attributes; end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2020, Optimizely and contributors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require_relative 'exceptions'
|
20
|
+
|
21
|
+
module Optimizely
|
22
|
+
module SemanticVersion
|
23
|
+
# Semantic Version Operators
|
24
|
+
SEMVER_PRE_RELEASE = '-'
|
25
|
+
SEMVER_BUILD = '+'
|
26
|
+
|
27
|
+
module_function
|
28
|
+
|
29
|
+
def pre_release?(target)
|
30
|
+
# Method to check if the given version is a prerelease
|
31
|
+
#
|
32
|
+
# target - String representing semantic version
|
33
|
+
#
|
34
|
+
# Returns true if the given version is a prerelease
|
35
|
+
# false if it doesn't
|
36
|
+
|
37
|
+
raise unless target.is_a? String
|
38
|
+
|
39
|
+
prerelease_index = target.index(SEMVER_PRE_RELEASE)
|
40
|
+
build_index = target.index(SEMVER_BUILD)
|
41
|
+
|
42
|
+
return false if prerelease_index.nil?
|
43
|
+
return true if build_index.nil?
|
44
|
+
|
45
|
+
# when both operators are present prerelease should precede the build operator
|
46
|
+
prerelease_index < build_index
|
47
|
+
end
|
48
|
+
|
49
|
+
def build?(target)
|
50
|
+
# Method to check if the given version is a build
|
51
|
+
#
|
52
|
+
# target - String representing semantic version
|
53
|
+
#
|
54
|
+
# Returns true if the given version is a build
|
55
|
+
# false if it doesn't
|
56
|
+
|
57
|
+
raise unless target.is_a? String
|
58
|
+
|
59
|
+
prerelease_index = target.index(SEMVER_PRE_RELEASE)
|
60
|
+
build_index = target.index(SEMVER_BUILD)
|
61
|
+
|
62
|
+
return false if build_index.nil?
|
63
|
+
return true if prerelease_index.nil?
|
64
|
+
|
65
|
+
# when both operators are present build should precede the prerelease operator
|
66
|
+
build_index < prerelease_index
|
67
|
+
end
|
68
|
+
|
69
|
+
def split_semantic_version(target)
|
70
|
+
# Method to split the given version.
|
71
|
+
#
|
72
|
+
# target - String representing semantic version
|
73
|
+
#
|
74
|
+
# Returns List The array of version split into smaller parts i.e major, minor, patch etc,
|
75
|
+
# Exception if the given version is invalid.
|
76
|
+
|
77
|
+
target_prefix = target
|
78
|
+
target_suffix = ''
|
79
|
+
target_parts = []
|
80
|
+
|
81
|
+
raise InvalidSemanticVersion if target.include? ' '
|
82
|
+
|
83
|
+
if pre_release?(target)
|
84
|
+
target_parts = target.split(SEMVER_PRE_RELEASE, 2)
|
85
|
+
elsif build? target
|
86
|
+
target_parts = target.split(SEMVER_BUILD, 2)
|
87
|
+
end
|
88
|
+
|
89
|
+
unless target_parts.empty?
|
90
|
+
target_prefix = target_parts[0].to_s
|
91
|
+
target_suffix = target_parts[1..-1]
|
92
|
+
end
|
93
|
+
|
94
|
+
# expect a version string of the form x.y.z
|
95
|
+
dot_count = target_prefix.count('.')
|
96
|
+
raise InvalidSemanticVersion if dot_count > 2
|
97
|
+
|
98
|
+
target_version_parts = target_prefix.split('.')
|
99
|
+
raise InvalidSemanticVersion if target_version_parts.length != dot_count + 1
|
100
|
+
|
101
|
+
target_version_parts.each do |part|
|
102
|
+
raise InvalidSemanticVersion unless Helpers::Validator.string_numeric? part
|
103
|
+
end
|
104
|
+
|
105
|
+
target_version_parts.concat(target_suffix) if target_suffix.is_a?(Array)
|
106
|
+
|
107
|
+
target_version_parts
|
108
|
+
end
|
109
|
+
|
110
|
+
def compare_user_version_with_target_version(target_version, user_version)
|
111
|
+
# Compares target and user versions
|
112
|
+
#
|
113
|
+
# target_version - String representing target version
|
114
|
+
# user_version - String representing user version
|
115
|
+
|
116
|
+
# Returns boolean 0 if user version is equal to target version,
|
117
|
+
# 1 if user version is greater than target version,
|
118
|
+
# -1 if user version is less than target version.
|
119
|
+
|
120
|
+
raise InvalidAttributeType unless target_version.is_a? String
|
121
|
+
raise InvalidAttributeType unless user_version.is_a? String
|
122
|
+
|
123
|
+
is_target_version_prerelease = pre_release?(target_version)
|
124
|
+
is_user_version_prerelease = pre_release?(user_version)
|
125
|
+
|
126
|
+
target_version_parts = split_semantic_version(target_version)
|
127
|
+
user_version_parts = split_semantic_version(user_version)
|
128
|
+
user_version_parts_len = user_version_parts.length if user_version_parts
|
129
|
+
|
130
|
+
# Up to the precision of targetedVersion, expect version to match exactly.
|
131
|
+
target_version_parts.each_with_index do |_item, idx|
|
132
|
+
if user_version_parts_len <= idx
|
133
|
+
# even if they are equal at this point. if the target is a prerelease
|
134
|
+
# then user version must be greater than the pre release.
|
135
|
+
return 1 if is_target_version_prerelease
|
136
|
+
|
137
|
+
return -1
|
138
|
+
|
139
|
+
elsif !Helpers::Validator.string_numeric? user_version_parts[idx]
|
140
|
+
# compare strings
|
141
|
+
if user_version_parts[idx] < target_version_parts[idx]
|
142
|
+
return 1 if is_target_version_prerelease && !is_user_version_prerelease
|
143
|
+
|
144
|
+
return -1
|
145
|
+
|
146
|
+
elsif user_version_parts[idx] > target_version_parts[idx]
|
147
|
+
return -1 if is_user_version_prerelease && !is_target_version_prerelease
|
148
|
+
|
149
|
+
return 1
|
150
|
+
end
|
151
|
+
|
152
|
+
else
|
153
|
+
user_version_part = user_version_parts[idx].to_i
|
154
|
+
target_version_part = target_version_parts[idx].to_i
|
155
|
+
|
156
|
+
return 1 if user_version_part > target_version_part
|
157
|
+
return -1 if user_version_part < target_version_part
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
return -1 if is_user_version_prerelease && !is_target_version_prerelease
|
162
|
+
|
163
|
+
0
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/lib/optimizely/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: optimizely-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Optimizely
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -174,6 +174,7 @@ files:
|
|
174
174
|
- lib/optimizely/optimizely_factory.rb
|
175
175
|
- lib/optimizely/params.rb
|
176
176
|
- lib/optimizely/project_config.rb
|
177
|
+
- lib/optimizely/semantic_version.rb
|
177
178
|
- lib/optimizely/user_profile_service.rb
|
178
179
|
- lib/optimizely/version.rb
|
179
180
|
homepage: https://www.optimizely.com/
|