bugsnag-maglev- 2.8.12

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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +52 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +15 -0
  6. data/CHANGELOG.md +425 -0
  7. data/CONTRIBUTING.md +43 -0
  8. data/Gemfile +2 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +804 -0
  11. data/Rakefile +29 -0
  12. data/VERSION +1 -0
  13. data/bugsnag.gemspec +32 -0
  14. data/lib/bugsnag.rb +129 -0
  15. data/lib/bugsnag/capistrano.rb +7 -0
  16. data/lib/bugsnag/capistrano2.rb +32 -0
  17. data/lib/bugsnag/configuration.rb +129 -0
  18. data/lib/bugsnag/delay/resque.rb +21 -0
  19. data/lib/bugsnag/delayed_job.rb +57 -0
  20. data/lib/bugsnag/delivery.rb +18 -0
  21. data/lib/bugsnag/delivery/synchronous.rb +51 -0
  22. data/lib/bugsnag/delivery/thread_queue.rb +53 -0
  23. data/lib/bugsnag/deploy.rb +35 -0
  24. data/lib/bugsnag/helpers.rb +127 -0
  25. data/lib/bugsnag/mailman.rb +28 -0
  26. data/lib/bugsnag/meta_data.rb +7 -0
  27. data/lib/bugsnag/middleware/callbacks.rb +19 -0
  28. data/lib/bugsnag/middleware/mailman.rb +13 -0
  29. data/lib/bugsnag/middleware/rack_request.rb +72 -0
  30. data/lib/bugsnag/middleware/rails2_request.rb +52 -0
  31. data/lib/bugsnag/middleware/rails3_request.rb +42 -0
  32. data/lib/bugsnag/middleware/rake.rb +23 -0
  33. data/lib/bugsnag/middleware/sidekiq.rb +13 -0
  34. data/lib/bugsnag/middleware/warden_user.rb +39 -0
  35. data/lib/bugsnag/middleware_stack.rb +98 -0
  36. data/lib/bugsnag/notification.rb +452 -0
  37. data/lib/bugsnag/rack.rb +53 -0
  38. data/lib/bugsnag/rails.rb +66 -0
  39. data/lib/bugsnag/rails/action_controller_rescue.rb +62 -0
  40. data/lib/bugsnag/rails/active_record_rescue.rb +20 -0
  41. data/lib/bugsnag/rails/controller_methods.rb +44 -0
  42. data/lib/bugsnag/railtie.rb +78 -0
  43. data/lib/bugsnag/rake.rb +25 -0
  44. data/lib/bugsnag/resque.rb +40 -0
  45. data/lib/bugsnag/sidekiq.rb +38 -0
  46. data/lib/bugsnag/tasks.rb +3 -0
  47. data/lib/bugsnag/tasks/bugsnag.cap +48 -0
  48. data/lib/bugsnag/tasks/bugsnag.rake +89 -0
  49. data/lib/bugsnag/version.rb +3 -0
  50. data/lib/generators/bugsnag/bugsnag_generator.rb +24 -0
  51. data/rails/init.rb +3 -0
  52. data/spec/code_spec.rb +86 -0
  53. data/spec/fixtures/crashes/end_of_file.rb +9 -0
  54. data/spec/fixtures/crashes/short_file.rb +1 -0
  55. data/spec/fixtures/crashes/start_of_file.rb +9 -0
  56. data/spec/fixtures/middleware/internal_info_setter.rb +11 -0
  57. data/spec/fixtures/middleware/public_info_setter.rb +11 -0
  58. data/spec/fixtures/tasks/Rakefile +15 -0
  59. data/spec/helper_spec.rb +144 -0
  60. data/spec/integration_spec.rb +110 -0
  61. data/spec/middleware_spec.rb +181 -0
  62. data/spec/notification_spec.rb +822 -0
  63. data/spec/rack_spec.rb +56 -0
  64. data/spec/spec_helper.rb +53 -0
  65. metadata +198 -0
