optimizely-sdk 1.3.0 → 1.4.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: 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