optimizely-sdk 1.2.0 → 1.3.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 +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
|