optimizely-sdk 1.3.0 → 1.4.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: 164c4cd46301497d6ad393c5ed49093f9e72d337
4
- data.tar.gz: b52ca87752dc4aa840e53c4be5ce7583604eb044
3
+ metadata.gz: '05059bf31a28b9a72a3f4ecb95808bf46671438b'
4
+ data.tar.gz: 481be969d392df0eb93bf8157f13d7625912d613
5
5
  SHA512:
6
- metadata.gz: dbdcbe648b51889c656578e8962c737380b0ab255be1941c5df48bdef9740c8e5e2d0aa2fd814f697e24197b36aaa8759f3882211059f7e2be55facfbd13e5e6
7
- data.tar.gz: 064f914a6ddc35ae884b7d3563e7791da2d48030e292796422df5e5191a6de920959dfeca0d0a7bde6646f1ac6a1581526f0ed08df50084eb8da7d73f16b08aa
6
+ metadata.gz: 5f5099fc8956cdeec7add609b9c13ec2cb1f7e0524889c626001afb9c24ec456f642bcf2307da4114c0747b1714faa70861820d3e0ed9fcb901f164f5400034b
7
+ data.tar.gz: f327bb0d528621dbcddea84d7a0495e1ceec473a616d5ce3afb5a26386794a45452cf1ce83deca7f0d2f0bab0ed2af6dfa3e08a6ec5313558169c25c78a6772a
data/lib/optimizely.rb CHANGED
@@ -80,7 +80,7 @@ module Optimizely
80
80
  end
81
81
 
82
82
  @decision_service = DecisionService.new(@config, @user_profile_service)
83
- @event_builder = EventBuilderV2.new(@config)
83
+ @event_builder = EventBuilder.new(@config)
84
84
  end
85
85
 
86
86
  def activate(experiment_key, user_id, attributes = nil)
@@ -108,7 +108,8 @@ module Optimizely
108
108
 
109
109
  # Create and dispatch impression event
110
110
  variation_id = @config.get_variation_id_from_key(experiment_key, variation_key)
111
- impression_event = @event_builder.create_impression_event(experiment_key, variation_id, user_id, attributes)
111
+ experiment = @config.get_experiment_from_key(experiment_key)
112
+ impression_event = @event_builder.create_impression_event(experiment, variation_id, user_id, attributes)
112
113
  @logger.log(Logger::INFO,
113
114
  'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
114
115
  impression_event.params])
@@ -137,6 +138,11 @@ module Optimizely
137
138
  return nil
138
139
  end
139
140
 
141
+ unless user_id.is_a? String
142
+ @logger.log(Logger::ERROR, "User id: #{user_id} is not a string")
143
+ return nil
144
+ end
145
+
140
146
  unless user_inputs_valid?(attributes)
141
147
  @logger.log(Logger::INFO, "Not activating user '#{user_id}.")
142
148
  return nil
@@ -150,6 +156,36 @@ module Optimizely
150
156
  nil
151
157
  end
152
158
 
159
+ def set_forced_variation(experiment_key, user_id, variation_key)
160
+ # Force a user into a variation for a given experiment.
161
+ #
162
+ # experiment_key - String - key identifying the experiment.
163
+ # user_id - String - The user ID to be used for bucketing.
164
+ # variation_key - The variation key specifies the variation which the user will
165
+ # be forced into. If nil, then clear the existing experiment-to-variation mapping.
166
+ #
167
+ # Returns - Boolean - indicates if the set completed successfully.
168
+
169
+ @config.set_forced_variation(experiment_key, user_id, variation_key);
170
+ end
171
+
172
+ def get_forced_variation(experiment_key, user_id)
173
+ # Gets the forced variation for a given user and experiment.
174
+ #
175
+ # experiment_key - String - Key identifying the experiment.
176
+ # user_id - String - The user ID to be used for bucketing.
177
+ #
178
+ # Returns String|nil The forced variation key.
179
+
180
+ forced_variation_key = nil
181
+ forced_variation = @config.get_forced_variation(experiment_key, user_id);
182
+ if forced_variation
183
+ forced_variation_key = forced_variation['key']
184
+ end
185
+
186
+ forced_variation_key
187
+ end
188
+
153
189
  def track(event_key, user_id, attributes = nil, event_tags = nil)
154
190
  # Send conversion event to Optimizely.
155
191
  #
@@ -20,17 +20,17 @@ module Optimizely
20
20
  module Audience
21
21
  module_function
22
22
 
23
- def user_in_experiment?(config, experiment_key, attributes)
23
+ def user_in_experiment?(config, experiment, attributes)
24
24
  # Determine for given experiment if user satisfies the audiences for the experiment.
25
25
  #
26
26
  # config - Representation of the Optimizely project config.
27
- # experiment_key - Key representing experiment for which visitor is to be bucketed.
27
+ # experiment - Experiment for which visitor is to be bucketed.
28
28
  # attributes - Hash representing user attributes which will be used in determining if
29
29
  # the audience conditions are met.
30
30
  #
31
31
  # Returns boolean representing if user satisfies audience conditions for any of the audiences or not.
32
32
 
33
- audience_ids = config.get_audience_ids_for_experiment(experiment_key)
33
+ audience_ids = experiment['audienceIds']
34
34
 
35
35
  # Return true if there are no audiences
36
36
  return true if audience_ids.empty?
@@ -35,26 +35,23 @@ module Optimizely
35
35
  @config = config
36
36
  end
37
37
 
38
- def bucket(experiment_key, user_id)
38
+ def bucket(experiment, user_id)
39
39
  # Determines ID of variation to be shown for a given experiment key and user ID.