@@ -0,0 +1,42 @@
1
+ module Bugsnag::Middleware
2
+ class Rails3Request
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ if notification.request_data[:rack_env]
9
+ env = notification.request_data[:rack_env]
10
+ params = env["action_dispatch.request.parameters"]
11
+
12
+ if params
13
+ # Set the context
14
+ notification.context = "#{params[:controller]}##{params[:action]}"
15
+
16
+ # Augment the request tab
17
+ notification.add_tab(:request, {
18
+ :railsAction => "#{params[:controller]}##{params[:action]}",
19
+ :params => params
20
+ })
21
+ end
22
+
23
+ # Use action_dispatch.remote_ip for IP address fields and send request id
24
+ notification.add_tab(:request, {
25
+ :clientIp => env["action_dispatch.remote_ip"],
26
+ :requestId => env["action_dispatch.request_id"]
27
+ })
28
+
29
+ notification.user_id = env["action_dispatch.remote_ip"]
30
+
31
+ # Add the rails version
32
+ if notification.configuration.send_environment
33
+ notification.add_tab(:environment, {
34
+ :railsVersion => Rails::VERSION::STRING
35
+ })
36
+ end
37
+ end
38
+
39
+ @bugsnag.call(notification)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ module Bugsnag::Middleware
2
+ class Rake
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ task = notification.request_data[:bugsnag_running_task]
9
+
10
+ if task
11
+ notification.add_tab(:rake_task, {
12
+ :name => task.name,
13
+ :description => task.full_comment,
14
+ :arguments => task.arg_description
15
+ })
16
+
17
+ notification.context ||= task.name
18
+ end
19
+
20
+ @bugsnag.call(notification)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module Bugsnag::Middleware
2
+ class Sidekiq
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ sidekiq = notification.request_data[:sidekiq]
9
+ notification.add_tab(:sidekiq, sidekiq) if sidekiq
10
+ @bugsnag.call(notification)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ module Bugsnag::Middleware
2
+ class WardenUser
3
+ SCOPE_PATTERN = /^warden\.user\.([^.]+)\.key$/
4
+ COMMON_USER_FIELDS = [:email, :name, :first_name, :last_name, :created_at, :id]
5
+
6
+ def initialize(bugsnag)
7
+ @bugsnag = bugsnag
8
+ end
9
+
10
+ def call(notification)
11
+ if notification.request_data[:rack_env] && notification.request_data[:rack_env]["warden"]
12
+ env = notification.request_data[:rack_env]
13
+ session = env["rack.session"] || {}
14
+
15
+ # Find all warden user scopes
16
+ warden_scopes = session.keys.select {|k| k.match(SCOPE_PATTERN)}.map {|k| k.gsub(SCOPE_PATTERN, '\1')}
17
+ unless warden_scopes.empty?
18
+ # Pick the best scope for unique id (the default is "user")
19
+ best_scope = warden_scopes.include?("user") ? "user" : warden_scopes.first
20
+
21
+ # Extract useful user information
22
+ user = {}
23
+ user_object = env["warden"].user({:scope => best_scope, :run_callbacks => false}) rescue nil
24
+ if user_object
25
+ # Build the user info for this scope
26
+ COMMON_USER_FIELDS.each do |field|
27
+ user[field] = user_object.send(field) if user_object.respond_to?(field)
28
+ end
29
+ end
30
+
31
+ # We merge the first warden scope down, so that it is the main "user" for the request
32
+ notification.user = user unless user.empty?
33
+ end
34
+ end
35
+
36
+ @bugsnag.call(notification)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,98 @@
1
+ module Bugsnag
2
+ class MiddlewareStack
3
+ def initialize
4
+ @middlewares = []
5
+ @disabled_middleware = []
6
+ @mutex = Mutex.new
7
+ end
8
+
9
+ def use(new_middleware)
10
+ @mutex.synchronize do
11
+ return if @disabled_middleware.include?(new_middleware)
12
+ return if @middlewares.include?(new_middleware)
13
+
14
+ @middlewares << new_middleware
15
+ end
16
+ end
17
+
18
+ def insert_after(after, new_middleware)
19
+ @mutex.synchronize do
20
+ return if @disabled_middleware.include?(new_middleware)
21
+ return if @middlewares.include?(new_middleware)
22
+
23
+ if after.is_a? Array
24
+ index = @middlewares.rindex {|el| after.include?(el)}
25
+ else
26
+ index = @middlewares.rindex(after)
27
+ end
28
+
29
+ if index.nil?
30
+ @middlewares << new_middleware
31
+ else
32
+ @middlewares.insert index + 1, new_middleware
33
+ end
34
+ end
35
+ end
36
+
37
+ def insert_before(before, new_middleware)
38
+ @mutex.synchronize do
39
+ return if @disabled_middleware.include?(new_middleware)
40
+ return if @middlewares.include?(new_middleware)
41
+
42
+ if before.is_a? Array
43
+ index = @middlewares.index {|el| before.include?(el)}
44
+ else
45
+ index = @middlewares.index(before)
46
+ end
47
+
48
+ @middlewares.insert index || @middlewares.length, new_middleware
49
+ end
50
+ end
51
+
52
+ def disable(*middlewares)
53
+ @mutex.synchronize do
54
+ @disabled_middleware += middlewares
55
+
56
+ @middlewares.delete_if {|m| @disabled_middleware.include?(m)}
57
+ end
58
+ end
59
+
60
+ # This allows people to proxy methods to the array if they want to do more complex stuff
61
+ def method_missing(method, *args, &block)
62
+ @middlewares.send(method, *args, &block)
63
+ end
64
+
65
+ # Runs the middleware stack and calls
66
+ def run(notification)
67
+ # The final lambda is the termination of the middleware stack. It calls deliver on the notification
68
+ lambda_has_run = false
69
+ notify_lambda = lambda do |notif|
70
+ lambda_has_run = true
71
+ yield if block_given?
72
+ end
73
+
74
+ begin
75
+ # We reverse them, so we can call "call" on the first middleware
76
+ middleware_procs.reverse.inject(notify_lambda) { |n,e| e.call(n) }.call(notification)
77
+ rescue StandardError => e
78
+ # KLUDGE: Since we don't re-raise middleware exceptions, this breaks rspec
79
+ raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError"
80
+
81
+ # We dont notify, as we dont want to loop forever in the case of really broken middleware, we will
82
+ # still send this notify
83
+ Bugsnag.warn "Bugsnag middleware error: #{e}"
84
+ Bugsnag.log "Middleware error stacktrace: #{e.backtrace.inspect}"
85
+ end
86
+
87
+ # Ensure that the deliver has been performed, and no middleware has botched it
88
+ notify_lambda.call(notification) unless lambda_has_run
89
+ end
90
+
91
+ private
92
+ # Generates a list of middleware procs that are ready to be run
93
+ # Pass each one a reference to the next in the queue
94
+ def middleware_procs
95
+ @middlewares.map{|middleware| proc { |next_middleware| middleware.new(next_middleware) } }
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,452 @@
1
+ require "json"
2
+
3
+ if RUBY_VERSION =~ /^1\.8/
4
+ begin
5
+ require "iconv"
6
+ rescue LoadError
7
+ end
8
+ end
9
+
10
+ require "pathname"
11
+
12
+ module Bugsnag
13
+ class Notification
14
+ NOTIFIER_NAME = "Ruby Bugsnag Notifier"
15
+ NOTIFIER_VERSION = Bugsnag::VERSION
16
+ NOTIFIER_URL = "http://www.bugsnag.com"
17
+
18
+ API_KEY_REGEX = /[0-9a-f]{32}/i
19
+
20
+ # e.g. "org/jruby/RubyKernel.java:1264:in `catch'"
21
+ BACKTRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$/
22
+
23
+ # e.g. "org.jruby.Ruby.runScript(Ruby.java:807)"
24
+ JAVA_BACKTRACE_REGEX = /^(.*)\((.*)(?::([0-9]+))?\)$/
25
+
26
+ MAX_EXCEPTIONS_TO_UNWRAP = 5
27
+
28
+ SUPPORTED_SEVERITIES = ["error", "warning", "info"]
29
+
30
+ CURRENT_PAYLOAD_VERSION = "2"
31
+
32
+ attr_accessor :context
33
+ attr_accessor :user
34
+ attr_accessor :configuration
35
+ attr_accessor :meta_data
36
+
37
+ class << self
38
+ def deliver_exception_payload(url, payload, configuration=Bugsnag.configuration, delivery_method=nil)
39
+
40
+ # If the payload is going to be too long, we trim the hashes to send
41
+ # a minimal payload instead
42
+ payload_string = ::JSON.dump(payload)
43
+ if payload_string.length > 128000
44
+ payload[:events].each {|e| e[:metaData] = Bugsnag::Helpers.reduce_hash_size(e[:metaData])}
45
+ payload_string = ::JSON.dump(payload)
46
+ end
47
+
48
+ Bugsnag::Delivery[delivery_method || configuration.delivery_method].deliver(url, payload_string, configuration)
49
+ end
50
+ end
51
+
52
+ def initialize(exception, configuration, overrides = nil, request_data = nil)
53
+ @configuration = configuration
54
+ @overrides = Bugsnag::Helpers.flatten_meta_data(overrides) || {}
55
+ @request_data = request_data
56
+ @meta_data = {}
57
+ @user = {}
58
+ @should_ignore = false
59
+
60
+ self.severity = @overrides[:severity]
61
+ @overrides.delete :severity
62
+
63
+ if @overrides.key? :grouping_hash
64
+ self.grouping_hash = @overrides[:grouping_hash]
65
+ @overrides.delete :grouping_hash
66
+ end
67
+
68
+ if @overrides.key? :api_key
69
+ self.api_key = @overrides[:api_key]
70
+ @overrides.delete :api_key
71
+ end
72
+
73
+ if @overrides.key? :delivery_method
74
+ @delivery_method = @overrides[:delivery_method]
75
+ @overrides.delete :delivery_method
76
+ end
77
+
78
+ # Unwrap exceptions
79
+ @exceptions = []
80
+
81
+ ex = exception
82
+ while ex != nil && !@exceptions.include?(ex) && @exceptions.length < MAX_EXCEPTIONS_TO_UNWRAP
83
+
84
+ unless ex.is_a? Exception
85
+ if ex.respond_to?(:to_exception)
86
+ ex = ex.to_exception
87
+ elsif ex.respond_to?(:exception)
88
+ ex = ex.exception
89
+ end
90
+ end
91
+
92
+ unless ex.is_a?(Exception) || (defined?(Java::JavaLang::Throwable) && ex.is_a?(Java::JavaLang::Throwable))
93
+ Bugsnag.warn("Converting non-Exception to RuntimeError: #{ex.inspect}")
94
+ ex = RuntimeError.new(ex.to_s)
95
+ ex.set_backtrace caller
96
+ end
97
+
98
+ @exceptions << ex
99
+
100
+ if ex.respond_to?(:cause) && ex.cause
101
+ ex = ex.cause
102
+ elsif ex.respond_to?(:continued_exception) && ex.continued_exception
103
+ ex = ex.continued_exception
104
+ elsif ex.respond_to?(:original_exception) && ex.original_exception
105
+ ex = ex.original_exception
106
+ else
107
+ ex = nil
108
+ end
109
+ end
110
+ end
111
+
112
+ # Add a single value as custom data, to this notification
113
+ def add_custom_data(name, value)
114
+ @meta_data[:custom] ||= {}
115
+ @meta_data[:custom][name.to_sym] = value
116
+ end
117
+
118
+ # Add a new tab to this notification
119
+ def add_tab(name, value)
120
+ return if name.nil?
121
+
122
+ if value.is_a? Hash
123
+ @meta_data[name.to_sym] ||= {}
124
+ @meta_data[name.to_sym].merge! value
125
+ else
126
+ self.add_custom_data(name, value)
127
+ Bugsnag.warn "Adding a tab requires a hash, adding to custom tab instead (name=#{name})"
128
+ end
129
+ end
130
+
131
+ # Remove a tab from this notification
132
+ def remove_tab(name)
133
+ return if name.nil?
134
+
135
+ @meta_data.delete(name.to_sym)
136
+ end
137
+
138
+ def user_id=(user_id)
139
+ @user[:id] = user_id
140
+ end
141
+
142
+ def user_id
143
+ @user[:id]
144
+ end
145
+
146
+ def user=(user = {})
147
+ return unless user.is_a? Hash
148
+ @user.merge!(user).delete_if{|k,v| v == nil}
149
+ end
150
+
151
+ def severity=(severity)
152
+ @severity = severity if SUPPORTED_SEVERITIES.include?(severity)
153
+ end
154
+
155
+ def severity
156
+ @severity || "warning"
157
+ end
158
+
159
+ def payload_version
160
+ CURRENT_PAYLOAD_VERSION
161
+ end
162
+
163
+ def grouping_hash=(grouping_hash)
164
+ @grouping_hash = grouping_hash
165
+ end
166
+
167
+ def grouping_hash
168
+ @grouping_hash || nil
169
+ end
170
+
171
+ def api_key=(api_key)
172
+ @api_key = api_key
173
+ end
174
+
175
+ def api_key
176
+ @api_key ||= @configuration.api_key
177
+ end
178
+
179
+ # Deliver this notification to bugsnag.com Also runs through the middleware as required.
180
+ def deliver
181
+ return unless @configuration.should_notify?
182
+
183
+ # Check we have at least an api_key
184
+ if api_key.nil?
185
+ Bugsnag.warn "No API key configured, couldn't notify"
186
+ return
187
+ elsif api_key !~ API_KEY_REGEX
188
+ Bugsnag.warn "Your API key (#{api_key}) is not valid, couldn't notify"
189
+ return
190
+ end
191
+
192
+ # Warn if no release_stage is set
193
+ Bugsnag.warn "You should set your app's release_stage (see https://bugsnag.com/docs/notifiers/ruby#release_stage)." unless @configuration.release_stage
194
+
195
+ @configuration.internal_middleware.run(self)
196
+
197
+ exceptions.each do |exception|
198
+ if exception.class.include?(Bugsnag::MetaData)
199
+ if exception.bugsnag_user_id.is_a?(String)
200
+ self.user_id = exception.bugsnag_user_id
201
+ end
202
+ if exception.bugsnag_context.is_a?(String)
203
+ self.context = exception.bugsnag_context
204
+ end
205
+ end
206
+ end
207
+
208
+ [:user_id, :context, :user, :grouping_hash].each do |symbol|
209
+ if @overrides[symbol]
210
+ self.send("#{symbol}=", @overrides[symbol])
211
+ @overrides.delete symbol
212
+ end
213
+ end
214
+
215
+ # make meta_data available to public middleware
216
+ @meta_data = generate_meta_data(@exceptions, @overrides)
217
+
218
+ # Run the middleware here (including Bugsnag::Middleware::Callbacks)
219
+ # at the end of the middleware stack, execute the actual notification delivery
220
+ @configuration.middleware.run(self) do
221
+ # This supports self.ignore! for before_notify_callbacks.
222
+ return if @should_ignore
223
+
224
+ # Build the endpoint url
225
+ endpoint = (@configuration.use_ssl ? "https://" : "http://") + @configuration.endpoint
226
+ Bugsnag.log("Notifying #{endpoint} of #{@exceptions.last.class} from api_key #{api_key}")
227
+
228
+ # Deliver the payload
229
+ self.class.deliver_exception_payload(endpoint, build_exception_payload, @configuration, @delivery_method)
230
+ end
231
+ end
232
+
233
+ # Build an exception payload
234
+ def build_exception_payload
235
+ # Build the payload's exception event
236
+ payload_event = {
237
+ :app => {
238
+ :version => @configuration.app_version,
239
+ :releaseStage => @configuration.release_stage,
240
+ :type => @configuration.app_type
241
+ },
242
+ :context => self.context,
243
+ :user => @user,
244
+ :payloadVersion => payload_version,
245
+ :exceptions => exception_list,
246
+ :severity => self.severity,
247
+ :groupingHash => self.grouping_hash,
248
+ }
249
+
250
+ payload_event[:device] = {:hostname => @configuration.hostname} if @configuration.hostname
251
+
252
+ # cleanup character encodings
253
+ payload_event = Bugsnag::Helpers.cleanup_obj_encoding(payload_event)
254
+
255
+ # filter out sensitive values in (and cleanup encodings) metaData
256
+ payload_event[:metaData] = Bugsnag::Helpers.cleanup_obj(@meta_data, @configuration.params_filters)
257
+ payload_event.reject! {|k,v| v.nil? }
258
+
259
+ # return the payload hash
260
+ {
261
+ :apiKey => api_key,
262
+ :notifier => {
263
+ :name => NOTIFIER_NAME,
264
+ :version => NOTIFIER_VERSION,
265
+ :url => NOTIFIER_URL
266
+ },
267
+ :events => [payload_event]
268
+ }
269
+ end
270
+
271
+ def ignore?
272
+ @should_ignore || ignore_exception_class? || ignore_user_agent?
273
+ end
274
+
275
+ def request_data
276
+ @request_data || Bugsnag.configuration.request_data
277
+ end
278
+
279
+ def exceptions
280
+ @exceptions
281
+ end
282
+
283
+ def ignore!
284
+ @should_ignore = true
285
+ end
286
+
287
+ private
288
+
289
+ def ignore_exception_class?
290
+ @exceptions.any? do |ex|
291
+ ancestor_chain = ex.class.ancestors.select { |ancestor| ancestor.is_a?(Class) }.map { |ancestor| error_class(ancestor) }.to_set
292
+
293
+ @configuration.ignore_classes.any? do |to_ignore|
294
+ to_ignore.is_a?(Proc) ? to_ignore.call(ex) : ancestor_chain.include?(to_ignore)
295
+ end
296
+ end
297
+ end
298
+
299
+ def ignore_user_agent?
300
+ if @configuration.request_data && @configuration.request_data[:rack_env] && (agent = @configuration.request_data[:rack_env]["HTTP_USER_AGENT"])
301
+ @configuration.ignore_user_agents.any? do |to_ignore|
302
+ agent =~ to_ignore
303
+ end
304
+ end
305
+ end
306
+
307
+ # Generate the meta data from both the request configuration, the overrides and the exceptions for this notification
308
+ def generate_meta_data(exceptions, overrides)
309
+ # Copy the request meta data so we dont edit it by mistake
310
+ meta_data = @meta_data.dup
311
+
312
+ exceptions.each do |exception|
313
+ if exception.respond_to?(:bugsnag_meta_data) && exception.bugsnag_meta_data
314
+ exception.bugsnag_meta_data.each do |key, value|
315
+ add_to_meta_data key, value, meta_data
316
+ end
317
+ end
318
+ end
319
+
320
+ overrides.each do |key, value|
321
+ add_to_meta_data key, value, meta_data
322
+ end
323
+
324
+ meta_data
325
+ end
326
+
327
+ def add_to_meta_data(key, value, meta_data)
328
+ # If its a hash, its a tab so we can just add it providing its not reserved
329
+ if value.is_a? Hash
330
+ key = key.to_sym
331
+
332
+ if meta_data[key]
333
+ # If its a clash, merge with the existing data
334
+ meta_data[key].merge! value
335
+ else
336
+ # Add it as is if its not special
337
+ meta_data[key] = value
338
+ end
339
+ else
340
+ meta_data[:custom] ||= {}
341
+ meta_data[:custom][key] = value
342
+ end
343
+ end
344
+
345
+ def exception_list
346
+ @exceptions.map do |exception|
347
+ {
348
+ :errorClass => error_class(exception),
349
+ :message => exception.message,
350
+ :stacktrace => stacktrace(exception)
351
+ }
352
+ end
353
+ end
354
+
355
+ def error_class(exception)
356
+ # The "Class" check is for some strange exceptions like Timeout::Error
357
+ # which throw the error class instead of an instance
358
+ (exception.is_a? Class) ? exception.name : exception.class.name
359
+ end
360
+
361
+ def stacktrace(exception)
362
+ (exception.backtrace || caller).map do |trace|
363
+
364
+ if trace.match(BACKTRACE_LINE_REGEX)
365
+ file, line_str, method = [$1, $2, $3]
366
+ elsif trace.match(JAVA_BACKTRACE_REGEX)
367
+ method, file, line_str = [$1, $2, $3]
368
+ end
369
+
370
+ # Parse the stacktrace line
371
+
372
+ # Skip stacktrace lines inside lib/bugsnag
373
+ next(nil) if file.nil? || file =~ %r{lib/bugsnag(/|\.rb)}
374
+
375
+ # Expand relative paths
376
+ p = Pathname.new(file)
377
+ if p.relative?
378
+ file = p.realpath.to_s rescue file
379
+ end
380
+
381
+ # Generate the stacktrace line hash
382
+ trace_hash = {}
383
+ trace_hash[:inProject] = true if @configuration.project_root && file.match(/^#{@configuration.project_root}/) && !file.match(/vendor\//)
384
+ trace_hash[:lineNumber] = line_str.to_i
385
+
386
+ if @configuration.send_code
387
+ trace_hash[:code] = code(file, trace_hash[:lineNumber])
388
+ end
389
+
390
+ # Clean up the file path in the stacktrace
391
+ if defined?(Bugsnag.configuration.project_root) && Bugsnag.configuration.project_root.to_s != ''
392
+ file.sub!(/#{Bugsnag.configuration.project_root}\//, "")
393
+ end
394
+
395
+ # Strip common gem path prefixes
396
+ if defined?(Gem)
397
+ file = Gem.path.inject(file) {|line, path| line.sub(/#{path}\//, "") }
398
+ end
399
+
400
+ trace_hash[:file] = file
401
+
402
+ # Add a method if we have it
403
+ trace_hash[:method] = method if method && (method =~ /^__bind/).nil?
404
+
405
+ if trace_hash[:file] && !trace_hash[:file].empty?
406
+ trace_hash
407
+ else
408
+ nil
409
+ end
410
+ end.compact
411
+ end
412
+
413
+ def code(file, line_number, num_lines = 7)
414
+ code_hash = {}
415
+
416
+ from_line = [line_number - num_lines, 1].max
417
+
418
+ # don't try and open '(irb)' or '-e'
419
+ return unless File.exist?(file)
420
+
421
+ # Populate code hash with line numbers and code lines
422
+ File.open(file) do |f|
423
+ current_line_number = 0
424
+ f.each_line do |line|
425
+ current_line_number += 1
426
+
427
+ next if current_line_number < from_line
428
+
429
+ code_hash[current_line_number] = line[0...200].rstrip
430
+
431
+ break if code_hash.length >= ( num_lines * 1.5 ).ceil
432
+ end
433
+ end
434
+
435
+ while code_hash.length > num_lines
436
+ last_line = code_hash.keys.max
437
+ first_line = code_hash.keys.min
438
+
439
+ if (last_line - line_number) > (line_number - first_line)
440
+ code_hash.delete(last_line)
441
+ else
442
+ code_hash.delete(first_line)
443
+ end
444
+ end
445
+
446
+ code_hash
447
+ rescue
448
+ Bugsnag.warn("Error fetching code: #{$!.inspect}")
449
+ nil
450
+ end
451
+ end
452
+ end