vwo-sdk 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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