vwo-sdk 1.5.0 → 1.15.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 +4 -4
- data/lib/vwo/constants.rb +26 -3
- data/lib/vwo/core/bucketer.rb +4 -6
- data/lib/vwo/core/variation_decider.rb +337 -45
- data/lib/vwo/enums.rb +49 -9
- data/lib/vwo/logger.rb +1 -1
- data/lib/vwo/schemas/settings_file.rb +1 -1
- data/lib/vwo/services/batch_events_dispatcher.rb +110 -0
- data/lib/vwo/services/batch_events_queue.rb +175 -0
- data/lib/vwo/services/event_dispatcher.rb +1 -13
- data/lib/vwo/services/hooks_manager.rb +36 -0
- data/lib/vwo/services/operand_evaluator.rb +10 -2
- data/lib/vwo/services/segment_evaluator.rb +5 -26
- data/lib/vwo/services/settings_file_manager.rb +8 -4
- data/lib/vwo/services/settings_file_processor.rb +6 -1
- data/lib/vwo/services/usage_stats.rb +29 -0
- data/lib/vwo/user_storage.rb +1 -1
- data/lib/vwo/utils/campaign.rb +108 -1
- data/lib/vwo/utils/custom_dimensions.rb +26 -3
- data/lib/vwo/utils/feature.rb +1 -1
- data/lib/vwo/utils/function.rb +1 -1
- data/lib/vwo/utils/impression.rb +58 -7
- data/lib/vwo/utils/request.rb +15 -1
- data/lib/vwo/utils/segment.rb +1 -1
- data/lib/vwo/utils/uuid.rb +1 -1
- data/lib/vwo/utils/validations.rb +85 -1
- data/lib/vwo.rb +586 -203
- metadata +36 -13
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -89,7 +89,7 @@ class VWO
|
|
89
89
|
# @param [String] :custom_variables_value Value from the custom_variables
|
90
90
|
#
|
91
91
|
# @return [Boolean]
|
92
|
-
def
|
92
|
+
def evaluate_custom_variable?(operand, custom_variables)
|
93
93
|
# Extract custom_variable_key and custom_variables_value from operand
|
94
94
|
|
95
95
|
operand_key, operand = get_key_value(operand)
|
@@ -109,6 +109,14 @@ class VWO
|
|
109
109
|
# Call the self method corresponding to operand_type to evaluate the result
|
110
110
|
public_send("#{operand_type}?", operand_value, custom_variables_value)
|
111
111
|
end
|
112
|
+
|
113
|
+
def evaluate_user?(operand, custom_variables)
|
114
|
+
users = operand.split(',')
|
115
|
+
users.each do |user|
|
116
|
+
return true if user.strip == custom_variables['_vwo_user_id']
|
117
|
+
end
|
118
|
+
false
|
119
|
+
end
|
112
120
|
end
|
113
121
|
end
|
114
122
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -50,7 +50,9 @@ class VWO
|
|
50
50
|
elsif operator == OperatorTypes::OR
|
51
51
|
sub_dsl.any? { |y| evaluate_util(y, custom_variables) }
|
52
52
|
elsif operator == OperandTypes::CUSTOM_VARIABLE
|
53
|
-
@operand_evaluator.
|
53
|
+
@operand_evaluator.evaluate_custom_variable?(sub_dsl, custom_variables)
|
54
|
+
elsif operator == OperandTypes::USER
|
55
|
+
@operand_evaluator.evaluate_user?(sub_dsl, custom_variables)
|
54
56
|
end
|
55
57
|
end
|
56
58
|
|
@@ -66,35 +68,12 @@ class VWO
|
|
66
68
|
#
|
67
69
|
def evaluate(campaign_key, user_id, dsl, custom_variables)
|
68
70
|
result = evaluate_util(dsl, custom_variables) if valid_value?(dsl)
|
69
|
-
if result
|
70
|
-
@logger.log(
|
71
|
-
LogLevelEnum::INFO,
|
72
|
-
format(
|
73
|
-
LogMessageEnum::InfoMessages::USER_PASSED_PRE_SEGMENTATION,
|
74
|
-
file: FileNameEnum::SegmentEvaluator,
|
75
|
-
user_id: user_id,
|
76
|
-
campaign_key: campaign_key,
|
77
|
-
custom_variables: custom_variables
|
78
|
-
)
|
79
|
-
)
|
80
|
-
else
|
81
|
-
@logger.log(
|
82
|
-
LogLevelEnum::INFO,
|
83
|
-
format(
|
84
|
-
LogMessageEnum::InfoMessages::USER_FAILED_PRE_SEGMENTATION,
|
85
|
-
file: FileNameEnum::SegmentEvaluator,
|
86
|
-
user_id: user_id,
|
87
|
-
campaign_key: campaign_key,
|
88
|
-
custom_variables: custom_variables
|
89
|
-
)
|
90
|
-
)
|
91
|
-
end
|
92
71
|
result
|
93
72
|
rescue StandardError => e
|
94
73
|
@logger.log(
|
95
74
|
LogLevelEnum::ERROR,
|
96
75
|
format(
|
97
|
-
LogMessageEnum::ErrorMessages::
|
76
|
+
LogMessageEnum::ErrorMessages::SEGMENTATION_ERROR,
|
98
77
|
file: FileNameEnum::SegmentEvaluator,
|
99
78
|
user_id: user_id,
|
100
79
|
campaign_key: campaign_key,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -25,7 +25,6 @@ class VWO
|
|
25
25
|
|
26
26
|
PROTOCOL = 'https'
|
27
27
|
HOSTNAME = ::VWO::CONSTANTS::ENDPOINTS::BASE_URL
|
28
|
-
PATH = ::VWO::CONSTANTS::ENDPOINTS::ACCOUNT_SETTINGS
|
29
28
|
|
30
29
|
def initialize(account_id, sdk_key)
|
31
30
|
@account_id = account_id
|
@@ -40,7 +39,7 @@ class VWO
|
|
40
39
|
# as received from the server,
|
41
40
|
# nil if no settings_file is found or sdk_key is incorrect
|
42
41
|
|
43
|
-
def get_settings_file
|
42
|
+
def get_settings_file(is_via_webhook = false)
|
44
43
|
is_valid_key = valid_number?(@account_id) || valid_string?(@account_id)
|
45
44
|
|
46
45
|
unless is_valid_key && valid_string?(@sdk_key)
|
@@ -48,7 +47,12 @@ class VWO
|
|
48
47
|
return '{}'
|
49
48
|
end
|
50
49
|
|
51
|
-
|
50
|
+
if is_via_webhook
|
51
|
+
path = ::VWO::CONSTANTS::ENDPOINTS::WEBHOOK_SETTINGS_URL
|
52
|
+
else
|
53
|
+
path = ::VWO::CONSTANTS::ENDPOINTS::SETTINGS_URL
|
54
|
+
end
|
55
|
+
vwo_server_url = "#{PROTOCOL}://#{HOSTNAME}#{path}"
|
52
56
|
|
53
57
|
settings_file_response = ::VWO::Utils::Request.get(vwo_server_url, params)
|
54
58
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -44,6 +44,11 @@ class VWO
|
|
44
44
|
)
|
45
45
|
end
|
46
46
|
|
47
|
+
def update_settings_file(settings_file)
|
48
|
+
@settings_file = settings_file
|
49
|
+
process_settings_file
|
50
|
+
end
|
51
|
+
|
47
52
|
def get_settings_file
|
48
53
|
@settings_file
|
49
54
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright 2019-2021 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
|
+
class VWO
|
16
|
+
module Services
|
17
|
+
class UsageStats
|
18
|
+
attr_reader :usage_stats
|
19
|
+
# Initialize the UsageStats
|
20
|
+
def initialize(stats, is_development_mode = false)
|
21
|
+
@usage_stats = {}
|
22
|
+
unless is_development_mode
|
23
|
+
@usage_stats = stats
|
24
|
+
@usage_stats[:_l] = 1 if @usage_stats.length > 0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/vwo/user_storage.rb
CHANGED
data/lib/vwo/utils/campaign.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -142,6 +142,113 @@ class VWO
|
|
142
142
|
campaign['key'] == campaign_key
|
143
143
|
end
|
144
144
|
end
|
145
|
+
|
146
|
+
# fetch campaigns from settings
|
147
|
+
#
|
148
|
+
# [string|array|nil] :campaign_key
|
149
|
+
# [Hash] :settings_file
|
150
|
+
# [string] :goal_identifier
|
151
|
+
# [string] :goal_type_to_track
|
152
|
+
# @return[Hash]
|
153
|
+
def get_campaigns(settings_file, campaign_key, goal_identifier, goal_type_to_track = 'ALL')
|
154
|
+
campaigns = []
|
155
|
+
if campaign_key.nil?
|
156
|
+
campaigns = get_campaigns_for_goal(settings_file, goal_identifier, goal_type_to_track)
|
157
|
+
elsif campaign_key.is_a?(Array)
|
158
|
+
campaigns = get_campaigns_from_campaign_keys(campaign_key, settings_file, goal_identifier, goal_type_to_track)
|
159
|
+
elsif campaign_key.is_a?(String)
|
160
|
+
campaign = get_campaign_for_campaign_key_and_goal(campaign_key, settings_file, goal_identifier, goal_type_to_track)
|
161
|
+
if campaign
|
162
|
+
campaigns = [campaign]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
if campaigns.length() == 0
|
166
|
+
VWO::Logger.get_instance.log(
|
167
|
+
LogLevelEnum::ERROR,
|
168
|
+
format(
|
169
|
+
LogMessageEnum::ErrorMessages::NO_CAMPAIGN_FOUND,
|
170
|
+
file: FileNameEnum::CampaignUtil,
|
171
|
+
goal_identifier: goal_identifier
|
172
|
+
)
|
173
|
+
)
|
174
|
+
end
|
175
|
+
return campaigns
|
176
|
+
end
|
177
|
+
|
178
|
+
# fetch all running campaigns (having goal identifier goal_type_to_track and goal type CUSTOM|REVENUE|ALL) from settings
|
179
|
+
#
|
180
|
+
# [Hash] :settings_file
|
181
|
+
# [string] :goal_identifier
|
182
|
+
# [string] :goal_type_to_track
|
183
|
+
# @return[Hash]
|
184
|
+
def get_campaigns_for_goal(settings_file, goal_identifier, goal_type_to_track = 'ALL')
|
185
|
+
campaigns = []
|
186
|
+
if settings_file
|
187
|
+
settings_file['campaigns'].each do |campaign|
|
188
|
+
if campaign.key?(:status) && campaign[:status] != 'RUNNING'
|
189
|
+
next
|
190
|
+
end
|
191
|
+
goal = get_campaign_goal(campaign, goal_identifier)
|
192
|
+
if validate_goal(goal, goal_type_to_track)
|
193
|
+
campaigns.push(campaign)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
campaigns
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate_goal(goal, goal_type_to_track)
|
201
|
+
result = goal && (
|
202
|
+
goal_type_to_track == 'ALL' ||
|
203
|
+
(
|
204
|
+
GOAL_TYPES.has_value?(goal['type']) &&
|
205
|
+
(GOAL_TYPES.key? goal_type_to_track) &&
|
206
|
+
goal['type'] == GOAL_TYPES[goal_type_to_track]
|
207
|
+
)
|
208
|
+
)
|
209
|
+
return result
|
210
|
+
end
|
211
|
+
|
212
|
+
def get_campaigns_from_campaign_keys(campaign_keys, settings_file, goal_identifier, goal_type_to_track = 'ALL')
|
213
|
+
campaigns = []
|
214
|
+
campaign_keys.each do |campaign_key|
|
215
|
+
|
216
|
+
campaign = get_campaign_for_campaign_key_and_goal(campaign_key, settings_file, goal_identifier, goal_type_to_track)
|
217
|
+
if campaign
|
218
|
+
campaigns.push(campaign)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
campaigns
|
222
|
+
end
|
223
|
+
|
224
|
+
def get_campaign_for_campaign_key_and_goal(campaign_key, settings_file, goal_identifier, goal_type_to_track)
|
225
|
+
campaign = get_running_campaign(campaign_key, settings_file)
|
226
|
+
if campaign
|
227
|
+
goal = get_campaign_goal(campaign, goal_identifier)
|
228
|
+
if validate_goal(goal, goal_type_to_track)
|
229
|
+
return campaign
|
230
|
+
end
|
231
|
+
end
|
232
|
+
nil
|
233
|
+
end
|
234
|
+
|
235
|
+
def get_running_campaign(campaign_key, settings_file)
|
236
|
+
campaign = get_campaign(settings_file, campaign_key)
|
237
|
+
if campaign.nil? || (campaign['status'] != 'RUNNING')
|
238
|
+
@logger.log(
|
239
|
+
LogLevelEnum::ERROR,
|
240
|
+
format(
|
241
|
+
LogMessageEnum::ErrorMessages::CAMPAIGN_NOT_RUNNING,
|
242
|
+
file: FILE,
|
243
|
+
campaign_key: campaign_key,
|
244
|
+
api_name: ApiMethods::TRACK
|
245
|
+
)
|
246
|
+
)
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
return campaign
|
250
|
+
end
|
251
|
+
|
145
252
|
end
|
146
253
|
end
|
147
254
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -26,13 +26,36 @@ class VWO
|
|
26
26
|
include VWO::Enums
|
27
27
|
include VWO::Utils::Impression
|
28
28
|
|
29
|
-
def get_url_params(settings_file, tag_key, tag_value, user_id)
|
29
|
+
def get_url_params(settings_file, tag_key, tag_value, user_id, sdk_key)
|
30
30
|
url = HTTPS_PROTOCOL + ENDPOINTS::BASE_URL + ENDPOINTS::PUSH
|
31
31
|
tag = { 'u' => {} }
|
32
32
|
tag['u'][tag_key] = tag_value
|
33
33
|
|
34
34
|
params = get_common_properties(user_id, settings_file)
|
35
|
-
params.merge!('url' => url, 'tags' => JSON.generate(tag))
|
35
|
+
params.merge!('url' => url, 'tags' => JSON.generate(tag), 'env' => sdk_key)
|
36
|
+
|
37
|
+
VWO::Logger.get_instance.log(
|
38
|
+
LogLevelEnum::DEBUG,
|
39
|
+
format(
|
40
|
+
LogMessageEnum::DebugMessages::PARAMS_FOR_PUSH_CALL,
|
41
|
+
file: FileNameEnum::CustomDimensionsUtil,
|
42
|
+
properties: JSON.generate(params)
|
43
|
+
)
|
44
|
+
)
|
45
|
+
params
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_batch_event_url_params(settings_file, tag_key, tag_value, user_id)
|
49
|
+
tag = { 'u' => {} }
|
50
|
+
tag['u'][tag_key] = tag_value
|
51
|
+
|
52
|
+
account_id = settings_file['accountId']
|
53
|
+
params = {
|
54
|
+
'eT' => 3,
|
55
|
+
't' => JSON.generate(tag),
|
56
|
+
'u' => generator_for(user_id, account_id),
|
57
|
+
'sId' => get_current_unix_timestamp
|
58
|
+
}
|
36
59
|
|
37
60
|
VWO::Logger.get_instance.log(
|
38
61
|
LogLevelEnum::DEBUG,
|
data/lib/vwo/utils/feature.rb
CHANGED
data/lib/vwo/utils/function.rb
CHANGED
data/lib/vwo/utils/impression.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2019-
|
1
|
+
# Copyright 2019-2021 Wingify Software Pvt. Ltd.
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -31,16 +31,17 @@ class VWO
|
|
31
31
|
|
32
32
|
# Creates the impression from the arguments passed
|
33
33
|
#
|
34
|
-
# @param[Hash] :settings_file
|
34
|
+
# @param[Hash] :settings_file Settings file object
|
35
35
|
# @param[String] :campaign_id Campaign identifier
|
36
36
|
# @param[String] :variation_id Variation identifier
|
37
37
|
# @param[String] :user_id User identifier
|
38
|
+
# @param[String] :sdk_key SDK Key
|
38
39
|
# @param[String] :goal_id Goal identifier, if building track impression
|
39
40
|
# @param[String|Float|Integer|nil) :revenue Number value, in any representation, if building track impression
|
40
41
|
#
|
41
42
|
# @return[nil|Hash] None if campaign ID or variation ID is invalid,
|
42
43
|
# Else Properties(dict)
|
43
|
-
def create_impression(settings_file, campaign_id, variation_id, user_id, goal_id = nil, revenue = nil)
|
44
|
+
def create_impression(settings_file, campaign_id, variation_id, user_id, sdk_key, goal_id = nil, revenue = nil, usage_stats = {})
|
44
45
|
return unless valid_number?(campaign_id) && valid_string?(user_id)
|
45
46
|
|
46
47
|
is_track_user_api = true
|
@@ -51,17 +52,19 @@ class VWO
|
|
51
52
|
account_id: account_id,
|
52
53
|
experiment_id: campaign_id,
|
53
54
|
ap: PLATFORM,
|
54
|
-
uId: CGI.escape(user_id.encode('utf-8')),
|
55
55
|
combination: variation_id,
|
56
56
|
random: get_random_number,
|
57
57
|
sId: get_current_unix_timestamp,
|
58
|
-
u: generator_for(user_id, account_id)
|
58
|
+
u: generator_for(user_id, account_id),
|
59
|
+
env: sdk_key
|
59
60
|
}
|
60
61
|
# Version and SDK constants
|
61
62
|
sdk_version = Gem.loaded_specs['vwo_sdk'] ? Gem.loaded_specs['vwo_sdk'].version : VWO::SDK_VERSION
|
62
63
|
impression['sdk'] = 'ruby'
|
63
64
|
impression['sdk-v'] = sdk_version
|
64
65
|
|
66
|
+
impression = usage_stats.merge(impression)
|
67
|
+
|
65
68
|
url = HTTPS_PROTOCOL + ENDPOINTS::BASE_URL
|
66
69
|
logger = VWO::Logger.get_instance
|
67
70
|
|
@@ -107,9 +110,57 @@ class VWO
|
|
107
110
|
'ap' => PLATFORM,
|
108
111
|
'sId' => get_current_unix_timestamp,
|
109
112
|
'u' => generator_for(user_id, account_id),
|
110
|
-
'account_id' => account_id
|
111
|
-
|
113
|
+
'account_id' => account_id
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
# Creates properties for the bulk impression event
|
118
|
+
#
|
119
|
+
# @param[Hash] :settings_file Settings file object
|
120
|
+
# @param[String] :campaign_id Campaign identifier
|
121
|
+
# @param[String] :variation_id Variation identifier
|
122
|
+
# @param[String] :user_id User identifier
|
123
|
+
# @param[String] :sdk_key SDK Key
|
124
|
+
# @param[String] :goal_id Goal identifier, if building track impression
|
125
|
+
# @param[String|Float|Integer|nil) :revenue Number value, in any representation, if building track impression
|
126
|
+
#
|
127
|
+
# @return[nil|Hash] None if campaign ID or variation ID is invalid,
|
128
|
+
# Else Properties(dict)
|
129
|
+
def create_bulk_event_impression(settings_file, campaign_id, variation_id, user_id, goal_id = nil, revenue = nil)
|
130
|
+
return unless valid_number?(campaign_id) && valid_string?(user_id)
|
131
|
+
is_track_user_api = true
|
132
|
+
is_track_user_api = false unless goal_id.nil?
|
133
|
+
account_id = settings_file['accountId']
|
134
|
+
impression = {
|
135
|
+
eT: is_track_user_api ? 1 : 2,
|
136
|
+
e: campaign_id,
|
137
|
+
c: variation_id,
|
138
|
+
u: generator_for(user_id, account_id),
|
139
|
+
sId: get_current_unix_timestamp
|
112
140
|
}
|
141
|
+
logger = VWO::Logger.get_instance
|
142
|
+
if is_track_user_api
|
143
|
+
logger.log(
|
144
|
+
LogLevelEnum::DEBUG,
|
145
|
+
format(
|
146
|
+
LogMessageEnum::DebugMessages::IMPRESSION_FOR_TRACK_USER,
|
147
|
+
file: FileNameEnum::ImpressionUtil,
|
148
|
+
properties: JSON.generate(impression)
|
149
|
+
)
|
150
|
+
)
|
151
|
+
else
|
152
|
+
impression['g'] = goal_id
|
153
|
+
impression['r'] = revenue if revenue
|
154
|
+
logger.log(
|
155
|
+
LogLevelEnum::DEBUG,
|
156
|
+
format(
|
157
|
+
LogMessageEnum::DebugMessages::IMPRESSION_FOR_TRACK_GOAL,
|
158
|
+
file: FileNameEnum::ImpressionUtil,
|
159
|
+
properties: JSON.generate(impression)
|
160
|
+
)
|
161
|
+
)
|
162
|
+
end
|
163
|
+
impression
|
113
164
|
end
|
114
165
|
end
|
115
166
|
end
|