vwo-ruby-sdk 1.0.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.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative '../custom_logger'
5
+ require_relative 'enums'
6
+ require_relative 'constants'
7
+
8
+ # Utility module for manipulating VWO campaigns
9
+ class VWO
10
+ module Common
11
+ module UUIDUtils
12
+ include VWO::Common::Enums
13
+ include VWO::Common::CONSTANTS
14
+
15
+ def self.parse(obj)
16
+ str = obj.to_s.sub(/\Aurn:uuid:/, '')
17
+ str.gsub!(/[^0-9A-Fa-f]/, '')
18
+ [str[0..31]].pack 'H*'
19
+ end
20
+
21
+ def self.uuid_v5(uuid_namespace, name)
22
+ uuid_namespace = parse(uuid_namespace)
23
+ hash_class = ::Digest::SHA1
24
+ version = 5
25
+
26
+ hash = hash_class.new
27
+ hash.update(uuid_namespace)
28
+ hash.update(name)
29
+
30
+ ary = hash.digest.unpack('NnnnnN')
31
+ ary[2] = (ary[2] & 0x0FFF) | (version << 12)
32
+ ary[3] = (ary[3] & 0x3FFF) | 0x8000
33
+ # rubocop:disable Lint/FormatString
34
+ '%08x-%04x-%04x-%04x-%04x%08x' % ary
35
+ # rubocop:enable Lint/FormatString
36
+ end
37
+
38
+ VWO_NAMESPACE = uuid_v5(URL_NAMESPACE, 'https://vwo.com')
39
+
40
+ # Generates desired UUID
41
+ #
42
+ # @param[Integer|String] :user_id User identifier
43
+ # @param[Integer|String] :account_id Account identifier
44
+ #
45
+ # @return[Integer] Desired UUID
46
+ #
47
+ def generator_for(user_id, account_id)
48
+ user_id = user_id.to_s
49
+ account_id = account_id.to_s
50
+ user_id_namespace = generate(VWO_NAMESPACE, account_id)
51
+ uuid_for_account_user_id = generate(user_id_namespace, user_id)
52
+
53
+ desired_uuid = uuid_for_account_user_id.delete('-').upcase
54
+
55
+ VWO::CustomLogger.get_instance.log(
56
+ LogLevelEnum::DEBUG,
57
+ format(
58
+ LogMessageEnum::DebugMessages::UUID_FOR_USER,
59
+ file: FileNameEnum::UuidUtil,
60
+ user_id: user_id,
61
+ account_id: account_id,
62
+ desired_uuid: desired_uuid
63
+ )
64
+ )
65
+ desired_uuid
66
+ end
67
+
68
+ # Generated uuid from namespace and name, uses uuid5
69
+ #
70
+ # @param[String] :namespace Namespace
71
+ # @param[String) :name Name
72
+ #
73
+ # @return[String|Nil] Uuid, nil if any of the arguments is empty
74
+ def generate(namespace, name)
75
+ VWO::Common::UUIDUtils.uuid_v5(namespace, name) if name && namespace
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'json-schema'
5
+ require_relative 'schemas/settings_file'
6
+
7
+ class VWO
8
+ module Common
9
+ module Validations
10
+ UTILITIES = {
11
+ 'logger' => ['log'],
12
+ 'event_dispatcher' => ['dispatch'],
13
+ 'user_profile_service' => %w[lookup save]
14
+ }.freeze
15
+ # Validates the project settings_file
16
+ # @param [Hash]: JSON object received from DACDN server or somewhere else,
17
+ # must be json string representation.
18
+ # @return [Boolean]
19
+ def valid_settings_file?(settings_file)
20
+ settings_file = JSON.parse(settings_file)
21
+ JSON::Validator.validate!(VWO::Common::Schema::SETTINGS_FILE_SCHEMA, settings_file)
22
+ rescue StandardError
23
+ false
24
+ end
25
+
26
+ # @return [Boolean]
27
+ def valid_value?(val)
28
+ !val.nil?
29
+ end
30
+
31
+ # @return [Boolean]
32
+ def valid_number?(val)
33
+ val.is_a?(Numeric)
34
+ end
35
+
36
+ # @return [Boolean]
37
+ def valid_string?(val)
38
+ val.is_a?(String)
39
+ end
40
+
41
+ # @return [Boolean]
42
+ def valid_hash?(val)
43
+ val.is_a?(Hash)
44
+ end
45
+
46
+ # @param [Class] - User defined class instance
47
+ # @param [utility_name] - Name of the utility
48
+ # @return [Boolean]
49
+ def valid_utility?(utility, utility_name)
50
+ utility_attributes = UTILITIES[utility_name]
51
+ return false if utility_attributes.nil?
52
+
53
+ utility_attributes.each do |attr|
54
+ return false unless method?(utility, attr)
55
+ end
56
+ true
57
+ end
58
+
59
+ private
60
+
61
+ # @return [Boolean]
62
+ def method?(object, method)
63
+ object.methods.include?(method.to_sym)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ class VWO
6
+ class CustomLogger
7
+ @logger = nil
8
+ @logger_instance = nil
9
+
10
+ def self.get_instance(logger_instance = nil)
11
+ @@logger ||= VWO::CustomLogger.new(logger_instance)
12
+ end
13
+
14
+ def initialize(logger_instance)
15
+ @@logger_instance = logger_instance || Logger.new(STDOUT)
16
+ end
17
+
18
+ # Override this method to handle logs in a custom manner
19
+ def log(level, message)
20
+ @@logger_instance.log(level, message)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_logger'
4
+ require_relative 'common/enums'
5
+ require_relative 'common/campaign_utils'
6
+ require_relative 'common/validations'
7
+ require_relative 'bucketing_service'
8
+
9
+ # Class encapsulating all decision related capabilities.
10
+ class VWO
11
+ class DecisionService
12
+ include VWO::Common::Enums
13
+ include VWO::Common::CampaignUtils
14
+ include Common::Validations
15
+
16
+ # Initializes DecisionService with settings_file, UserProfileService and logger.
17
+ # @param[Hash] - Settings file of the project.
18
+ # @param[Class] - Class instance having the capability of
19
+ # lookup and save.
20
+ def initialize(settings_file, user_profile_service = nil)
21
+ @logger = CustomLogger.get_instance
22
+ @user_profile_service = user_profile_service
23
+ # Check if user_profile_service provided is valid or not
24
+ @user_profile_service = user_profile_service if valid_utility?(user_profile_service, 'user_profile_service')
25
+ @bucketer = VWO::BucketingService.new
26
+ @settings_file = settings_file
27
+ end
28
+
29
+ # Returns variation for the user for required campaign
30
+ # First lookup in the UPS, if user_profile is found,
31
+ # return from there
32
+ # Else, calculates the variation with helper method
33
+ #
34
+ #
35
+ # @param[String] :user_id The unique ID assigned to User
36
+ # @param[Hash] :campaign Campaign in which user is participating
37
+ # @param[String] :campaign_test_key The unique ID of the campaign passed
38
+ # @return[String,String] ({variation_id, variation_name}|Nil): Tuple of
39
+ # variation_id and variation_name if variation allotted, else nil
40
+
41
+ def get(user_id, campaign, campaign_test_key)
42
+ campaign_bucket_map = resolve_campaign_bucket_map(user_id)
43
+ variation = get_stored_variation(user_id, campaign_test_key, campaign_bucket_map) if valid_hash?(campaign_bucket_map)
44
+
45
+ if variation
46
+ @logger.log(
47
+ LogLevelEnum::INFO,
48
+ format(
49
+ LogMessageEnum::InfoMessages::GOT_STORED_VARIATION,
50
+ file: FILE,
51
+ campaign_test_key: campaign_test_key,
52
+ user_id: user_id,
53
+ variation_name: variation['name']
54
+ )
55
+ )
56
+ return variation['id'], variation['name']
57
+ end
58
+
59
+ variation_id, variation_name = get_variation_allotted(user_id, campaign)
60
+
61
+ if variation_name
62
+ save_user_profile(user_id, campaign_test_key, variation_name) if variation_name
63
+
64
+ @logger.log(
65
+ LogLevelEnum::INFO,
66
+ format(
67
+ LogMessageEnum::InfoMessages::VARIATION_ALLOCATED,
68
+ file: FILE,
69
+ campaign_test_key: campaign_test_key,
70
+ user_id: user_id,
71
+ variation_name: variation_name
72
+ )
73
+ )
74
+ else
75
+ @logger.log(
76
+ LogLevelEnum::INFO,
77
+ format(LogMessageEnum::InfoMessages::NO_VARIATION_ALLOCATED, file: FILE, campaign_test_key: campaign_test_key, user_id: user_id)
78
+ )
79
+ end
80
+ [variation_id, variation_name]
81
+ end
82
+
83
+ # Returns the Variation Allotted to User
84
+ #
85
+ # @param[String] :user_id The unique ID assigned to User
86
+ # @param[Hash] :campaign Campaign Object
87
+ #
88
+ # @return[Hash] Variation Object allotted to User
89
+
90
+ def get_variation_allotted(user_id, campaign)
91
+ variation_id, variation_name = nil
92
+ unless valid_value?(user_id)
93
+ @logger.log(
94
+ LogLevelEnum::ERROR,
95
+ format(LogMessageEnum::ErrorMessages::INVALID_USER_ID, file: FILE, user_id: user_id, method: 'get_variation_alloted')
96
+ )
97
+ return variation_id, variation_name
98
+ end
99
+
100
+ if @bucketer.user_part_of_campaign?(user_id, campaign)
101
+ variation_id, variation_name = get_variation_of_campaign_for_user(user_id, campaign)
102
+ @logger.log(
103
+ LogLevelEnum::DEBUG,
104
+ format(
105
+ LogMessageEnum::DebugMessages::GOT_VARIATION_FOR_USER,
106
+ file: FILE,
107
+ variation_name: variation_name,
108
+ user_id: user_id,
109
+ campaign_test_key: campaign['key'],
110
+ method: 'get_variation_allotted'
111
+ )
112
+ )
113
+ else
114
+ # not part of campaign
115
+ @logger.log(
116
+ LogLevelEnum::DEBUG,
117
+ format(
118
+ LogMessageEnum::DebugMessages::USER_NOT_PART_OF_CAMPAIGN,
119
+ file: FILE,
120
+ user_id: user_id,
121
+ campaign_test_key: nil,
122
+ method: 'get_variation_allotted'
123
+ )
124
+ )
125
+ end
126
+ [variation_id, variation_name]
127
+ end
128
+
129
+ # Assigns random variation ID to a particular user
130
+ # Depending on the PercentTraffic.
131
+ # Makes user a part of campaign if user's included in Traffic.
132
+ #
133
+ # @param[String] :user_id The unique ID assigned to a user
134
+ # @param[Hash] :campaign The Campaign of which user is to be made a part of
135
+ # @return[Hash|nil] Variation allotted to User
136
+ def get_variation_of_campaign_for_user(user_id, campaign)
137
+ unless campaign
138
+ @logger.log(
139
+ LogLevelEnum::ERROR,
140
+ format(
141
+ LogMessageEnum::ErrorMessages::INVALID_CAMPAIGN,
142
+ file: FILE,
143
+ method: 'get_variation_of_campaign_for_user'
144
+ )
145
+ )
146
+ return nil, nil
147
+ end
148
+
149
+ variation = @bucketer.bucket_user_to_variation(user_id, campaign)
150
+
151
+ if variation && variation['name']
152
+ @logger.log(
153
+ LogLevelEnum::INFO,
154
+ format(
155
+ LogMessageEnum::InfoMessages::GOT_VARIATION_FOR_USER,
156
+ file: FILE,
157
+ variation_name: variation['name'],
158
+ user_id: user_id,
159
+ campaign_test_key: campaign['key']
160
+ )
161
+ )
162
+ return variation['id'], variation['name']
163
+ end
164
+
165
+ @logger.log(
166
+ LogLevelEnum::INFO,
167
+ format(
168
+ LogMessageEnum::InfoMessages::USER_GOT_NO_VARIATION,
169
+ file: FILE,
170
+ user_id: user_id,
171
+ campaign_test_key: campaign['key']
172
+ )
173
+ )
174
+ [nil, nil]
175
+ end
176
+
177
+ private
178
+
179
+ # Returns the campaign bucket map corresponding to the user_id
180
+ #
181
+ # @param[String] :user_id Unique user identifier
182
+ # @return[Hash]
183
+
184
+ def resolve_campaign_bucket_map(user_id)
185
+ user_data = get_user_profile(user_id)
186
+ campaign_bucket_map = {}
187
+ campaign_bucket_map = user_data['campaignBucketMap'] if user_data
188
+ campaign_bucket_map.dup
189
+ end
190
+
191
+ # Get the UserProfileData after looking up into lookup method
192
+ # Being provided via UserProfileService
193
+ #
194
+ # @param[String]: Unique user identifier
195
+ # @return[Hash|Boolean]: user_profile data
196
+
197
+ def get_user_profile(user_id)
198
+ unless @user_profile_service
199
+ @logger.log(
200
+ LogLevelEnum::DEBUG,
201
+ format(LogMessageEnum::DebugMessages::NO_USER_PROFILE_SERVICE_LOOKUP, file: FILE)
202
+ )
203
+ return false
204
+ end
205
+
206
+ data = @user_profile_service.lookup(user_id)
207
+ @logger.log(
208
+ LogLevelEnum::INFO,
209
+ format(
210
+ LogMessageEnum::InfoMessages::LOOKING_UP_USER_PROFILE_SERVICE,
211
+ file: FILE,
212
+ user_id: user_id,
213
+ status: data.nil? ? 'Not Found' : 'Found'
214
+ )
215
+ )
216
+ data
217
+ rescue StandardError
218
+ @logger.log(
219
+ LogLevelEnum::ERROR,
220
+ format(LogMessageEnum::ErrorMessages::LOOK_UP_USER_PROFILE_SERVICE_FAILED, file: FILE, user_id: user_id)
221
+ )
222
+ false
223
+ end
224
+
225
+ # If userProfileService is provided and variation was stored,
226
+ # Get the stored variation
227
+ # @param[String] :user_id
228
+ # @param[String] :campaign_test_key campaign identified
229
+ # @param[Hash] :campaign_bucket_map BucketMap consisting of stored user variation
230
+ #
231
+ # @return[Object, nil] if found then variation settings object otherwise None
232
+
233
+ def get_stored_variation(user_id, campaign_test_key, campaign_bucket_map)
234
+ if campaign_bucket_map[campaign_test_key]
235
+ decision = campaign_bucket_map[campaign_test_key]
236
+ variation_name = decision['variationName']
237
+ @logger.log(
238
+ LogLevelEnum::DEBUG,
239
+ format(
240
+ LogMessageEnum::DebugMessages::GETTING_STORED_VARIATION,
241
+ file: FILE,
242
+ campaign_test_key: campaign_test_key,
243
+ user_id: user_id,
244
+ variation_name: variation_name
245
+ )
246
+ )
247
+ return get_campaign_variation(
248
+ @settings_file,
249
+ campaign_test_key,
250
+ variation_name
251
+ )
252
+ end
253
+
254
+ @logger.log(
255
+ LogLevelEnum::DEBUG,
256
+ format(
257
+ LogMessageEnum::DebugMessages::NO_STORED_VARIATION,
258
+ file: FILE,
259
+ campaign_test_key: campaign_test_key,
260
+ user_id: user_id
261
+ )
262
+ )
263
+ nil
264
+ end
265
+
266
+ # If userProfileService is provided and variation was stored
267
+ # Save the assigned variation
268
+ # It creates bucket and then stores.
269
+ #
270
+ # @param[String] :user_id Unique user identifier
271
+ # @param[String] :campaign_test_key Unique campaign identifier
272
+ # @param[String] :variation_name Variation identifier
273
+ # @return[Boolean] true if found otherwise false
274
+
275
+ def save_user_profile(user_id, _campaign_test_key, variation_name)
276
+ unless @user_profile_service
277
+ @logger.log(
278
+ LogLevelEnum::DEBUG,
279
+ format(LogMessageEnum::DebugMessages::NO_USER_PROFILE_SERVICE_SAVE, file: FILE)
280
+ )
281
+ return false
282
+ end
283
+ new_campaign_bucket_map = {
284
+ campaign_test_key: {
285
+ variationName: variation_name
286
+ }
287
+ }
288
+ @user_profile_service.save(
289
+ userId: user_id,
290
+ campaignBucketMap: new_campaign_bucket_map
291
+ )
292
+ @logger.log(
293
+ LogLevelEnum::INFO,
294
+ format(LogMessageEnum::InfoMessages::SAVING_DATA_USER_PROFILE_SERVICE, file: FILE, user_id: user_id)
295
+ )
296
+ true
297
+ rescue StandardError
298
+ @logger.log(
299
+ LogLevelEnum::ERROR,
300
+ format(LogMessageEnum::ErrorMessages::SAVE_USER_PROFILE_SERVICE_FAILED, file: FILE, user_id: user_id)
301
+ )
302
+ false
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_logger'
4
+ require_relative 'common/enums'
5
+ require_relative 'common/requests'
6
+
7
+ # Module for dispatching events to the server.
8
+ class VWO
9
+ class EventDispatcher
10
+ include VWO::Common::Enums
11
+
12
+ EXCLUDE_KEYS = ['url'].freeze
13
+
14
+ # Initialize the dispatcher with logger
15
+ #
16
+ # @param [Boolean] : To specify whether the request
17
+ # to our server should be made or not.
18
+ #
19
+ def initialize(is_development_mode = false)
20
+ @logger = CustomLogger.get_instance
21
+ @is_development_mode = is_development_mode
22
+ end
23
+
24
+ # Dispatch the event being represented in the properties object.
25
+ #
26
+ # @param[Hash] :properties Object holding information about
27
+ # the request to be dispatched to the VWO backend.
28
+ # @return[Boolean]
29
+ #
30
+ def dispatch(properties)
31
+ return true if @is_development_mode
32
+
33
+ modified_properties = properties.reject do |key, _value|
34
+ EXCLUDE_KEYS.include?(key)
35
+ end
36
+
37
+ resp = VWO::Common::Requests.get(properties['url'], modified_properties)
38
+ if resp.code == '200'
39
+ @logger.log(
40
+ LogLevelEnum::INFO,
41
+ format(
42
+ LogMessageEnum::InfoMessages::IMPRESSION_SUCCESS,
43
+ file: FileNameEnum::EventDispatcher,
44
+ end_point: properties[:url],
45
+ campaign_id: properties[:experiment_id],
46
+ user_id: properties[:uId],
47
+ account_id: properties[:account_id],
48
+ variation_id: properties[:combination]
49
+ )
50
+ )
51
+ return true
52
+ else
53
+ @logger.log(
54
+ LogLevelEnum::ERROR,
55
+ format(LogMessageEnum::ErrorMessages::IMPRESSION_FAILED, file: FileNameEnum.EventDispatcher, end_point: properties['url'])
56
+ )
57
+ return false
58
+ end
59
+ rescue StandardError
60
+ @logger.log(
61
+ LogLevelEnum::ERROR,
62
+ format(LogMessageEnum::ErrorMessages::IMPRESSION_FAILED, file: FileNameEnum::EventDispatcher, end_point: properties['url'])
63
+ )
64
+ false
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common/utils'
4
+ require_relative 'common/constants'
5
+ require_relative 'common/requests'
6
+ require_relative 'common/validations'
7
+
8
+ class VWO
9
+ class GetSettings
10
+ include VWO::Common::Validations
11
+
12
+ PROTOCOL = 'https'
13
+ HOSTNAME = VWO::Common::CONSTANTS::ENDPOINTS::BASE_URL
14
+ PATH = VWO::Common::CONSTANTS::ENDPOINTS::ACCOUNT_SETTINGS
15
+
16
+ def initialize(account_id, sdk_key)
17
+ @account_id = account_id
18
+ @sdk_key = sdk_key
19
+ end
20
+
21
+ # Get method to retrieve settings_file for customer from dacdn server
22
+ # @param [string]: Account ID of user
23
+ # @param [string]: Unique sdk key for user,
24
+ # can be retrieved from our website
25
+ # @return[string]: Json String representation of settings_file,
26
+ # as received from the website,
27
+ # nil if no settings_file is found or sdk_key is incorrect
28
+
29
+ def get
30
+ is_valid_key = valid_number?(@account_id) || valid_string?(@account_id)
31
+
32
+ unless is_valid_key && valid_string?(@sdk_key)
33
+ STDERR.warn 'account_id and sdk_key are required for fetching account settings. Aborting!'
34
+ return '{}'
35
+ end
36
+
37
+ dacdn_url = "#{PROTOCOL}://#{HOSTNAME}#{PATH}"
38
+
39
+ settings_file_response = VWO::Common::Requests.get(dacdn_url, params)
40
+
41
+ if settings_file_response.code != '200'
42
+ STDERR.warn <<-DOC
43
+ Request failed for fetching account settings.
44
+ Got Status Code: #{settings_file_response.code}
45
+ and message: #{settings_file_response.body}.
46
+ DOC
47
+ end
48
+ settings_file_response.body
49
+ rescue StandardError => e
50
+ STDERR.warn "Error fetching Settings File #{e}"
51
+ end
52
+
53
+ private
54
+
55
+ def params
56
+ {
57
+ a: @account_id,
58
+ i: @sdk_key,
59
+ r: VWO::Common::Utils.get_random_number,
60
+ platform: 'server',
61
+ 'api-version' => 2
62
+ }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_logger'
4
+ require_relative 'common/enums'
5
+ require_relative 'common/campaign_utils'
6
+
7
+ # Representation of the VWO settings file.
8
+
9
+ class VWO
10
+ class ProjectConfigManager
11
+ include VWO::Common::Enums
12
+ include VWO::Common::CampaignUtils
13
+
14
+ # ProjectConfigManager init method to load and set project config data.
15
+ #
16
+ # @params
17
+ # settings_file (Hash): Hash object of setting
18
+ # representing the project settings_file.
19
+
20
+ def initialize(settings_file)
21
+ @settings_file = JSON.parse(settings_file)
22
+ @logger = VWO::CustomLogger.get_instance
23
+ end
24
+
25
+ # Processes the settings_file, assigns variation allocation range
26
+ def process_settings_file
27
+ (@settings_file['campaigns'] || []).each do |campaign|
28
+ set_variation_allocation(campaign)
29
+ end
30
+ @logger.log(
31
+ LogLevelEnum::DEBUG,
32
+ format(LogMessageEnum::DebugMessages::SETTINGS_FILE_PROCESSED, file: FileNameEnum::ProjectConfigManager)
33
+ )
34
+ end
35
+
36
+ def get_settings_file
37
+ @settings_file
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VWO
4
+ # Abstract class encapsulating user profile service functionality.
5
+ # Override with your own implementation for storing
6
+ # And retrieving the user profile.
7
+
8
+ class UserProfile
9
+ # Abstract method, must be defined to fetch the
10
+ # User profile dict corresponding to the user_id.
11
+ #
12
+ # @param[String] :user_id ID for user whose profile needs to be retrieved.
13
+ # @return[Hash] :user_profile_obj Object representing the user's profile.
14
+ #
15
+ def lookup(_user_id); end
16
+
17
+ # Abstract method, must be to defined to save
18
+ # The user profile dict sent to this method.
19
+ # @param[Hash] :user_profile_obj Object representing the user's profile.
20
+ #
21
+ def save(_user_profile_obj); end
22
+ end
23
+ end