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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8eff682f4c4c99808f246f2828a4e34e276e54b
4
- data.tar.gz: 1c4de8ae37ce421199a1d66ea67c5e9ba78e1c41
3
+ metadata.gz: 164c4cd46301497d6ad393c5ed49093f9e72d337
4
+ data.tar.gz: b52ca87752dc4aa840e53c4be5ce7583604eb044
5
5
  SHA512:
6
- metadata.gz: 32ad714501f8b55a48201653de322c2f47e33634d323bb653909307cfc289ceff833ba44508b823ecfe0a4ade94cae2aac8ecca5bd2d6ee350d1ec82977a2a69
7
- data.tar.gz: 96297561d1ee0a395d35f8b9878ac71b2c097583442bf7327b01db773ef13b32a5aa223c288beeb10f1da75ff4d713baf511286a150bf5a38cd5ea5e5bf2c1cd
6
+ metadata.gz: dbdcbe648b51889c656578e8962c737380b0ab255be1941c5df48bdef9740c8e5e2d0aa2fd814f697e24197b36aaa8759f3882211059f7e2be55facfbd13e5e6
7
+ data.tar.gz: 064f914a6ddc35ae884b7d3563e7791da2d48030e292796422df5e5191a6de920959dfeca0d0a7bde6646f1ac6a1581526f0ed08df50084eb8da7d73f16b08aa
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  require_relative 'optimizely/audience'
17
- require_relative 'optimizely/bucketer'
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 :is_valid
31
+ attr_reader :is_valid
32
32
 
33
- attr_accessor :config
34
- attr_accessor :bucketer
35
- attr_accessor :event_builder
36
- attr_accessor :event_dispatcher
37
- attr_accessor :logger
38
- attr_accessor :error_handler
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 param which provides a log method to log messages. By default nothing would be logged.
46
- # error_handler - Optional param which provides a handle_error method to handle exceptions.
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
- @bucketer = Bucketer.new(@config)
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 preconditions_valid?(experiment_key, attributes)
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 = @bucketer.get_forced_variation_id(experiment_key, user_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
  #
@@ -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
@@ -42,7 +42,7 @@ module Optimizely
42
42
 
43
43
  class BaseEventBuilder
44
44
  attr_reader :config
45
- attr_accessor :params
45
+ attr_reader :params
46
46
 
47
47
  def initialize(config)
48
48
  @config = config
@@ -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
@@ -14,5 +14,5 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  module Optimizely
17
- VERSION = '1.2.0'.freeze
17
+ VERSION = '1.3.0'.freeze
18
18
  end
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.2.0
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-05 00:00:00.000000000 Z
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.11
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