40
40
  #
41
- # experiment_key - String Key representing experiment for which visitor is to be bucketed.
41
+ # experiment - Experiment for which visitor is to be bucketed.
42
42
  # user_id - String ID for user.
43
43
  #
44
- # Returns String variation ID in which visitor with ID user_id has been placed. Nil if no variation.
44
+ # Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
45
45
 
46
46
  # check if experiment is in a group; if so, check if user is bucketed into specified experiment
47
- experiment_id = @config.get_experiment_id(experiment_key)
48
- group_id = @config.get_experiment_group_id(experiment_key)
47
+ experiment_id = experiment['id']
48
+ experiment_key = experiment['key']
49
+ group_id = experiment['groupId']
49
50
  if group_id
50
51
  group = @config.group_key_map.fetch(group_id)
51
52
  if Helpers::Group.random_policy?(group)
52
- bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: group_id)
53
53
  traffic_allocations = group.fetch('trafficAllocation')
54
- bucket_value = generate_bucket_value(bucketing_id)
55
- @config.logger.log(Logger::DEBUG, "Assigned experiment bucket #{bucket_value} to user '#{user_id}'.")
56
- bucketed_experiment_id = find_bucket(bucket_value, traffic_allocations)
57
-
54
+ bucketed_experiment_id = find_bucket(user_id, group_id, traffic_allocations)
58
55
  # return if the user is not bucketed into any experiment
59
56
  unless bucketed_experiment_id
60
57
  @config.logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
@@ -78,18 +75,16 @@ module Optimizely
78
75
  end
79
76
  end
80
77
 
81
- bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: experiment_id)
82
- bucket_value = generate_bucket_value(bucketing_id)
83
- @config.logger.log(Logger::DEBUG, "Assigned variation bucket #{bucket_value} to user '#{user_id}'.")
84
- traffic_allocations = @config.get_traffic_allocation(experiment_key)
85
- variation_id = find_bucket(bucket_value, traffic_allocations)
78
+ traffic_allocations = experiment['trafficAllocation']
79
+ variation_id = find_bucket(user_id, experiment_id, traffic_allocations)
86
80
  if variation_id && variation_id != ''
87
- variation_key = @config.get_variation_key_from_id(experiment_key, variation_id)
81
+ variation = @config.get_variation_from_id(experiment_key, variation_id)
82
+ variation_key = variation ? variation['key'] : nil
88
83
  @config.logger.log(
89
84
  Logger::INFO,
90
85
  "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
91
86
  )
92
- return variation_id
87
+ return variation
93
88
  end
94
89
 
95
90
  # Handle the case when the traffic range is empty due to sticky bucketing
@@ -101,16 +96,19 @@ module Optimizely
101
96
  nil
102
97
  end
103
98
 
104
- private
105
-
106
- def find_bucket(bucket_value, traffic_allocations)
99
+ def find_bucket(user_id, parent_id, traffic_allocations)
107
100
  # Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
108
101
  #
109
- # bucket_value - Integer bucket value
102
+ # user_id - String ID for user
103
+ # parent_id - String entity ID to use for bucketing ID
110
104
  # traffic_allocations - Array of traffic allocations
111
105
  #
112
106
  # Returns entity ID corresponding to the provided bucket value or nil if no match is found.
113
107
 
108
+ bucketing_id = sprintf(BUCKETING_ID_TEMPLATE, user_id: user_id, entity_id: parent_id)
109
+ bucket_value = generate_bucket_value(bucketing_id)
110
+ @config.logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}'.")
111
+
114
112
  traffic_allocations.each do |traffic_allocation|
115
113
  current_end_of_range = traffic_allocation['endOfRange']
116
114
  if bucket_value < current_end_of_range
@@ -122,6 +120,8 @@ module Optimizely
122
120
  nil
123
121
  end
124
122
 
123
+ private
124
+
125
125
  def generate_bucket_value(bucketing_id)
126
126
  # Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
127
127
  #
