optimizely-sdk 1.2.0 → 1.3.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 +16 -44
- data/lib/optimizely/bucketer.rb +0 -30
- data/lib/optimizely/decision_service.rb +190 -0
- data/lib/optimizely/event_builder.rb +1 -1
- data/lib/optimizely/project_config.rb +23 -0
- data/lib/optimizely/user_profile_service.rb +36 -0
- data/lib/optimizely/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 164c4cd46301497d6ad393c5ed49093f9e72d337
|
4
|
+
data.tar.gz: b52ca87752dc4aa840e53c4be5ce7583604eb044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbdcbe648b51889c656578e8962c737380b0ab255be1941c5df48bdef9740c8e5e2d0aa2fd814f697e24197b36aaa8759f3882211059f7e2be55facfbd13e5e6
|
7
|
+
data.tar.gz: 064f914a6ddc35ae884b7d3563e7791da2d48030e292796422df5e5191a6de920959dfeca0d0a7bde6646f1ac6a1581526f0ed08df50084eb8da7d73f16b08aa
|
data/lib/optimizely.rb
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
require_relative 'optimizely/audience'
|
17
|
-
require_relative 'optimizely/
|
17
|
+
require_relative 'optimizely/decision_service'
|
18
18
|
require_relative 'optimizely/error_handler'
|
19
19
|
require_relative 'optimizely/event_builder'
|
20
20
|
require_relative 'optimizely/event_dispatcher'
|
@@ -28,29 +28,31 @@ module Optimizely
|
|
28
28
|
class Project
|
29
29
|
|
30
30
|
# Boolean representing if the instance represents a usable Optimizely Project
|
31
|
-
attr_reader
|
31
|
+
attr_reader :is_valid
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
attr_reader :config
|
34
|
+
attr_reader :decision_service
|
35
|
+
attr_reader :error_handler
|
36
|
+
attr_reader :event_builder
|
37
|
+
attr_reader :event_dispatcher
|
38
|
+
attr_reader :logger
|
39
39
|
|
40
|
-
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false)
|
40
|
+
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
|
41
41
|
# Constructor for Projects.
|
42
42
|
#
|
43
43
|
# datafile - JSON string representing the project.
|
44
44
|
# event_dispatcher - Provides a dispatch_event method which if given a URL and params sends a request to it.
|
45
|
-
# logger - Optional
|
46
|
-
# error_handler - Optional
|
45
|
+
# logger - Optional component which provides a log method to log messages. By default nothing would be logged.
|
46
|
+
# error_handler - Optional component which provides a handle_error method to handle exceptions.
|
47
47
|
# By default all exceptions will be suppressed.
|
48
|
+
# user_profile_service - Optional component which provides methods to store and retreive user profiles.
|
48
49
|
# skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
|
49
50
|
|
50
51
|
@is_valid = true
|
51
52
|
@logger = logger || NoOpLogger.new
|
52
53
|
@error_handler = error_handler || NoOpErrorHandler.new
|
53
54
|
@event_dispatcher = event_dispatcher || EventDispatcher.new
|
55
|
+
@user_profile_service = user_profile_service
|
54
56
|
|
55
57
|
begin
|
56
58
|
validate_instantiation_options(datafile, skip_json_validation)
|
@@ -77,7 +79,7 @@ module Optimizely
|
|
77
79
|
return
|
78
80
|
end
|
79
81
|
|
80
|
-
@
|
82
|
+
@decision_service = DecisionService.new(@config, @user_profile_service)
|
81
83
|
@event_builder = EventBuilderV2.new(@config)
|
82
84
|
end
|
83
85
|
|
@@ -135,24 +137,13 @@ module Optimizely
|
|
135
137
|
return nil
|
136
138
|
end
|
137
139
|
|
138
|
-
unless
|
140
|
+
unless user_inputs_valid?(attributes)
|
139
141
|
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
|
140
142
|
return nil
|
141
143
|
end
|
142
144
|
|
143
|
-
variation_id = @
|
145
|
+
variation_id = @decision_service.get_variation(experiment_key, user_id, attributes)
|
144
146
|
|
145
|
-
unless variation_id.nil?
|
146
|
-
return @config.get_variation_key_from_id(experiment_key, variation_id)
|
147
|
-
end
|
148
|
-
|
149
|
-
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
|
150
|
-
@logger.log(Logger::INFO,
|
151
|
-
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'.")
|
152
|
-
return nil
|
153
|
-
end
|
154
|
-
|
155
|
-
variation_id = @bucketer.bucket(experiment_key, user_id)
|
156
147
|
unless variation_id.nil?
|
157
148
|
return @config.get_variation_key_from_id(experiment_key, variation_id)
|
158
149
|
end
|
@@ -240,25 +231,6 @@ module Optimizely
|
|
240
231
|
valid_experiments
|
241
232
|
end
|
242
233
|
|
243
|
-
def preconditions_valid?(experiment_key, attributes = nil, event_tags = nil)
|
244
|
-
# Validates preconditions for bucketing a user.
|
245
|
-
#
|
246
|
-
# experiment_key - String key for an experiment.
|
247
|
-
# user_id - String ID of user.
|
248
|
-
# attributes - Hash of user attributes.
|
249
|
-
#
|
250
|
-
# Returns boolean representing whether all preconditions are valid.
|
251
|
-
|
252
|
-
return false unless user_inputs_valid?(attributes, event_tags)
|
253
|
-
|
254
|
-
unless @config.experiment_running?(experiment_key)
|
255
|
-
@logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
|
256
|
-
return false
|
257
|
-
end
|
258
|
-
|
259
|
-
true
|
260
|
-
end
|
261
|
-
|
262
234
|
def user_inputs_valid?(attributes = nil, event_tags = nil)
|
263
235
|
# Helper method to validate user inputs.
|
264
236
|
#
|
data/lib/optimizely/bucketer.rb
CHANGED
@@ -101,36 +101,6 @@ module Optimizely
|
|
101
101
|
nil
|
102
102
|
end
|
103
103
|
|
104
|
-
def get_forced_variation_id(experiment_key, user_id)
|
105
|
-
# Determine if a user is forced into a variation for the given experiment and return the id of that variation.
|
106
|
-
#
|
107
|
-
# experiment_key - Key representing the experiment for which user is to be bucketed.
|
108
|
-
# user_id - ID for the user.
|
109
|
-
#
|
110
|
-
# Returns variation ID in which the user with ID user_id is forced into. Nil if no variation.
|
111
|
-
|
112
|
-
forced_variations = @config.get_forced_variations(experiment_key)
|
113
|
-
|
114
|
-
return nil unless forced_variations
|
115
|
-
|
116
|
-
forced_variation_key = forced_variations[user_id]
|
117
|
-
|
118
|
-
return nil unless forced_variation_key
|
119
|
-
|
120
|
-
forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
|
121
|
-
|
122
|
-
unless forced_variation_id
|
123
|
-
@config.logger.log(
|
124
|
-
Logger::INFO,
|
125
|
-
"Variation key '#{forced_variation_key}' is not in datafile. Not activating user '#{user_id}'."
|
126
|
-
)
|
127
|
-
return nil
|
128
|
-
end
|
129
|
-
|
130
|
-
@config.logger.log(Logger::INFO, "User '#{user_id}' is forced in variation '#{forced_variation_key}'.")
|
131
|
-
forced_variation_id
|
132
|
-
end
|
133
|
-
|
134
104
|
private
|
135
105
|
|
136
106
|
def find_bucket(bucket_value, traffic_allocations)
|
@@ -0,0 +1,190 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2017, Optimizely and contributors
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
require_relative './bucketer'
|
17
|
+
|
18
|
+
module Optimizely
|
19
|
+
class DecisionService
|
20
|
+
# Optimizely's decision service that determines into which variation of an experiment a user will be allocated.
|
21
|
+
#
|
22
|
+
# The decision service contains all logic relating to how a user bucketing decisions is made.
|
23
|
+
# This includes all of the following (in order):
|
24
|
+
#
|
25
|
+
# 1. Checking experiment status
|
26
|
+
# 2. Checking whitelisting
|
27
|
+
# 3. Checking user profile service for past bucketing decisions (sticky bucketing)
|
28
|
+
# 3. Checking audience targeting
|
29
|
+
# 4. Using Murmurhash3 to bucket the user
|
30
|
+
|
31
|
+
attr_reader :bucketer
|
32
|
+
attr_reader :config
|
33
|
+
|
34
|
+
def initialize(config, user_profile_service = nil)
|
35
|
+
@config = config
|
36
|
+
@user_profile_service = user_profile_service
|
37
|
+
@bucketer = Bucketer.new(@config)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_variation(experiment_key, user_id, attributes = nil)
|
41
|
+
# Determines variation into which user will be bucketed.
|
42
|
+
#
|
43
|
+
# experiment_key - Experiment for which visitor variation needs to be determined
|
44
|
+
# user_id - String ID for user
|
45
|
+
# attributes - Hash representing user attributes
|
46
|
+
#
|
47
|
+
# Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
|
48
|
+
|
49
|
+
# Check to make sure experiment is active
|
50
|
+
unless @config.experiment_running?(experiment_key)
|
51
|
+
@config.logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
55
|
+
experiment_id = @config.get_experiment_id(experiment_key)
|
56
|
+
|
57
|
+
# Check if user is in a forced variation
|
58
|
+
forced_variation_id = get_forced_variation_id(experiment_key, user_id)
|
59
|
+
return forced_variation_id if forced_variation_id
|
60
|
+
|
61
|
+
# Check for saved bucketing decisions
|
62
|
+
user_profile = get_user_profile(user_id)
|
63
|
+
saved_variation_id = get_saved_variation_id(experiment_id, user_profile)
|
64
|
+
if saved_variation_id
|
65
|
+
@config.logger.log(
|
66
|
+
Logger::INFO,
|
67
|
+
"Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
|
68
|
+
)
|
69
|
+
return saved_variation_id
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check audience conditions
|
73
|
+
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
|
74
|
+
@config.logger.log(
|
75
|
+
Logger::INFO,
|
76
|
+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
|
77
|
+
)
|
78
|
+
return nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# Bucket normally
|
82
|
+
variation_id = @bucketer.bucket(experiment_key, user_id)
|
83
|
+
|
84
|
+
# Persist bucketing decision
|
85
|
+
save_user_profile(user_profile, experiment_id, variation_id)
|
86
|
+
variation_id
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def get_forced_variation_id(experiment_key, user_id)
|
92
|
+
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
|
93
|
+
#
|
94
|
+
# experiment_key - Key representing the experiment for which user is to be bucketed
|
95
|
+
# user_id - ID for the user
|
96
|
+
#
|
97
|
+
# Returns variation ID into which user_id is forced (nil if no variation)
|
98
|
+
|
99
|
+
forced_variations = @config.get_forced_variations(experiment_key)
|
100
|
+
|
101
|
+
return nil unless forced_variations
|
102
|
+
|
103
|
+
forced_variation_key = forced_variations[user_id]
|
104
|
+
|
105
|
+
return nil unless forced_variation_key
|
106
|
+
|
107
|
+
forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
|
108
|
+
|
109
|
+
unless forced_variation_id
|
110
|
+
@config.logger.log(
|
111
|
+
Logger::INFO,
|
112
|
+
"User '#{user_id}' is whitelisted into variation '#{forced_variation_key}', which is not in the datafile."
|
113
|
+
)
|
114
|
+
return nil
|
115
|
+
end
|
116
|
+
|
117
|
+
@config.logger.log(
|
118
|
+
Logger::INFO,
|
119
|
+
"User '#{user_id}' is whitelisted into variation '#{forced_variation_key}' of experiment '#{experiment_key}'."
|
120
|
+
)
|
121
|
+
forced_variation_id
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_saved_variation_id(experiment_id, user_profile)
|
125
|
+
# Retrieve variation ID of stored bucketing decision for a given experiment from a given user profile
|
126
|
+
#
|
127
|
+
# experiment_id - String experiment ID
|
128
|
+
# user_profile - Hash user profile
|
129
|
+
#
|
130
|
+
# Returns string variation ID (nil if no decision is found)
|
131
|
+
return nil unless user_profile[:experiment_bucket_map]
|
132
|
+
|
133
|
+
decision = user_profile[:experiment_bucket_map][experiment_id]
|
134
|
+
return nil unless decision
|
135
|
+
variation_id = decision[:variation_id]
|
136
|
+
return variation_id if @config.variation_id_exists?(experiment_id, variation_id)
|
137
|
+
|
138
|
+
@config.logger.log(
|
139
|
+
Logger::INFO,
|
140
|
+
"User '#{user_profile['user_id']}' was previously bucketed into variation ID '#{variation_id}' for experiment '#{experiment_id}', but no matching variation was found. Re-bucketing user."
|
141
|
+
)
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def get_user_profile(user_id)
|
146
|
+
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
|
147
|
+
#
|
148
|
+
# user_id - String ID for the user
|
149
|
+
#
|
150
|
+
# Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
|
151
|
+
|
152
|
+
user_profile = {
|
153
|
+
:user_id => user_id,
|
154
|
+
:experiment_bucket_map => {}
|
155
|
+
}
|
156
|
+
|
157
|
+
return user_profile unless @user_profile_service
|
158
|
+
|
159
|
+
begin
|
160
|
+
user_profile = @user_profile_service.lookup(user_id) || user_profile
|
161
|
+
rescue => e
|
162
|
+
@config.logger.log(Logger::ERROR, "Error while looking up user profile for user ID '#{user_id}': #{e}.")
|
163
|
+
end
|
164
|
+
|
165
|
+
user_profile
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
def save_user_profile(user_profile, experiment_id, variation_id)
|
170
|
+
# Save a given bucketing decision to a given user profile
|
171
|
+
#
|
172
|
+
# user_profile - Hash user profile
|
173
|
+
# experiment_id - String experiment ID
|
174
|
+
# variation_id - String variation ID
|
175
|
+
|
176
|
+
return unless @user_profile_service
|
177
|
+
|
178
|
+
user_id = user_profile[:user_id]
|
179
|
+
begin
|
180
|
+
user_profile[:experiment_bucket_map][experiment_id] = {
|
181
|
+
:variation_id => variation_id
|
182
|
+
}
|
183
|
+
@user_profile_service.save(user_profile)
|
184
|
+
@config.logger.log(Logger::INFO, "Saved variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
|
185
|
+
rescue => e
|
186
|
+
@config.logger.log(Logger::ERROR, "Error while saving user profile for user ID '#{user_id}': #{e}.")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -294,6 +294,29 @@ module Optimizely
|
|
294
294
|
@parsing_succeeded
|
295
295
|
end
|
296
296
|
|
297
|
+
def variation_id_exists?(experiment_id, variation_id)
|
298
|
+
# Determines if a given experiment ID / variation ID pair exists in the datafile
|
299
|
+
#
|
300
|
+
# experiment_id - String experiment ID
|
301
|
+
# variation_id - String variation ID
|
302
|
+
#
|
303
|
+
# Returns true if variation is in datafile
|
304
|
+
|
305
|
+
experiment_key = get_experiment_key(experiment_id)
|
306
|
+
variation_id_map = @variation_id_map[experiment_key]
|
307
|
+
if variation_id_map
|
308
|
+
variation = variation_id_map[variation_id]
|
309
|
+
return true if variation
|
310
|
+
@logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
|
311
|
+
@error_handler.handle_error InvalidVariationError
|
312
|
+
return false
|
313
|
+
end
|
314
|
+
|
315
|
+
@logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
|
316
|
+
@error_handler.handle_error InvalidExperimentError
|
317
|
+
false
|
318
|
+
end
|
319
|
+
|
297
320
|
private
|
298
321
|
|
299
322
|
def generate_key_map(array, key)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2017, Optimizely and contributors
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
module Optimizely
|
18
|
+
class BaseUserProfileService
|
19
|
+
# Class encapsulating user profile service functionality.
|
20
|
+
# Override with your own implementation for storing and retrieving user profiles.
|
21
|
+
|
22
|
+
def lookup(user_id)
|
23
|
+
# Retrieve the Hash user profile associated with a given user ID.
|
24
|
+
#
|
25
|
+
# user_id - String user ID
|
26
|
+
#
|
27
|
+
# Returns Hash user profile.
|
28
|
+
end
|
29
|
+
|
30
|
+
def save(user_profile)
|
31
|
+
# Saves a given user profile.
|
32
|
+
#
|
33
|
+
# user_profile - Hash user profile.
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/optimizely/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: optimizely-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Delikat
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2017-05-
|
13
|
+
date: 2017-05-24 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: bundler
|
@@ -121,6 +121,7 @@ files:
|
|
121
121
|
- lib/optimizely/audience.rb
|
122
122
|
- lib/optimizely/bucketer.rb
|
123
123
|
- lib/optimizely/condition.rb
|
124
|
+
- lib/optimizely/decision_service.rb
|
124
125
|
- lib/optimizely/error_handler.rb
|
125
126
|
- lib/optimizely/event_builder.rb
|
126
127
|
- lib/optimizely/event_dispatcher.rb
|
@@ -132,6 +133,7 @@ files:
|
|
132
133
|
- lib/optimizely/logger.rb
|
133
134
|
- lib/optimizely/params.rb
|
134
135
|
- lib/optimizely/project_config.rb
|
136
|
+
- lib/optimizely/user_profile_service.rb
|
135
137
|
- lib/optimizely/version.rb
|
136
138
|
homepage: https://www.optimizely.com/
|
137
139
|
licenses:
|
@@ -153,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
155
|
version: '0'
|
154
156
|
requirements: []
|
155
157
|
rubyforge_project:
|
156
|
-
rubygems_version: 2.6.
|
158
|
+
rubygems_version: 2.6.10
|
157
159
|
signing_key:
|
158
160
|
specification_version: 4
|
159
161
|
summary: Ruby SDK for Optimizely's testing framework
|