rollbar 2.10.0 → 2.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -2
- data/CHANGELOG.md +20 -0
- data/README.md +73 -16
- data/docs/configuration.md +10 -0
- data/gemfiles/rails30.gemfile +2 -0
- data/gemfiles/rails31.gemfile +2 -0
- data/gemfiles/rails32.gemfile +2 -0
- data/gemfiles/rails40.gemfile +2 -0
- data/gemfiles/rails41.gemfile +2 -0
- data/gemfiles/rails42.gemfile +2 -0
- data/gemfiles/rails50.gemfile +2 -0
- data/gemfiles/ruby_1_8_and_1_9_2.gemfile +43 -0
- data/lib/rollbar.rb +139 -353
- data/lib/rollbar/configuration.rb +4 -0
- data/lib/rollbar/item.rb +225 -0
- data/lib/rollbar/item/backtrace.rb +97 -0
- data/lib/rollbar/js.rb +0 -28
- data/lib/rollbar/language_support.rb +10 -0
- data/lib/rollbar/{js/middleware.rb → middleware/js.rb} +3 -4
- data/lib/rollbar/plugin.rb +63 -0
- data/lib/rollbar/plugins.rb +41 -0
- data/lib/rollbar/{active_job.rb → plugins/active_job.rb} +0 -0
- data/lib/rollbar/plugins/basic_socket.rb +16 -0
- data/lib/rollbar/plugins/delayed_job.rb +12 -0
- data/lib/rollbar/plugins/delayed_job/job_data.rb +16 -0
- data/lib/rollbar/{delayed_job.rb → plugins/delayed_job/plugin.rb} +1 -17
- data/lib/rollbar/plugins/goalie.rb +46 -0
- data/lib/rollbar/plugins/rack.rb +16 -0
- data/lib/rollbar/plugins/rails.rb +77 -0
- data/lib/rollbar/{rails → plugins/rails}/controller_methods.rb +0 -0
- data/lib/rollbar/plugins/rails/railtie30.rb +17 -0
- data/lib/rollbar/plugins/rails/railtie32.rb +18 -0
- data/lib/rollbar/plugins/rails/railtie_mixin.rb +33 -0
- data/lib/rollbar/plugins/rake.rb +45 -0
- data/lib/rollbar/plugins/sidekiq.rb +35 -0
- data/lib/rollbar/{sidekiq.rb → plugins/sidekiq/plugin.rb} +0 -18
- data/lib/rollbar/plugins/thread.rb +13 -0
- data/lib/rollbar/plugins/validations.rb +33 -0
- data/lib/rollbar/request_data_extractor.rb +30 -18
- data/lib/rollbar/scrubbers/params.rb +4 -2
- data/lib/rollbar/scrubbers/url.rb +30 -28
- data/lib/rollbar/util.rb +10 -0
- data/lib/rollbar/version.rb +1 -1
- data/spec/controllers/home_controller_spec.rb +4 -3
- data/spec/dummyapp/app/models/post.rb +9 -0
- data/spec/dummyapp/app/models/user.rb +2 -0
- data/spec/dummyapp/config/initializers/rollbar.rb +1 -0
- data/spec/fixtures/plugins/dummy1.rb +5 -0
- data/spec/fixtures/plugins/dummy2.rb +5 -0
- data/spec/rollbar/item_spec.rb +635 -0
- data/spec/rollbar/logger_proxy_spec.rb +4 -0
- data/spec/rollbar/{js/middleware_spec.rb → middleware/js_spec.rb} +32 -3
- data/spec/rollbar/plugin_spec.rb +147 -0
- data/spec/rollbar/{active_job_spec.rb → plugins/active_job_spec.rb} +0 -1
- data/spec/rollbar/{delayed_job → plugins/delayed_job}/job_data.rb +0 -0
- data/spec/rollbar/{delayed_job_spec.rb → plugins/delayed_job_spec.rb} +3 -6
- data/spec/rollbar/{middleware/rack/builder_spec.rb → plugins/rack_spec.rb} +2 -1
- data/spec/rollbar/{js/frameworks/rails_spec.rb → plugins/rails_js_spec.rb} +1 -1
- data/spec/rollbar/{rake_spec.rb → plugins/rake_spec.rb} +2 -1
- data/spec/rollbar/{sidekiq_spec.rb → plugins/sidekiq_spec.rb} +2 -1
- data/spec/rollbar/plugins/validations_spec.rb +43 -0
- data/spec/rollbar/plugins_spec.rb +68 -0
- data/spec/rollbar/request_data_extractor_spec.rb +56 -10
- data/spec/rollbar/scrubbers/params_spec.rb +13 -10
- data/spec/rollbar/scrubbers/url_spec.rb +17 -12
- data/spec/rollbar/sidekig/clear_scope_spec.rb +2 -1
- data/spec/rollbar/util_spec.rb +61 -0
- data/spec/rollbar_bc_spec.rb +10 -10
- data/spec/rollbar_spec.rb +57 -706
- data/spec/spec_helper.rb +8 -0
- data/spec/support/notifier_helpers.rb +1 -0
- data/spec/support/rollbar_api.rb +57 -0
- metadata +57 -33
- data/lib/rollbar/active_record_extension.rb +0 -14
- data/lib/rollbar/core_ext/basic_socket.rb +0 -7
- data/lib/rollbar/core_ext/thread.rb +0 -9
- data/lib/rollbar/goalie.rb +0 -33
- data/lib/rollbar/js/frameworks.rb +0 -6
- data/lib/rollbar/js/frameworks/rails.rb +0 -49
- data/lib/rollbar/js/version.rb +0 -5
- data/lib/rollbar/rack.rb +0 -9
- data/lib/rollbar/railtie.rb +0 -46
- data/lib/rollbar/rake.rb +0 -40
@@ -31,7 +31,9 @@ module Rollbar
|
|
31
31
|
attr_accessor :person_email_method
|
32
32
|
attr_accessor :populate_empty_backtraces
|
33
33
|
attr_accessor :report_dj_data
|
34
|
+
attr_accessor :open_timeout
|
34
35
|
attr_accessor :request_timeout
|
36
|
+
attr_accessor :net_retries
|
35
37
|
attr_accessor :root
|
36
38
|
attr_accessor :js_options
|
37
39
|
attr_accessor :js_enabled
|
@@ -88,7 +90,9 @@ module Rollbar
|
|
88
90
|
@project_gems = []
|
89
91
|
@populate_empty_backtraces = false
|
90
92
|
@report_dj_data = true
|
93
|
+
@open_timeout = 3
|
91
94
|
@request_timeout = 3
|
95
|
+
@net_retries = 3
|
92
96
|
@js_enabled = false
|
93
97
|
@js_options = {}
|
94
98
|
@scrub_fields = [:passwd, :password, :password_confirmation, :secret,
|
data/lib/rollbar/item.rb
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'securerandom'
|
6
|
+
rescue LoadError
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rollbar/item/backtrace'
|
11
|
+
require 'rollbar/util'
|
12
|
+
require 'rollbar/encoding'
|
13
|
+
|
14
|
+
module Rollbar
|
15
|
+
# This class represents the payload to be sent to the API.
|
16
|
+
# It contains the logic to build the payload, trucante it
|
17
|
+
# and dump the JSON.
|
18
|
+
class Item
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
attr_writer :payload
|
22
|
+
|
23
|
+
attr_reader :level
|
24
|
+
attr_reader :message
|
25
|
+
attr_reader :exception
|
26
|
+
attr_reader :extra
|
27
|
+
|
28
|
+
attr_reader :configuration
|
29
|
+
attr_reader :scope
|
30
|
+
attr_reader :logger
|
31
|
+
attr_reader :notifier
|
32
|
+
|
33
|
+
def_delegators :payload, :[]
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def build_with(payload, options = {})
|
37
|
+
new(options).tap do |item|
|
38
|
+
item.payload = payload
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(options)
|
44
|
+
@level = options[:level]
|
45
|
+
@message = options[:message]
|
46
|
+
@exception = options[:exception]
|
47
|
+
@extra = options[:extra]
|
48
|
+
@configuration = options[:configuration]
|
49
|
+
@logger = options[:logger]
|
50
|
+
@scope = options[:scope]
|
51
|
+
@payload = nil
|
52
|
+
@notifier = options[:notifier]
|
53
|
+
end
|
54
|
+
|
55
|
+
def payload
|
56
|
+
@payload ||= build
|
57
|
+
end
|
58
|
+
|
59
|
+
def build
|
60
|
+
data = build_data
|
61
|
+
self.payload = {
|
62
|
+
'access_token' => configuration.access_token,
|
63
|
+
'data' => data
|
64
|
+
}
|
65
|
+
|
66
|
+
enforce_valid_utf8
|
67
|
+
transform
|
68
|
+
payload
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_data
|
72
|
+
data = {
|
73
|
+
:timestamp => Time.now.to_i,
|
74
|
+
:environment => build_environment,
|
75
|
+
:level => level,
|
76
|
+
:language => 'ruby',
|
77
|
+
:framework => configuration.framework,
|
78
|
+
:server => server_data,
|
79
|
+
:notifier => {
|
80
|
+
:name => 'rollbar-gem',
|
81
|
+
:version => VERSION
|
82
|
+
},
|
83
|
+
:body => build_body
|
84
|
+
}
|
85
|
+
data[:project_package_paths] = configuration.project_gem_paths if configuration.project_gem_paths
|
86
|
+
data[:code_version] = configuration.code_version if configuration.code_version
|
87
|
+
data[:uuid] = SecureRandom.uuid if defined?(SecureRandom) && SecureRandom.respond_to?(:uuid)
|
88
|
+
|
89
|
+
Util.deep_merge(data, configuration.payload_options)
|
90
|
+
Util.deep_merge(data, scope)
|
91
|
+
|
92
|
+
# Our API doesn't allow null context values, so just delete
|
93
|
+
# the key if value is nil.
|
94
|
+
data.delete(:context) unless data[:context]
|
95
|
+
|
96
|
+
data
|
97
|
+
end
|
98
|
+
|
99
|
+
def dump
|
100
|
+
# Ensure all keys are strings since we can receive the payload inline or
|
101
|
+
# from an async handler job, which can be serialized.
|
102
|
+
stringified_payload = Util::Hash.deep_stringify_keys(payload)
|
103
|
+
result = Truncation.truncate(stringified_payload)
|
104
|
+
return result unless Truncation.truncate?(result)
|
105
|
+
|
106
|
+
original_size = Rollbar::JSON.dump(payload).bytesize
|
107
|
+
final_size = result.bytesize
|
108
|
+
notifier.send_failsafe("Could not send payload due to it being too large after truncating attempts. Original size: #{original_size} Final size: #{final_size}", nil)
|
109
|
+
logger.error("[Rollbar] Payload too large to be sent: #{Rollbar::JSON.dump(payload)}")
|
110
|
+
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def ignored?
|
115
|
+
data = payload['data']
|
116
|
+
|
117
|
+
return unless data[:person]
|
118
|
+
|
119
|
+
person_id = data[:person][configuration.person_id_method.to_sym]
|
120
|
+
configuration.ignored_person_ids.include?(person_id)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def build_environment
|
126
|
+
env = configuration.environment
|
127
|
+
env = 'unspecified' if env.nil? || env.empty?
|
128
|
+
|
129
|
+
env
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_body
|
133
|
+
exception ? build_backtrace_body : build_message_body
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_backtrace_body
|
137
|
+
backtrace = Backtrace.new(exception,
|
138
|
+
:message => message,
|
139
|
+
:extra => build_extra,
|
140
|
+
:configuration => configuration
|
141
|
+
)
|
142
|
+
|
143
|
+
backtrace.build
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_extra
|
147
|
+
if custom_data_method?
|
148
|
+
Util.deep_merge(custom_data, extra || {})
|
149
|
+
else
|
150
|
+
extra
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def custom_data_method?
|
155
|
+
!!configuration.custom_data_method
|
156
|
+
end
|
157
|
+
|
158
|
+
def custom_data
|
159
|
+
data = configuration.custom_data_method.call
|
160
|
+
Rollbar::Util.deep_copy(data)
|
161
|
+
rescue => e
|
162
|
+
return {} if configuration.safely?
|
163
|
+
|
164
|
+
report_custom_data_error(e)
|
165
|
+
end
|
166
|
+
|
167
|
+
def report_custom_data_error(e)
|
168
|
+
data = notifier.safely.error(e)
|
169
|
+
|
170
|
+
return {} unless data.is_a?(Hash) && data[:uuid]
|
171
|
+
|
172
|
+
uuid_url = Util.uuid_rollbar_url(data, configuration)
|
173
|
+
|
174
|
+
{ :_error_in_custom_data_method => uuid_url }
|
175
|
+
end
|
176
|
+
|
177
|
+
def build_message_body
|
178
|
+
extra = build_extra
|
179
|
+
result = { :body => message || 'Empty message' }
|
180
|
+
result[:extra] = extra if extra
|
181
|
+
|
182
|
+
{ :message => result }
|
183
|
+
end
|
184
|
+
|
185
|
+
def server_data
|
186
|
+
data = {
|
187
|
+
:host => Socket.gethostname
|
188
|
+
}
|
189
|
+
data[:root] = configuration.root.to_s if configuration.root
|
190
|
+
data[:branch] = configuration.branch if configuration.branch
|
191
|
+
data[:pid] = Process.pid
|
192
|
+
|
193
|
+
data
|
194
|
+
end
|
195
|
+
|
196
|
+
def enforce_valid_utf8
|
197
|
+
Util.enforce_valid_utf8(payload)
|
198
|
+
end
|
199
|
+
|
200
|
+
def transform
|
201
|
+
handlers = configuration.transform
|
202
|
+
|
203
|
+
handlers.each do |handler|
|
204
|
+
begin
|
205
|
+
handler.call(transform_options)
|
206
|
+
rescue => e
|
207
|
+
logger.error("[Rollbar] Error calling the `transform` hook: #{e}")
|
208
|
+
|
209
|
+
break
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def transform_options
|
215
|
+
{
|
216
|
+
:level => level,
|
217
|
+
:scope => scope,
|
218
|
+
:exception => exception,
|
219
|
+
:message => message,
|
220
|
+
:extra => extra,
|
221
|
+
:payload => payload
|
222
|
+
}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Rollbar
|
2
|
+
class Item
|
3
|
+
class Backtrace
|
4
|
+
attr_reader :exception
|
5
|
+
attr_reader :message
|
6
|
+
attr_reader :extra
|
7
|
+
attr_reader :configuration
|
8
|
+
|
9
|
+
def initialize(exception, options = {})
|
10
|
+
@exception = exception
|
11
|
+
@message = options[:message]
|
12
|
+
@extra = options[:extra]
|
13
|
+
@configuration = options[:configuration]
|
14
|
+
end
|
15
|
+
|
16
|
+
def build
|
17
|
+
traces = trace_chain
|
18
|
+
|
19
|
+
traces[0][:exception][:description] = message if message
|
20
|
+
traces[0][:extra] = extra if extra
|
21
|
+
|
22
|
+
if traces.size > 1
|
23
|
+
{ :trace_chain => traces }
|
24
|
+
elsif traces.size == 1
|
25
|
+
{ :trace => traces[0] }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def trace_chain
|
32
|
+
exception
|
33
|
+
traces = [trace_data(exception)]
|
34
|
+
visited = [exception]
|
35
|
+
|
36
|
+
current_exception = exception
|
37
|
+
|
38
|
+
while current_exception.respond_to?(:cause) && (cause = current_exception.cause) && cause.is_a?(Exception) && !visited.include?(cause)
|
39
|
+
traces << trace_data(cause)
|
40
|
+
visited << cause
|
41
|
+
current_exception = cause
|
42
|
+
end
|
43
|
+
|
44
|
+
traces
|
45
|
+
end
|
46
|
+
|
47
|
+
def trace_data(current_exception)
|
48
|
+
frames = reduce_frames(current_exception)
|
49
|
+
# reverse so that the order is as rollbar expects
|
50
|
+
frames.reverse!
|
51
|
+
|
52
|
+
{
|
53
|
+
:frames => frames,
|
54
|
+
:exception => {
|
55
|
+
:class => current_exception.class.name,
|
56
|
+
:message => current_exception.message
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def reduce_frames(current_exception)
|
62
|
+
exception_backtrace(current_exception).map do |frame|
|
63
|
+
# parse the line
|
64
|
+
match = frame.match(/(.*):(\d+)(?::in `([^']+)')?/)
|
65
|
+
|
66
|
+
if match
|
67
|
+
{ :filename => match[1], :lineno => match[2].to_i, :method => match[3] }
|
68
|
+
else
|
69
|
+
{ :filename => '<unknown>', :lineno => 0, :method => frame }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the backtrace to be sent to our API. There are 3 options:
|
75
|
+
#
|
76
|
+
# 1. The exception received has a backtrace, then that backtrace is returned.
|
77
|
+
# 2. configuration.populate_empty_backtraces is disabled, we return [] here
|
78
|
+
# 3. The user has configuration.populate_empty_backtraces is enabled, then:
|
79
|
+
#
|
80
|
+
# We want to send the caller as backtrace, but the first lines of that array
|
81
|
+
# are those from the user's Rollbar.error line until this method. We want
|
82
|
+
# to remove those lines.
|
83
|
+
def exception_backtrace(current_exception)
|
84
|
+
return current_exception.backtrace if current_exception.backtrace.respond_to?(:map)
|
85
|
+
return [] unless configuration.populate_empty_backtraces
|
86
|
+
|
87
|
+
caller_backtrace = caller
|
88
|
+
caller_backtrace.shift while caller_backtrace[0].include?(rollbar_lib_gem_dir)
|
89
|
+
caller_backtrace
|
90
|
+
end
|
91
|
+
|
92
|
+
def rollbar_lib_gem_dir
|
93
|
+
Gem::Specification.find_by_name('rollbar').gem_dir + '/lib'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/rollbar/js.rb
CHANGED
@@ -1,32 +1,4 @@
|
|
1
|
-
require "rollbar/js/version"
|
2
|
-
|
3
1
|
module Rollbar
|
4
2
|
module Js
|
5
|
-
extend self
|
6
|
-
|
7
|
-
attr_reader :framework
|
8
|
-
attr_reader :framework_loader
|
9
|
-
|
10
|
-
def prepare
|
11
|
-
@framework ||= detect_framework
|
12
|
-
@framework_loader ||= load_framework_class.new
|
13
|
-
|
14
|
-
@framework_loader.prepare
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def detect_framework
|
20
|
-
case
|
21
|
-
when defined?(::Rails::VERSION)
|
22
|
-
:rails
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def load_framework_class
|
27
|
-
require "rollbar/js/frameworks/#{framework}"
|
28
|
-
|
29
|
-
Rollbar::Js::Frameworks.const_get(framework.to_s.capitalize)
|
30
|
-
end
|
31
3
|
end
|
32
4
|
end
|
@@ -26,10 +26,20 @@ module Rollbar
|
|
26
26
|
version?('1.8')
|
27
27
|
end
|
28
28
|
|
29
|
+
def ruby_19?
|
30
|
+
version?('1.9')
|
31
|
+
end
|
32
|
+
|
29
33
|
def version?(version)
|
30
34
|
numbers = version.split('.')
|
31
35
|
|
32
36
|
numbers == ::RUBY_VERSION.split('.')[0, numbers.size]
|
33
37
|
end
|
38
|
+
|
39
|
+
def timeout_exceptions
|
40
|
+
return [] if ruby_18? || ruby_19?
|
41
|
+
|
42
|
+
[Net::ReadTimeout, Net::OpenTimeout]
|
43
|
+
end
|
34
44
|
end
|
35
45
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
require 'rack'
|
2
2
|
require 'rack/response'
|
3
3
|
|
4
|
-
|
5
4
|
module Rollbar
|
6
|
-
module
|
7
|
-
class
|
5
|
+
module Middleware
|
6
|
+
class Js
|
8
7
|
attr_reader :app
|
9
8
|
attr_reader :config
|
10
9
|
|
@@ -117,7 +116,7 @@ module Rollbar
|
|
117
116
|
end
|
118
117
|
|
119
118
|
def script_tag(content, env)
|
120
|
-
if defined?(::SecureHeaders)
|
119
|
+
if defined?(::SecureHeaders) && ::SecureHeaders.respond_to?(:content_security_policy_script_nonce)
|
121
120
|
nonce = ::SecureHeaders.content_security_policy_script_nonce(::Rack::Request.new(env))
|
122
121
|
script_tag_content = "\n<script type=\"text/javascript\" nonce=\"#{nonce}\">#{content}</script>"
|
123
122
|
else
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Rollbar
|
2
|
+
# Represents a plugin in the gem. Every plugin can have multiple dependencies
|
3
|
+
# and multiple execution blocks.
|
4
|
+
# On Rollbar initialization, all plugins will be saved in memory and those that
|
5
|
+
# satisfy the dependencies will be loaded
|
6
|
+
class Plugin
|
7
|
+
attr_reader :name
|
8
|
+
attr_reader :dependencies
|
9
|
+
attr_reader :callables
|
10
|
+
attr_accessor :loaded
|
11
|
+
|
12
|
+
private :loaded=
|
13
|
+
|
14
|
+
def initialize(name)
|
15
|
+
@name = name
|
16
|
+
@dependencies = []
|
17
|
+
@callables = []
|
18
|
+
@loaded = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def configuration
|
22
|
+
Rollbar.configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
def load!
|
26
|
+
return unless load?
|
27
|
+
|
28
|
+
begin
|
29
|
+
callables.each(&:call)
|
30
|
+
rescue => e
|
31
|
+
log_loading_error(e)
|
32
|
+
ensure
|
33
|
+
self.loaded = true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def execute(&block)
|
38
|
+
callables << block
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute!(&block)
|
42
|
+
block.call if load?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def dependency(&block)
|
48
|
+
dependencies << block
|
49
|
+
end
|
50
|
+
|
51
|
+
def load?
|
52
|
+
!loaded && dependencies.all?(&:call)
|
53
|
+
rescue => e
|
54
|
+
log_loading_error(e)
|
55
|
+
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
def log_loading_error(e)
|
60
|
+
Rollbar.log_error("Error trying to load plugin '#{name}': #{e.class}, #{e.message}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|