bugsnag-maglev- 2.8.12

Sign up to get free protection for your applications and to get access to all the features.
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