optimizely-sdk 3.5.0 → 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/
|