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 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