vwo-sdk 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2555f32eb2068e63dea737bdf3937708a5ef3e1e
4
+ data.tar.gz: dd662a2ac80ba5dc6892375c15b3d13e2e0ed3a4
5
+ SHA512:
6
+ metadata.gz: 2dae59f19f9342856a343e5be3359b487069db19f4503398b577eed09c9e1e6ff98c37923c53b1fca91b5da5d66ad2d7a2414c582e0b02b3cb78f529a80a1568
7
+ data.tar.gz: 1d1778ccde23e98efed73462dca543c84c12ccb2a7cf4cb18e129f63514268742afc4a0d7a394b16476dadef08eb2d0bbd47b312a13afce23f1c66c23f5a3a99
data/lib/vwo.rb ADDED
@@ -0,0 +1,348 @@
1
+ # Copyright 2019 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # frozen_string_literal: true
16
+
17
+ require_relative 'vwo/services/settings_file_manager'
18
+ require_relative 'vwo/services/event_dispatcher'
19
+ require_relative 'vwo/services/settings_file_processor'
20
+ require_relative 'vwo/logger'
21
+ require_relative 'vwo/enums'
22
+ require_relative 'vwo/utils/campaign'
23
+ require_relative 'vwo/utils/impression'
24
+ require_relative 'vwo/constants'
25
+ require_relative 'vwo/core/variation_decider'
26
+
27
+
28
+ # VWO main file
29
+ class VWO
30
+ attr_accessor :is_instance_valid
31
+
32
+ include Enums
33
+ include Utils::Validations
34
+ include Utils::Campaign
35
+ include Utils::Impression
36
+ include CONSTANTS
37
+
38
+ FILE = FileNameEnum::VWO
39
+
40
+ # Initializes and expose APIs
41
+ #
42
+ # @param[Numeric|String] :account_id Account Id in VWO
43
+ # @param[String] :sdk_key Unique sdk key for user
44
+ # @param[Object] :logger Optional - should have log method defined
45
+ # @param[Object] :user_storage Optional - to store and manage user data mapping
46
+ # @param[Boolean] :is_development_mode To specify whether the request
47
+ # to our server should be sent or not.
48
+ # @param[String] :settings_file Settings-file data
49
+
50
+ def initialize(
51
+ account_id,
52
+ sdk_key,
53
+ logger = nil,
54
+ user_storage = nil,
55
+ is_development_mode = false,
56
+ settings_file = nil
57
+ )
58
+ @account_id = account_id
59
+ @sdk_key = sdk_key
60
+ @user_storage = user_storage
61
+ @is_development_mode = is_development_mode
62
+ @logger = VWO::Logger.get_instance(logger)
63
+
64
+ unless valid_settings_file?(get_settings(settings_file))
65
+ @logger.log(
66
+ LogLevelEnum::ERROR,
67
+ format(LogMessageEnum::ErrorMessages::SETTINGS_FILE_CORRUPTED, file: FILE)
68
+ )
69
+ @is_instance_valid = false
70
+ return
71
+ end
72
+ @is_instance_valid = true
73
+ @config = VWO::Services::SettingsFileProcessor.new(get_settings)
74
+
75
+ @logger.log(
76
+ LogLevelEnum::DEBUG,
77
+ format(LogMessageEnum::DebugMessages::VALID_CONFIGURATION, file: FILE)
78
+ )
79
+
80
+ # Process the settings file
81
+ @config.process_settings_file
82
+ @settings_file = @config.get_settings_file
83
+
84
+ # Assign VariationDecider to VWO
85
+ @variation_decider = VWO::Core::VariationDecider.new(@settings_file, user_storage)
86
+
87
+ if is_development_mode
88
+ @logger.log(
89
+ LogLevelEnum::DEBUG,
90
+ format(LogMessageEnum::DebugMessages::SET_DEVELOPMENT_MODE, file: FILE)
91
+ )
92
+ end
93
+ # Assign event dispatcher
94
+ @event_dispatcher = VWO::Services::EventDispatcher.new(is_development_mode)
95
+
96
+ # Successfully initialized VWO SDK
97
+ @logger.log(
98
+ LogLevelEnum::DEBUG,
99
+ format(LogMessageEnum::DebugMessages::SDK_INITIALIZED, file: FILE)
100
+ )
101
+ end
102
+
103
+ # Public Methods
104
+
105
+ # VWO get_settings method to get settings for a particular account_id
106
+ def get_settings(settings_file = nil)
107
+ @settings ||=
108
+ settings_file || VWO::Services::SettingsFileManager.new(@account_id, @sdk_key).get_settings_file
109
+ @settings
110
+ end
111
+
112
+ # This API method: Gets the variation assigned for the user
113
+ # For the campaign and send the metrics to VWO server
114
+ #
115
+ # 1. Validates the arguments being passed
116
+ # 2. Checks if user is eligible to get bucketed into the campaign,
117
+ # 3. Assigns the deterministic variation to the user(based on userId),
118
+ # If user becomes part of campaign
119
+ # If UserStorage is used, it will look into it for the
120
+ # Variation and if found, no further processing is done
121
+ # 4. Sends an impression call to VWO server to track user
122
+ #
123
+ # @param[String] :campaign_key Unique campaign key
124
+ # @param[String] :user_id ID assigned to a user
125
+ # @return[String|None] If variation is assigned then variation-name
126
+ # otherwise null in case of user not becoming part
127
+
128
+ def activate(campaign_key, user_id)
129
+ # Validate input parameters
130
+ unless valid_string?(campaign_key) && valid_string?(user_id)
131
+ @logger.log(
132
+ LogLevelEnum::ERROR,
133
+ format(LogMessageEnum::ErrorMessages::ACTIVATE_API_MISSING_PARAMS, file: FILE)
134
+ )
135
+ return
136
+ end
137
+
138
+ unless @is_instance_valid
139
+ @logger.log(
140
+ LogLevelEnum::ERROR,
141
+ format(LogMessageEnum::ErrorMessages::ACTIVATE_API_CONFIG_CORRUPTED, file: FILE)
142
+ )
143
+ return
144
+ end
145
+
146
+ # Get the campaign settings
147
+ campaign = get_campaign(@settings_file, campaign_key)
148
+
149
+ # Validate campaign
150
+ unless campaign && campaign['status'] == STATUS_RUNNING
151
+ # Log Campaign as invalid
152
+ @logger.log(
153
+ LogLevelEnum::ERROR,
154
+ format(LogMessageEnum::ErrorMessages::CAMPAIGN_NOT_RUNNING, file: FILE, campaign_key: campaign_key, api: 'activate')
155
+ )
156
+ return
157
+ end
158
+
159
+ # Once the matching RUNNING campaign is found, assign the
160
+ # deterministic variation to the user_id provided
161
+ variation_id, variation_name = @variation_decider.get_variation(
162
+ user_id,
163
+ campaign
164
+ )
165
+
166
+ # Check if variation_name has been assigned
167
+ unless valid_value?(variation_name)
168
+ @logger.log(
169
+ LogLevelEnum::INFO,
170
+ format(LogMessageEnum::InfoMessages::INVALID_VARIATION_KEY, file: FILE, user_id: user_id, campaign_key: campaign_key)
171
+ )
172
+ return
173
+ end
174
+
175
+ # Variation found, dispatch it to server
176
+ impression = create_impression(
177
+ @settings_file,
178
+ campaign['id'],
179
+ variation_id,
180
+ user_id
181
+ )
182
+ @event_dispatcher.dispatch(impression)
183
+ variation_name
184
+ end
185
+
186
+ # This API method: Gets the variation name assigned for the
187
+ # user for the campaign
188
+ #
189
+ # 1. Validates the arguments being passed
190
+ # 2. Checks if user is eligible to get bucketed into the campaign,
191
+ # 3. Assigns the deterministic variation to the user(based on user_id),
192
+ # If user becomes part of campaign
193
+ # If UserStorage is used, it will look into it for the
194
+ # variation and if found, no further processing is done
195
+ #
196
+ # @param[String] :campaign_key Unique campaign key
197
+ # @param[String] :user_id ID assigned to a user
198
+ #
199
+ # @@return[String|Nil] If variation is assigned then variation-name
200
+ # Otherwise null in case of user not becoming part
201
+ #
202
+ def get_variation_name(campaign_key, user_id)
203
+ # Check for valid arguments
204
+ unless valid_string?(campaign_key) && valid_string?(user_id)
205
+ # log invalid params
206
+ @logger.log(
207
+ LogLevelEnum::ERROR,
208
+ format(LogMessageEnum::ErrorMessages::GET_VARIATION_NAME_API_MISSING_PARAMS, file: FILE)
209
+ )
210
+ return
211
+ end
212
+
213
+ unless @is_instance_valid
214
+ @logger.log(
215
+ LogLevelEnum::ERROR,
216
+ format(LogMessageEnum::ErrorMessages::ACTIVATE_API_CONFIG_CORRUPTED, file: FILE)
217
+ )
218
+ return
219
+ end
220
+
221
+ # Get the campaign settings
222
+ campaign = get_campaign(@settings_file, campaign_key)
223
+
224
+ # Validate campaign
225
+ if campaign.nil? || campaign['status'] != STATUS_RUNNING
226
+ @logger.log(
227
+ LogLevelEnum::ERROR,
228
+ format(LogMessageEnum::ErrorMessages::CAMPAIGN_NOT_RUNNING, file: FILE, campaign_key: campaign_key, api: 'get_variation')
229
+ )
230
+ return
231
+ end
232
+
233
+ _variation_id, variation_name = @variation_decider.get_variation(
234
+ user_id,
235
+ campaign
236
+ )
237
+
238
+ # Check if variation_name has been assigned
239
+ unless valid_value?(variation_name)
240
+ # log invalid variation key
241
+ @logger.log(
242
+ LogLevelEnum::INFO,
243
+ format(LogMessageEnum::InfoMessages::INVALID_VARIATION_KEY, file: FILE, user_id: user_id, campaign_key: campaign_key)
244
+ )
245
+ return
246
+ end
247
+
248
+ variation_name
249
+ end
250
+
251
+ # This API method: Marks the conversion of the campaign
252
+ # for a particular goal
253
+ # 1. validates the arguments being passed
254
+ # 2. Checks if user is eligible to get bucketed into the campaign,
255
+ # 3. Gets the assigned deterministic variation to the
256
+ # user(based on user_d), if user becomes part of campaign
257
+ # 4. Sends an impression call to VWO server to track goal data
258
+ #
259
+ # @param[String] :campaign_key Unique campaign key
260
+ # @param[String] :user_id ID assigned to a user
261
+ # @param[String] :goal_identifier Unique campaign's goal identifier
262
+ # @param[Numeric|String] :revenue_value Revenue value for revenue-type goal
263
+ #
264
+ def track(campaign_key, user_id, goal_identifier, *args)
265
+ if args[0].is_a?(Hash)
266
+ revenue_value = args[0]['revenue_value']
267
+ elsif args.is_a?(Array)
268
+ revenue_value = args[0]
269
+ end
270
+
271
+ # Check for valid args
272
+ unless valid_string?(campaign_key) && valid_string?(user_id) && valid_string?(goal_identifier)
273
+ # log invalid params
274
+ @logger.log(
275
+ LogLevelEnum::ERROR,
276
+ format(LogMessageEnum::ErrorMessages::TRACK_API_MISSING_PARAMS, file: FILE)
277
+ )
278
+ return false
279
+ end
280
+
281
+ unless @is_instance_valid
282
+ @logger.log(
283
+ LogLevelEnum::ERROR,
284
+ format(LogMessageEnum::ErrorMessages::ACTIVATE_API_CONFIG_CORRUPTED, file: FILE)
285
+ )
286
+ return false
287
+ end
288
+
289
+ # Get the campaign settings
290
+ campaign = get_campaign(@settings_file, campaign_key)
291
+
292
+ # Validate campaign
293
+ if campaign.nil? || campaign['status'] != STATUS_RUNNING
294
+ # log error
295
+ @logger.log(
296
+ LogLevelEnum::ERROR,
297
+ format(LogMessageEnum::ErrorMessages::CAMPAIGN_NOT_RUNNING, file: FILE, campaign_key: campaign_key, api: 'track')
298
+ )
299
+ return false
300
+ end
301
+
302
+ campaign_id = campaign['id']
303
+ variation_id, variation_name = @variation_decider.get_variation_allotted(user_id, campaign)
304
+
305
+ if variation_name
306
+ goal = get_campaign_goal(@settings_file, campaign['key'], goal_identifier)
307
+
308
+ if goal.nil?
309
+ @logger.log(
310
+ LogLevelEnum::ERROR,
311
+ format(
312
+ LogMessageEnum::ErrorMessages::TRACK_API_GOAL_NOT_FOUND,
313
+ file: FILE, goal_identifier: goal_identifier,
314
+ user_id: user_id,
315
+ campaign_key: campaign_key
316
+ )
317
+ )
318
+ return false
319
+ elsif goal['type'] == GOALTYPES::REVENUE && !valid_value?(revenue_value)
320
+ @logger.log(
321
+ LogLevelEnum::ERROR,
322
+ format(
323
+ LogMessageEnum::ErrorMessages::TRACK_API_REVENUE_NOT_PASSED_FOR_REVENUE_GOAL,
324
+ file: FILE,
325
+ user_id: user_id,
326
+ goal_identifier: goal_identifier,
327
+ campaign_key: campaign_key
328
+ )
329
+ )
330
+ return false
331
+ end
332
+
333
+ revenue_value = nil if goal['type'] == GOALTYPES::CUSTOM
334
+
335
+ impression = create_impression(
336
+ @settings_file,
337
+ campaign_id,
338
+ variation_id,
339
+ user_id,
340
+ goal['id'],
341
+ revenue_value
342
+ )
343
+ @event_dispatcher.dispatch(impression)
344
+ return true
345
+ end
346
+ false
347
+ end
348
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright 2019 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # frozen_string_literal: true
16
+
17
+ class VWO
18
+ module CONSTANTS
19
+ API_VERSION = 1
20
+ PLATFORM = 'server'
21
+ SEED_VALUE = 1
22
+ MAX_TRAFFIC_PERCENT = 100
23
+ MAX_TRAFFIC_VALUE = 10_000
24
+ STATUS_RUNNING = 'RUNNING'
25
+ LIBRARY_PATH = File.expand_path('../..', __FILE__)
26
+ HTTP_PROTOCOL = 'http://'
27
+ HTTPS_PROTOCOL = 'https://'
28
+ URL_NAMESPACE = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'
29
+ SDK_VERSION = '1.3.0'
30
+ SDK_NAME = 'ruby'
31
+
32
+ module ENDPOINTS
33
+ BASE_URL = 'dev.visualwebsiteoptimizer.com'
34
+ ACCOUNT_SETTINGS = '/server-side/settings'
35
+ TRACK_USER = '/server-side/track-user'
36
+ TRACK_GOAL = '/server-side/track-goal'
37
+ end
38
+
39
+ module EVENTS
40
+ TRACK_USER = 'track-user'
41
+ TRACK_GOAL = 'track-goal'
42
+ end
43
+
44
+ module DATATYPE
45
+ NUMBER = 'number'
46
+ STRING = 'string'
47
+ FUNCTION = 'function'
48
+ BOOLEAN = 'boolean'
49
+ end
50
+
51
+ module APIMETHODS
52
+ CREATE_INSTANCE = 'CREATE_INSTANCE'
53
+ ACTIVATE = 'ACTIVATE'
54
+ GET_VARIATION = 'GET_VARIATION'
55
+ TRACK = 'TRACK'
56
+ end
57
+
58
+ module GOALTYPES
59
+ REVENUE = 'REVENUE_TRACKING'
60
+ CUSTOM = 'CUSTOM_GOAL'
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,177 @@
1
+ # Copyright 2019 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # frozen_string_literal: true
16
+
17
+ require 'murmurhash3'
18
+ require_relative '../logger'
19
+ require_relative '../enums'
20
+ require_relative '../utils/validations'
21
+ require_relative '../constants'
22
+
23
+ class VWO
24
+ module Core
25
+ class Bucketer
26
+ include VWO::Enums
27
+ include VWO::CONSTANTS
28
+ include VWO::Utils::Validations
29
+
30
+ # Took reference from StackOverflow(https://stackoverflow.com/) to:
31
+ # convert signed to unsigned integer in python from StackOverflow
32
+ # Author - Duncan (https://stackoverflow.com/users/107660/duncan)
33
+ # Source - https://stackoverflow.com/a/20766900/2494535
34
+ U_MAX_32_BIT = 0xFFFFFFFF
35
+ MAX_HASH_VALUE = 2**32
36
+ FILE = FileNameEnum::Bucketer
37
+
38
+ def initialize
39
+ @logger = VWO::Logger.get_instance
40
+ end
41
+
42
+ # Calculate if this user should become part of the campaign or not
43
+ # @param[String] :user_id The unique ID assigned to a user
44
+ # @param[Dict] :campaign For getting traffic allotted to the campaign
45
+ # @return[Boolean] If User is a part of Campaign or not
46
+ #
47
+ def user_part_of_campaign?(user_id, campaign)
48
+ unless valid_value?(user_id)
49
+ @logger.log(
50
+ LogLevelEnum::ERROR,
51
+ format(LogMessageEnum::ErrorMessages::INVALID_USER_ID, file: FILE, user_id: user_id, method: 'is_user_part_of_campaign')
52
+ )
53
+ return false
54
+ end
55
+
56
+ if campaign.nil?
57
+ @logger.log(
58
+ LogLevelEnum::ERROR,
59
+ format(LogMessageEnum::ErrorMessages::INVALID_CAMPAIGN, file: FILE, method: 'is_user_part_of_campaign')
60
+ )
61
+ return false
62
+ end
63
+
64
+ traffic_allocation = campaign['percentTraffic']
65
+
66
+ value_assigned_to_user = get_bucket_value_for_user(user_id)
67
+ is_user_part = (value_assigned_to_user != 0) && value_assigned_to_user <= traffic_allocation
68
+ @logger.log(
69
+ LogLevelEnum::INFO,
70
+ format(LogMessageEnum::InfoMessages::USER_ELIGIBILITY_FOR_CAMPAIGN, file: FILE, user_id: user_id, is_user_part: is_user_part)
71
+ )
72
+ is_user_part
73
+ end
74
+
75
+ # Validates the User ID and
76
+ # Generates Variation into which the User is bucketed to
77
+ #
78
+ # @param[String] :user_id The unique ID assigned to User
79
+ # @param[Hash] :campaign The Campaign of which User is a part of
80
+ #
81
+ # @return[Hash|nil} Variation data into which user is bucketed to
82
+ # or nil if not
83
+ def bucket_user_to_variation(user_id, campaign)
84
+ unless valid_value?(user_id)
85
+ @logger.log(
86
+ LogLevelEnum::ERROR,
87
+ format(LogMessageEnum::ErrorMessages::INVALID_USER_ID, file: FILE, user_id: user_id, method: 'bucket_user_to_variation')
88
+ )
89
+ return
90
+ end
91
+
92
+ unless campaign
93
+ @logger.log(
94
+ LogLevelEnum::ERROR,
95
+ format(LogMessageEnum::ErrorMessages::INVALID_CAMPAIGN, file: FILE, method: 'is_user_part_of_campaign')
96
+ )
97
+ return
98
+ end
99
+
100
+ hash_value = MurmurHash3::V32.str_hash(user_id, SEED_VALUE) & U_MAX_32_BIT
101
+ normalize = MAX_TRAFFIC_VALUE / campaign['percentTraffic']
102
+ multiplier = normalize / 100
103
+ bucket_value = generate_bucket_value(
104
+ hash_value,
105
+ MAX_TRAFFIC_VALUE,
106
+ multiplier
107
+ )
108
+
109
+ @logger.log(
110
+ LogLevelEnum::DEBUG,
111
+ format(
112
+ LogMessageEnum::DebugMessages::VARIATION_HASH_BUCKET_VALUE,
113
+ file: FILE,
114
+ user_id: user_id,
115
+ campaign_key: campaign['key'],
116
+ percent_traffic: campaign['percentTraffic'],
117
+ bucket_value: bucket_value,
118
+ hash_value: hash_value
119
+ )
120
+ )
121
+ get_variation(campaign, bucket_value)
122
+ end
123
+
124
+ private
125
+
126
+ # Returns the Variation by checking the Start and End
127
+ # Bucket Allocations of each Variation
128
+ #
129
+ # @param[Hash] :campaign Which contains the variations
130
+ # @param[Integer] :bucket_value The bucket Value of the user
131
+ # @return[Hash|nil] Variation data allotted to the user or None if not
132
+ #
133
+ def get_variation(campaign, bucket_value)
134
+ campaign['variations'].find do |variation|
135
+ (variation['start_variation_allocation']..variation['end_variation_allocation']).cover?(bucket_value)
136
+ end
137
+ end
138
+
139
+ # Validates the User ID and generates Bucket Value of the
140
+ # User by hashing the userId by murmurHash and scaling it down.
141
+ #
142
+ # @param[String] :user_id The unique ID assigned to User
143
+ # @return[Integer] The bucket Value allotted to User
144
+ # (between 1 to $this->$MAX_TRAFFIC_PERCENT)
145
+ def get_bucket_value_for_user(user_id)
146
+ hash_value = MurmurHash3::V32.str_hash(user_id, SEED_VALUE) & U_MAX_32_BIT
147
+ bucket_value = generate_bucket_value(hash_value, MAX_TRAFFIC_PERCENT)
148
+
149
+ @logger.log(
150
+ LogLevelEnum::DEBUG,
151
+ format(
152
+ LogMessageEnum::DebugMessages::USER_HASH_BUCKET_VALUE,
153
+ file: FILE,
154
+ hash_value: hash_value,
155
+ bucket_value: bucket_value,
156
+ user_id: user_id
157
+ )
158
+ )
159
+ bucket_value
160
+ end
161
+
162
+ # Generates Bucket Value of the User by hashing the User ID by murmurHash
163
+ # And scaling it down.
164
+ #
165
+ # @param[Integer] :hash_value HashValue generated after hashing
166
+ # @param[Integer] :max_value The value up-to which hashValue needs to be scaled
167
+ # @param[Integer] :multiplier
168
+ # @return[Integer] Bucket Value of the User
169
+ #
170
+ def generate_bucket_value(hash_value, max_value, multiplier = 1)
171
+ ratio = hash_value.to_f / MAX_HASH_VALUE
172
+ multiplied_value = (max_value * ratio + 1) * multiplier
173
+ multiplied_value.to_i
174
+ end
175
+ end
176
+ end
177
+ end