@@ -143,4 +143,4 @@ module Optimizely
143
143
  MurmurHash3::V32.str_hash(bucketing_id, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
144
144
  end
145
145
  end
146
- end
146
+ end
@@ -22,11 +22,12 @@ module Optimizely
22
22
  # The decision service contains all logic relating to how a user bucketing decisions is made.
23
23
  # This includes all of the following (in order):
24
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
25
+ # 1. Check experiment status
26
+ # 2. Check forced bucketing
27
+ # 3. Check whitelisting
28
+ # 4. Check user profile service for past bucketing decisions (sticky bucketing)
29
+ # 5. Check audience targeting
30
+ # 6. Use Murmurhash3 to bucket the user
30
31
 
31
32
  attr_reader :bucketer
32
33
  attr_reader :config
@@ -47,16 +48,24 @@ module Optimizely
47
48
  # Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
48
49
 
49
50
  # Check to make sure experiment is active
50
- unless @config.experiment_running?(experiment_key)
51
+ experiment = @config.get_experiment_from_key(experiment_key)
52
+ if experiment.nil?
53
+ return nil
54
+ end
55
+
56
+ experiment_id = experiment['id']
57
+ unless @config.experiment_running?(experiment)
51
58
  @config.logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
52
59
  return nil
53
60
  end
54
61
 
55
- experiment_id = @config.get_experiment_id(experiment_key)
62
+ # Check if a forced variation is set for the user
63
+ forced_variation = @config.get_forced_variation(experiment_key, user_id)
64
+ return forced_variation['id'] if forced_variation
56
65
 
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
66
+ # Check if user is in a white-listed variation
67
+ whitelisted_variation_id = get_whitelisted_variation_id(experiment_key, user_id)
68
+ return whitelisted_variation_id if whitelisted_variation_id
60
69
 
61
70
  # Check for saved bucketing decisions
62
71
  user_profile = get_user_profile(user_id)
@@ -70,7 +79,7 @@ module Optimizely
70
79
  end
71
80
 
72
81
  # Check audience conditions
73
- unless Audience.user_in_experiment?(@config, experiment_key, attributes)
82
+ unless Audience.user_in_experiment?(@config, experiment, attributes)
74
83
  @config.logger.log(
75
84
  Logger::INFO,
76
85
  "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
@@ -79,7 +88,8 @@ module Optimizely
79
88
  end
80
89
 
81
90
  # Bucket normally
82
- variation_id = @bucketer.bucket(experiment_key, user_id)
91
+ variation = @bucketer.bucket(experiment, user_id)
92
+ variation_id = variation ? variation['id'] : nil
83
93
 
84
94
  # Persist bucketing decision
85
95
  save_user_profile(user_profile, experiment_id, variation_id)
@@ -87,38 +97,38 @@ module Optimizely
87
97
  end
88
98
 
89
99
  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
100
+
101
+ def get_whitelisted_variation_id(experiment_key, user_id)
102
+ # Determine if a user is whitelisted into a variation for the given experiment and return the ID of that variation
93
103
  #
94
104
  # experiment_key - Key representing the experiment for which user is to be bucketed
95
105
  # user_id - ID for the user
96
106
  #
97
- # Returns variation ID into which user_id is forced (nil if no variation)
107
+ # Returns variation ID into which user_id is whitelisted (nil if no variation)
98
108
 
99
- forced_variations = @config.get_forced_variations(experiment_key)
109
+ whitelisted_variations = @config.get_whitelisted_variations(experiment_key)
100
110
 
101
- return nil unless forced_variations
111
+ return nil unless whitelisted_variations
102
112
 
103
- forced_variation_key = forced_variations[user_id]
113
+ whitelisted_variation_key = whitelisted_variations[user_id]
104
114
 
105
- return nil unless forced_variation_key
115
+ return nil unless whitelisted_variation_key
106
116
 
107
- forced_variation_id = @config.get_variation_id_from_key(experiment_key, forced_variation_key)
117
+ whitelisted_variation_id = @config.get_variation_id_from_key(experiment_key, whitelisted_variation_key)
108
118
 
109
- unless forced_variation_id
119
+ unless whitelisted_variation_id
110
120
  @config.logger.log(
111
121
  Logger::INFO,
112
- "User '#{user_id}' is whitelisted into variation '#{forced_variation_key}', which is not in the datafile."
122
+ "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}', which is not in the datafile."
113
123
  )
114
124
  return nil
115
125
  end
116
126
 
117
127
  @config.logger.log(
118
128
  Logger::INFO,
119
- "User '#{user_id}' is whitelisted into variation '#{forced_variation_key}' of experiment '#{experiment_key}'."
129
+ "User '#{user_id}' is whitelisted into variation '#{whitelisted_variation_key}' of experiment '#{experiment_key}'."
120
130
  )
121
- forced_variation_id
131
+ whitelisted_variation_id
122
132
  end
123
133
 
124
134
  def get_saved_variation_id(experiment_id, user_profile)
@@ -147,7 +157,7 @@ module Optimizely
147
157
  #
148
158
  # user_id - String ID for the user
149
159
  #
150
- # Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
160
+ # Returns Hash stored user profile (or a default one if lookup fails or user profile service not provided)
151
161
 
152
162
  user_profile = {
153
163
  :user_id => user_id,
@@ -17,6 +17,7 @@ require_relative './audience'
17
17
  require_relative './params'
18
18
  require_relative './version'
19
19
  require_relative '../optimizely/helpers/event_tag_utils'
20
+ require 'securerandom'
20
21
 
21
22
  module Optimizely
22
23
  class Event
@@ -41,197 +42,198 @@ module Optimizely
41
42
  end
42
43
 
43
44
  class BaseEventBuilder
45
+ CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'
46
+
44
47
  attr_reader :config
45
- attr_reader :params
46
48
 
47
49
  def initialize(config)
48
50
  @config = config
49
- @params = {}
50
51
  end
51
52
 
52
53
  private
53
54
 
54
- def add_common_params(user_id, attributes)
55
- # Add params which are used in both conversion and impression events.
56
- #
57
- # user_id - ID for user.
58
- # attributes - Hash representing user attributes and values which need to be recorded.
59
-
60
- add_project_id
61
- add_account_id
62
- add_user_id(user_id)
63
- add_attributes(attributes)
64
- add_source
65
- add_time
55
+ def get_common_params(user_id, attributes)
56
+ # Get params which are used in both conversion and impression events.
57
+ #
58
+ # user_id - +String+ ID for user
59
+ # attributes - +Hash+ representing user attributes and values which need to be recorded.
60
+ #
61
+ # Returns +Hash+ Common event params
62
+
63
+ visitor_attributes = []
64
+
65
+ unless attributes.nil?
66
+
67
+ attributes.keys.each do |attribute_key|
68
+ # Omit null attribute value
69
+ attribute_value = attributes[attribute_key]
70
+ next if attribute_value.nil?
71
+
72
+ # Skip attributes not in the datafile
73
+ attribute_id = @config.get_attribute_id(attribute_key)
74
+ next unless attribute_id
75
+
76
+ feature = {
77
+ entity_id: attribute_id,
78
+ key: attribute_key,
79
+ type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
80
+ value: attribute_value
81
+ }
82
+
83
+ visitor_attributes.push(feature)
84
+ end
85
+ end
86
+
87
+ common_params = {
88
+ account_id: @config.account_id,
89
+ project_id: @config.project_id,
90
+ visitors: [
91
+ {
92
+ attributes: visitor_attributes,
93
+ snapshots: [],
94
+ visitor_id: user_id
95
+ }
96
+ ],
97
+ revision: @config.revision,
98
+ client_name: CLIENT_ENGINE,
99
+ client_version: VERSION
100
+ }
101
+
102
+ common_params
66
103
  end
67
104
  end
68
105
 
69
- class EventBuilderV2 < BaseEventBuilder
70
- CONVERSION_EVENT_ENDPOINT = 'https://logx.optimizely.com/log/event'
71
- IMPRESSION_EVENT_ENDPOINT = 'https://logx.optimizely.com/log/decision'
106
+ class EventBuilder < BaseEventBuilder
107
+ ENDPOINT = 'https://logx.optimizely.com/v1/events'
72
108
  POST_HEADERS = { 'Content-Type' => 'application/json' }
109
+ ACTIVATE_EVENT_KEY = 'campaign_activated'
73
110
 
74
- def create_impression_event(experiment_key, variation_id, user_id, attributes)
75
- # Create conversion Event to be sent to the logging endpoint.
111
+ def create_impression_event(experiment, variation_id, user_id, attributes)
112
+ # Create impression Event to be sent to the logging endpoint.
76
113
  #
77
- # experiment_key - Experiment for which impression needs to be recorded.
78
- # variation_id - ID for variation which would be presented to user.
79
- # user_id - ID for user.
80
- # attributes - Hash representing user attributes and values which need to be recorded.
114
+ # experiment - +Object+ Experiment for which impression needs to be recorded.
115
+ # variation_id - +String+ ID for variation which would be presented to user.
116
+ # user_id - +String+ ID for user.
117
+ # attributes - +Hash+ representing user attributes and values which need to be recorded.
81
118
  #
82
- # Returns event hash encapsulating the impression event.
119
+ # Returns +Event+ encapsulating the impression event.
83
120
 
84
- @params = {}
85
- add_common_params(user_id, attributes)
86
- add_decision(experiment_key, variation_id)
87
- add_attributes(attributes)
88
- Event.new(:post, IMPRESSION_EVENT_ENDPOINT, @params, POST_HEADERS)
121
+ event_params = get_common_params(user_id, attributes)
122
+ impression_params = get_impression_params(experiment, variation_id)
123
+ event_params[:visitors][0][:snapshots].push(impression_params)
124
+
125
+ Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
89
126
  end
90
127
 
91
128
  def create_conversion_event(event_key, user_id, attributes, event_tags, experiment_variation_map)
92
129
  # Create conversion Event to be sent to the logging endpoint.
93
130
  #
94
- # event_key - Event key representing the event which needs to be recorded.
95
- # user_id - ID for user.
96
- # attributes - Hash representing user attributes and values which need to be recorded.
97
- # event_tags - Hash representing metadata associated with the event.
98
- # experiment_variation_map - Map of experiment ID to the ID of the variation that the user is bucketed into.
131
+ # event_key - +String+ Event key representing the event which needs to be recorded.
132
+ # user_id - +String+ ID for user.
133
+ # attributes - +Hash+ representing user attributes and values which need to be recorded.
134
+ # event_tags - +Hash+ representing metadata associated with the event.
135
+ # experiment_variation_map - +Map+ of experiment ID to the ID of the variation that the user is bucketed into.
99
136
  #
100
- # Returns event hash encapsulating the conversion event.
101
-
102
- @params = {}
103
- add_common_params(user_id, attributes)
104
- add_conversion_event(event_key)
105
- add_event_tags(event_tags)
106
- add_layer_states(experiment_variation_map)
107
- Event.new(:post, CONVERSION_EVENT_ENDPOINT, @params, POST_HEADERS)
108
- end
109
-
110
- private
137
+ # Returns +Event+ encapsulating the conversion event.
111
138
 
112
- def add_common_params(user_id, attributes)
113
- super
114
- @params['isGlobalHoldback'] = false
139
+ event_params = get_common_params(user_id, attributes)
140
+ conversion_params = get_conversion_params(event_key, event_tags, experiment_variation_map)
141
+ event_params[:visitors][0][:snapshots] = conversion_params;
142
+
143
+ Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
115
144
  end
116
145
 
117
- def add_project_id
118
- @params['projectId'] = @config.project_id
119
- end
146
+ private
120
147
 
121
- def add_account_id
122
- @params['accountId'] = @config.account_id
123
- end
148
+ def get_impression_params(experiment, variation_id)
149
+ # Creates object of params specific to impression events
150
+ #
151
+ # experiment - +Hash+ experiment for which impression needs to be recorded
152
+ # variation_id - +string+ ID for variation which would be presented to user
153
+ #
154
+ # Returns +Hash+ Impression event params
155
+
156
+ experiment_key = experiment['key']
157
+ experiment_id = experiment['id']
158
+
159
+ impressionEventParams = {
160
+ decisions: [{
161
+ campaign_id: @config.experiment_key_map[experiment_key]['layerId'],
162
+ experiment_id: experiment_id,
163
+ variation_id: variation_id,
164
+ }],
165
+ events: [{
166
+ entity_id: @config.experiment_key_map[experiment_key]['layerId'],
167
+ timestamp: get_timestamp(),
168
+ key: ACTIVATE_EVENT_KEY,
169
+ uuid: get_uuid()
170
+ }]
171
+ }
124
172
 
125
- def add_user_id(user_id)
126
- @params['visitorId'] = user_id
173
+ impressionEventParams;
127
174
  end
128
175
 
129
- def add_attributes(attributes)
130
- @params['userFeatures'] = []
131
-
132
- return if attributes.nil?
176
+ def get_conversion_params(event_key, event_tags, experiment_variation_map)
177
+ # Creates object of params specific to conversion events
178
+ #
179
+ # event_key - +String+ Key representing the event which needs to be recorded
180
+ # event_tags - +Hash+ Values associated with the event.
181
+ # experiment_variation_map - +Hash+ Map of experiment IDs to bucketed variation IDs
182
+ #
183
+ # Returns +Hash+ Impression event params
133
184
 
134
- attributes.keys.each do |attribute_key|
135
- # Omit falsy attribute values
136
- attribute_value = attributes[attribute_key]
137
- next unless attribute_value
185
+ conversionEventParams = []
138
186
 
139
- # Skip attributes not in the datafile
140
- attribute_id = @config.get_attribute_id(attribute_key)
141
- next unless attribute_id
187
+ experiment_variation_map.each do |experiment_id, variation_id|
142
188
 
143
- feature = {
144
- 'id' => attribute_id,
145
- 'name' => attribute_key,
146
- 'type' => 'custom',
147
- 'value' => attribute_value,
148
- 'shouldIndex' => true,
189
+ single_snapshot = {
190
+ decisions: [{
191
+ campaign_id: @config.experiment_id_map[experiment_id]['layerId'],
192
+ experiment_id: experiment_id,
193
+ variation_id: variation_id,
194
+ }],
195
+ events: [],
149
196
  }
150
- @params['userFeatures'].push(feature)
151
- end
152
- end
153
-
154
- def add_decision(experiment_key, variation_id)
155
- experiment_id = @config.get_experiment_id(experiment_key)
156
- @params['layerId'] = @config.experiment_key_map[experiment_key]['layerId']
157
- @params['decision'] = {
158
- 'variationId' => variation_id,
159
- 'experimentId' => experiment_id,
160
- 'isLayerHoldback' => false,
161
- }
162
- end
163
-
164
- def add_event_tags(event_tags)
165
- @params['eventFeatures'] ||= []
166
- @params['eventMetrics'] ||= []
167
197
 
168
- return if event_tags.nil?
198
+ event_object = {
199
+ entity_id: @config.event_key_map[event_key]['id'],
200
+ timestamp: get_timestamp(),
201
+ uuid: get_uuid(),
202
+ key: event_key,
203
+ }
169
204
 
170
- event_tags.each_pair do |event_tag_key, event_tag_value|
171
- next if event_tag_value.nil?
205
+ if event_tags
206
+ revenue_value = Helpers::EventTagUtils.get_revenue_value(event_tags)
207
+ if revenue_value
208
+ event_object[:revenue] = revenue_value
209
+ end
172
210
 
173
- event_feature = {
174
- 'name' => event_tag_key,
175
- 'type' => 'custom',
176
- 'value' => event_tag_value,
177
- 'shouldIndex' => false,
178
- }
179
- @params['eventFeatures'].push(event_feature)
211
+ numeric_value = Helpers::EventTagUtils.get_numeric_value(event_tags)
212
+ if numeric_value
213
+ event_object[:value] = numeric_value
214
+ end
180
215
 
181
- end
216
+ event_object[:tags] = event_tags
217
+ end
182
218
 
183
- event_value = Helpers::EventTagUtils.get_revenue_value(event_tags)
219
+ single_snapshot[:events] = [event_object]
184
220
 
185
- if event_value
186
- event_metric = {
187
- 'name' => 'revenue',
188
- 'value' => event_value
189
- }
190
- @params['eventMetrics'].push(event_metric)
221
+ conversionEventParams.push(single_snapshot)
191
222
  end
192
-
223
+
224
+ conversionEventParams
193
225
  end
194
226
 
195
- def add_conversion_event(event_key)
196
- # Add conversion event information to the event.
197
- #
198
- # event_key - Event key representing the event which needs to be recorded.
199
-
200
- event_id = @config.event_key_map[event_key]['id']
201
- event_name = @config.event_key_map[event_key]['key']
227
+ def get_timestamp
228
+ # Returns +Integer+ Current timestamp
202
229
 
203
- @params['eventEntityId'] = event_id
204
- @params['eventName'] = event_name
230
+ (Time.now.to_f * 1000).to_i
205
231
  end
206
232
 
207
- def add_layer_states(experiments_map)
208
- # Add layer states information to the event.
209
- #
210
- # experiments_map - Hash with experiment ID as a key and variation ID as a value.
211
-
212
- @params['layerStates'] = []
213
-
214
- experiments_map.each do |experiment_id, variation_id|
215
- layer_state = {
216
- 'layerId' => @config.experiment_id_map[experiment_id]['layerId'],
217
- 'decision' => {
218
- 'variationId' => variation_id,
219
- 'experimentId' => experiment_id,
220
- 'isLayerHoldback' => false,
221
- },
222
- 'actionTriggered' => true,
223
- }
224
- @params['layerStates'].push(layer_state)
225
- end
226
- end
227
-
228
- def add_source
229
- @params['clientEngine'] = 'ruby-sdk'
230
- @params['clientVersion'] = VERSION
231
- end
233
+ def get_uuid
234
+ # Returns +String+ Random UUID
232
235
 
233
- def add_time
234
- @params['timestamp'] = (Time.now.to_f * 1000).to_i
236
+ SecureRandom.uuid
235
237
  end
236
238
  end
237
239
  end
@@ -21,23 +21,31 @@ module Optimizely
21
21
  module EventTagUtils
22
22
  module_function
23
23
 
24
+ REVENUE_EVENT_METRIC_NAME = 'revenue';
25
+ NUMERIC_EVENT_METRIC_NAME = 'value';
26
+
27
+ def isStringNumeric(str)
28
+ Float(str) != nil rescue false
29
+ end
30
+
24
31
  def get_revenue_value(event_tags)
25
32
  # Grab the revenue value from the event tags. "revenue" is a reserved keyword.
26
- #
33
+ # The revenue value must be an integer.
34
+ #
27
35
  # event_tags - Hash representing metadata associated with the event.
28
36
  # Returns revenue value as an integer number
29
37
  # Returns nil if revenue can't be retrieved from the event tags.
30
38
 
31
- if event_tags.nil? or !Helpers::Validator.attributes_valid?(event_tags)
39
+ if event_tags.nil? or !Helpers::Validator.event_tags_valid?(event_tags)
32
40
  return nil
33
41
  end
34
42
 
35
- unless event_tags.has_key?('revenue')
43
+ unless event_tags.has_key?(REVENUE_EVENT_METRIC_NAME)
36
44
  return nil
37
45
  end
38
46
 
39
47
  logger = SimpleLogger.new
40
- raw_value = event_tags['revenue']
48
+ raw_value = event_tags[REVENUE_EVENT_METRIC_NAME]
41
49
 
42
50
  unless raw_value.is_a? Numeric
43
51
  logger.log(Logger::WARN, "Failed to parse revenue value #{raw_value} from event tags.")
@@ -52,6 +60,59 @@ module Optimizely
52
60
  logger.log(Logger::INFO, "Parsed revenue value #{raw_value} from event tags.")
53
61
  raw_value
54
62
  end
63
+
64
+ def get_numeric_value(event_tags, logger = nil)
65
+ # Grab the numeric event value from the event tags. "value" is a reserved keyword.
66
+ # The value of 'value' can be a float or a numeric string
67
+ #
68
+ # event_tags - +Hash+ representing metadata associated with the event.
69
+ # Returns +Number+ | +nil+ if value can't be retrieved from the event tags.
70
+ logger = SimpleLogger.new if logger.nil?
71
+
72
+ if event_tags.nil?
73
+ logger.log(Logger::DEBUG,"Event tags is undefined.")
74
+ return nil
75
+ end
76
+
77
+ if !Helpers::Validator.event_tags_valid?(event_tags)
78
+ logger.log(Logger::DEBUG,"Event tags is not a dictionary.")
79
+ return nil
80
+ end
81
+
82
+ if !event_tags.has_key?(NUMERIC_EVENT_METRIC_NAME)
83
+ logger.log(Logger::DEBUG,"The numeric metric key is not defined in the event tags.")
84
+ return nil
85
+ end
86
+
87
+ if event_tags[NUMERIC_EVENT_METRIC_NAME].nil?
88
+ logger.log(Logger::DEBUG,"The numeric metric key is null.")
89
+ return nil
90
+ end
91
+
92
+ raw_value = event_tags[NUMERIC_EVENT_METRIC_NAME]
93
+
94
+ if raw_value === true or raw_value === false
95
+ logger.log(Logger::DEBUG,"Provided numeric value is a boolean, which is an invalid format.")
96
+ return nil
97
+ end
98
+
99
+ if raw_value.is_a? Array or raw_value.is_a? Hash or raw_value.to_f.nan? or raw_value.to_f.infinite?
100
+ logger.log(Logger::DEBUG,"Provided numeric value is in an invalid format.")
101
+ return nil
102
+ end
103
+
104
+ if !Helpers::Validator.string_numeric?(raw_value)
105
+ logger.log(Logger::DEBUG,"Provided numeric value is not a numeric string.")
106
+ return nil
107
+ end
108
+
109
+ raw_value = raw_value.to_f
110
+
111
+ logger.log(Logger::INFO,"The numeric metric value #{raw_value} will be sent to results.")
112
+
113
+ raw_value
114
+ end
115
+
55
116
  end
56
117
  end
57
- end
118
+ end
@@ -87,6 +87,11 @@ module Optimizely
87
87
 
88
88
  logger.respond_to?(:log)
89
89
  end
90
+
91
+ def string_numeric?(str)
92
+ Float(str) != nil rescue false
93
+ end
94
+
90
95
  end
91
96
  end
92
97
  end
@@ -25,10 +25,6 @@ module Optimizely
25
25
 
26
26
  class ProjectConfig
27
27
  # Representation of the Optimizely project config.
28
-
29
- PROJECT_CONFIG_LINK_TEMPLATE = 'https://cdn.optimizely.com/json/%{project_id}.json'
30
- REVENUE_GOAL_KEY = 'Total Revenue'
31
- REQUEST_TIMEOUT = 10
32
28
  RUNNING_EXPERIMENT_STATUS = ['Running']
33
29
 
34
30
  # Gets project config attributes.
@@ -56,6 +52,12 @@ module Optimizely
56
52
  attr_reader :variation_id_map
57
53
  attr_reader :variation_key_map
58
54
 
55
+ # Hash of user IDs to a Hash
56
+ # of experiments to variations. This contains all the forced variations
57
+ # set by the user by calling setForcedVariation (it is not the same as the
58
+ # whitelisting forcedVariations data structure in the Experiments class).
59
+ attr_reader :forced_variation_map
60
+
59
61
  def initialize(datafile, logger, error_handler)
60
62
  # ProjectConfig init method to fetch and set project config data
61
63
  #
@@ -96,6 +98,9 @@ module Optimizely
96
98
  @audience_id_map = generate_key_map(@audiences, 'id')
97
99
  @variation_id_map = {}
98
100
  @variation_key_map = {}
101
+ @forced_variation_map = {}
102
+ @variation_id_to_variable_usage_map = {}
103
+ @variation_id_to_experiment_map = {}
99
104
  @experiment_key_map.each do |key, exp|
100
105
  variations = exp.fetch('variations')
101
106
  @variation_id_map[key] = generate_key_map(variations, 'id')
@@ -104,28 +109,24 @@ module Optimizely
104
109
  @parsing_succeeded = true
105
110
  end
106
111
 
107
- def experiment_running?(experiment_key)
112
+ def experiment_running?(experiment)
108
113
  # Determine if experiment corresponding to given key is running
109
114
  #
110
- # experiment_key - String key representing the experiment
115
+ # experiment - Experiment
111
116
  #
112
117
  # Returns true if experiment is running
113
- experiment = @experiment_key_map[experiment_key]
114
- return RUNNING_EXPERIMENT_STATUS.include?(experiment['status']) if experiment
115
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
116
- @error_handler.handle_error InvalidExperimentError
117
- nil
118
+ return RUNNING_EXPERIMENT_STATUS.include?(experiment['status'])
118
119
  end
119
120
 
120
- def get_experiment_id(experiment_key)
121
+ def get_experiment_from_key(experiment_key)
121
122
  # Retrieves experiment ID for a given key
122
123
  #
123
124
  # experiment_key - String key representing the experiment
124
125
  #
125
- # Returns String ID
126
+ # Returns Experiment
126
127
 
127
128
  experiment = @experiment_key_map[experiment_key]
128
- return experiment['id'] if experiment
129
+ return experiment if experiment
129
130
  @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
130
131
  @error_handler.handle_error InvalidExperimentError
131
132
  nil
@@ -159,34 +160,6 @@ module Optimizely
159
160
  []
160
161
  end
161
162
 
162
- def get_traffic_allocation(experiment_key)
163
- # Retrieves traffic allocation for a given experiment Key
164
- #
165
- # experiment_key - String Key representing the experiment
166
- #
167
- # Returns traffic allocation for the experiment or nil
168
-
169
- experiment = @experiment_key_map[experiment_key]
170
- return experiment['trafficAllocation'] if experiment
171
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
172
- @error_handler.handle_error InvalidExperimentError
173
- nil
174
- end
175
-
176
- def get_audience_ids_for_experiment(experiment_key)
177
- # Get audience IDs for the experiment
178
- #
179
- # experiment_key - Experiment key for which audience IDs are to be determined
180
- #
181
- # Returns audience IDs corresponding to the experiment.
182
-
183
- experiment = @experiment_key_map[experiment_key]
184
- return experiment['audienceIds'] if experiment
185
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
186
- @error_handler.handle_error InvalidExperimentError
187
- nil
188
- end
189
-
190
163
  def get_audience_conditions_from_id(audience_id)
191
164
  # Get audience conditions for the provided audience ID
192
165
  #
@@ -223,6 +196,28 @@ module Optimizely
223
196
  nil
224
197
  end
225
198
 
199
+ def get_variation_from_id(experiment_key, variation_id)
200
+ # Get variation given experiment key and variation ID
201
+ #
202
+ # experiment_key - Key representing parent experiment of variation
203
+ # variation_id - ID of the variation
204
+ #
205
+ # Returns the variation or nil if not found
206
+
207
+ variation_id_map = @variation_id_map[experiment_key]
208
+ if variation_id_map
209
+ variation = variation_id_map[variation_id]
210
+ return variation if variation
211
+ @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
212
+ @error_handler.handle_error InvalidVariationError
213
+ return nil
214
+ end
215
+
216
+ @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
217
+ @error_handler.handle_error InvalidExperimentError
218
+ nil
219
+ end
220
+
226
221
  def get_variation_id_from_key(experiment_key, variation_key)
227
222
  # Get variation ID given experiment key and variation key
228
223
  #
@@ -245,12 +240,12 @@ module Optimizely
245
240
  nil
246
241
  end
247
242
 
248
- def get_forced_variations(experiment_key)
249
- # Retrieves forced variations for a given experiment Key
243
+ def get_whitelisted_variations(experiment_key)
244
+ # Retrieves whitelisted variations for a given experiment Key
250
245
  #
251
246
  # experiment_key - String Key representing the experiment
252
247
  #
253
- # Returns forced variations for the experiment or nil
248
+ # Returns whitelisted variations for the experiment or nil
254
249
 
255
250
  experiment = @experiment_key_map[experiment_key]
256
251
  return experiment['forcedVariations'] if experiment
@@ -258,11 +253,98 @@ module Optimizely
258
253
  @error_handler.handle_error InvalidExperimentError
259
254
  end
260
255
 
261
- def get_experiment_group_id(experiment_key)
262
- experiment = @experiment_key_map[experiment_key]
263
- return experiment['groupId'] if experiment
264
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
265
- @error_handler.handle_error InvalidExperimentError
256
+ def get_forced_variation(experiment_key, user_id)
257
+ # Gets the forced variation for the given user and experiment.
258
+ #
259
+ # experiment_key - String Key for experiment.
260
+ # user_id - String ID for user
261
+ #
262
+ # Returns Variation The variation which the given user and experiment should be forced into.
263
+
264
+ # check for nil and empty string user ID
265
+ if user_id.nil? or user_id.empty?
266
+ @logger.log(Logger::DEBUG, "User ID is invalid")
267
+ return nil
268
+ end
269
+
270
+ unless @forced_variation_map.has_key? (user_id)
271
+ @logger.log(Logger::DEBUG, "User '#{user_id}' is not in the forced variation map.")
272
+ return nil
273
+ end
274
+
275
+ experimentToVariationMap = @forced_variation_map[user_id]
276
+ experiment = get_experiment_from_key(experiment_key)
277
+ experiment_id = experiment["id"] if experiment
278
+ # check for nil and empty string experiment ID
279
+ if experiment_id.nil? or experiment_id.empty?
280
+ # this case is logged in get_experiment_from_key
281
+ return nil
282
+ end
283
+
284
+ unless experimentToVariationMap.has_key? (experiment_id)
285
+ @logger.log(Logger::DEBUG, "No experiment '#{experiment_key}' mapped to user '#{user_id}' in the forced variation map.")
286
+ return nil
287
+ end
288
+
289
+ variation_id = experimentToVariationMap[experiment_id]
290
+ variation_key = ""
291
+ variation = get_variation_from_id(experiment_key,variation_id)
292
+ variation_key = variation["key"] if variation
293
+
294
+ # check if the variation exists in the datafile
295
+ if variation_key.empty?
296
+ # this case is logged in get_variation_from_id
297
+ return nil
298
+ end
299
+
300
+ @logger.log(Logger::DEBUG, "Variation '#{variation_key}' is mapped to experiment '#{experiment_key}' and user '#{user_id}' in the forced variation map")
301
+
302
+ variation
303
+ end
304
+
305
+ def set_forced_variation(experiment_key, user_id, variation_key)
306
+ # Sets a Hash of user IDs to a Hash of experiments to forced variations.
307
+ #
308
+ # experiment_key - String Key for experiment.
309
+ # user_id - String ID for user.
310
+ # variation_key - String Key for variation. If null, then clear the existing experiment-to-variation mapping.
311
+ #
312
+ # Returns a boolean value that indicates if the set completed successfully.
313
+
314
+ # check for null and empty string user ID
315
+ if user_id.nil? or user_id.empty?
316
+ @logger.log(Logger::DEBUG, "User ID is invalid")
317
+ return false
318
+ end
319
+
320
+ experiment = get_experiment_from_key(experiment_key)
321
+ experiment_id = experiment["id"] if experiment
322
+ # check if the experiment exists in the datafile
323
+ if experiment_id.nil? or experiment_id.empty?
324
+ return false
325
+ end
326
+
327
+ # clear the forced variation if the variation key is null
328
+ if variation_key.nil? or variation_key.empty?
329
+ @forced_variation_map[user_id].delete(experiment_id) if @forced_variation_map.has_key? (user_id)
330
+ @logger.log(Logger::DEBUG, "Variation mapped to experiment '#{experiment_key}' has been removed for user '#{user_id}'.")
331
+ return true
332
+ end
333
+
334
+ variation_id = get_variation_id_from_key(experiment_key, variation_key)
335
+
336
+ # check if the variation exists in the datafile
337
+ unless variation_id
338
+ # this case is logged in get_variation_id_from_key
339
+ return false
340
+ end
341
+
342
+ unless @forced_variation_map.has_key? user_id
343
+ @forced_variation_map[user_id] = {}
344
+ end
345
+ @forced_variation_map[user_id][experiment_id] = variation_id
346
+ @logger.log(Logger::DEBUG, "Set variation '#{variation_id}' for experiment '#{experiment_id}' and user '#{user_id}' in the forced variation map.")
347
+ return true
266
348
  end
267
349
 
268
350
  def get_attribute_id(attribute_key)
@@ -273,19 +355,6 @@ module Optimizely
273
355
  nil
274
356
  end
275
357
 
276
- def user_in_forced_variation?(experiment_key, user_id)
277
- # Determines if a given user is in a forced variation
278
- #
279
- # experiment_key - String experiment key
280
- # user_id - String user ID
281
- #
282
- # Returns true if user is in a forced variation
283
-
284
- forced_variations = get_forced_variations(experiment_key)
285
- return forced_variations.include?(user_id) if forced_variations
286
- false
287
- end
288
-
289
358
  def parsing_succeeded?
290
359
  # Helper method to determine if parsing the datafile was successful.
291
360
  #
@@ -309,11 +378,8 @@ module Optimizely
309
378
  return true if variation
310
379
  @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
311
380
  @error_handler.handle_error InvalidVariationError
312
- return false
313
381
  end
314
382
 
315
- @logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
316
- @error_handler.handle_error InvalidExperimentError
317
383
  false
318
384
  end
319
385
 
@@ -14,5 +14,8 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  module Optimizely
17
- VERSION = '1.3.0'.freeze
17
+
18
+ CLIENT_ENGINE = 'ruby-sdk'.freeze
19
+ VERSION = '1.4.0'.freeze
20
+
18
21
  end
metadata CHANGED
@@ -1,16 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optimizely-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - Andrew Delikat
8
- - Haley Bash
9
7
  - Optimizely
10
8
  autorequire:
11
9
  bindir: bin
12
10
  cert_chain: []
13
- date: 2017-05-24 00:00:00.000000000 Z
11
+ date: 2017-10-04 00:00:00.000000000 Z
14
12
  dependencies:
15
13
  - !ruby/object:Gem::Dependency
16
14
  name: bundler
@@ -155,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
153
  version: '0'
156
154
  requirements: []
157
155
  rubyforge_project:
158
- rubygems_version: 2.6.10
156
+ rubygems_version: 2.6.13
159
157
  signing_key:
160
158
  specification_version: 4
161
159
  summary: Ruby SDK for Optimizely's testing framework