diff --git a/CHANGELOG.md b/CHANGELOG.md index be53a6bef..4c416d366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ Changelog ========= +## 6.15.0 (27 July 2020) + +### Enhancements + +* Add `on_error` callbacks to replace `before_notify_callbacks` + | [#608](https://github.com/bugsnag/bugsnag-ruby/pull/608) + +* Improve performance when extracting code from files in stacktraces + | [#604](https://github.com/bugsnag/bugsnag-ruby/pull/604) + +* Reduce memory use when session tracking is disabled + | [#606](https://github.com/bugsnag/bugsnag-ruby/pull/606) + +### Deprecated + +* `before_notify_callbacks` have been deprecated in favour of `on_error` and will be removed in the next major release + ## 6.14.0 (20 July 2020) ### Enhancements diff --git a/VERSION b/VERSION index 68390495f..a3748c562 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.14.0 +6.15.0 diff --git a/example/padrino/app/app.rb b/example/padrino/app/app.rb index 3630647ef..17ba9878b 100644 --- a/example/padrino/app/app.rb +++ b/example/padrino/app/app.rb @@ -75,21 +75,6 @@ class App < Padrino::Application 'bugsnag.com for a new notification!') end - get '/crash_with_callback' do - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Padrino demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - - msg = 'Bugsnag Padrino demo says: It crashed! But, due to the attached callback' + - ' the exception has meta information. Go check' + - ' bugsnag.com for a new notification (see the Diagnostics tab)!' - raise RuntimeError.new(msg) - end - get '/notify' do Bugsnag.notify(RuntimeError.new("Bugsnag Padrino demo says: False alarm, your application didn't crash")) @@ -100,21 +85,16 @@ class App < Padrino::Application get '/notify_data' do error = RuntimeError.new("Bugsnag Padrino demo says: False alarm, your application didn't crash") - Bugsnag.notify error do |report| - report.add_tab(:user, { - :username => "bob-hoskins", - :email => 'bugsnag@bugsnag.com', - :registered_user => true - }) + Bugsnag.notify(error) do |report| report.add_tab(:diagnostics, { - :message => 'Padrino demo says: Everything is great', - :code => 200 + message: 'Padrino demo says: Everything is great', + code: 200 }) end "Bugsnag Padrino demo says: It didn't crash! " + 'But still go check https://bugsnag.com' + - ' for a new notification. Check out the User tab for the meta data' + ' for a new notification. Check out the Diagnostics tab for the meta data' end get '/notify_severity' do diff --git a/example/padrino/app/templates/index.md b/example/padrino/app/templates/index.md index 4f2d4be06..c2151a982 100644 --- a/example/padrino/app/templates/index.md +++ b/example/padrino/app/templates/index.md @@ -8,18 +8,14 @@ While testing the examples open [your dashboard](https://app.bugsnag.com) in ord
Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/crash_with_callback) -
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify](/notify) +2. [Notify](/notify)
Sends Bugsnag a report on demand using `bugsnag.notify`. Allows details of handled errors or information to be sent to the Bugsnag dashboard without crashing your code. -4. [Notify with data](/notify_data) +3. [Notify with data](/notify_data)
- Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the user to go into the `user` tab, and additional diagnostics as a `diagnostics` tab. + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding additional diagnostics as a `diagnostics` tab. -5. [Set the severity](/notify_severity) +4. [Set the severity](/notify_severity)
- This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. \ No newline at end of file + This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. diff --git a/example/padrino/config/boot.rb b/example/padrino/config/boot.rb index 54e54347c..1af233fa7 100644 --- a/example/padrino/config/boot.rb +++ b/example/padrino/config/boot.rb @@ -38,6 +38,14 @@ Padrino.before_load do Bugsnag.configure do |config| config.api_key = 'YOUR_API_KEY' + + config.add_on_error(proc do |report| + report.add_tab(:user, { + username: 'bob-hoskins', + email: 'bugsnag@bugsnag.com', + registered_user: true + }) + end) end end diff --git a/example/rack/server.rb b/example/rack/server.rb index c02887894..19674730e 100644 --- a/example/rack/server.rb +++ b/example/rack/server.rb @@ -5,9 +5,16 @@ require 'bugsnag' require 'redcarpet' - Bugsnag.configure do |config| config.api_key = 'YOUR_API_KEY' + + config.add_on_error(proc do |report| + report.add_tab(:user, { + username: 'bob-hoskins', + email: 'bugsnag@bugsnag.com', + registered_user: true + }) + end) end class BugsnagDemo @@ -18,19 +25,6 @@ def call(env) when '/crash' raise RuntimeError.new('Bugsnag Rack demo says: It crashed! Go check ' + 'bugsnag.com for a new notification!') - when '/crash_with_callback' - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Rack demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - - msg = 'Bugsnag Rack demo says: It crashed! But, due to the attached callback' + - ' the exception has meta information. Go check' + - ' bugsnag.com for a new notification (see the Diagnostics tab)!' - raise RuntimeError.new(msg) when '/notify' Bugsnag.notify(RuntimeError.new("Bugsnag Rack demo says: False alarm, your application didn't crash")) @@ -39,21 +33,16 @@ def call(env) ' for a new notification.' when '/notify_data' error = RuntimeError.new("Bugsnag Rack demo says: False alarm, your application didn't crash") - Bugsnag.notify error do |report| - report.add_tab(:user, { - :username => "bob-hoskins", - :email => 'bugsnag@bugsnag.com', - :registered_user => true - }) + Bugsnag.notify(error) do |report| report.add_tab(:diagnostics, { - :message => 'Rack demo says: Everything is great', - :code => 200 + message: 'Padrino demo says: Everything is great', + code: 200 }) end text = "Bugsnag Rack demo says: It didn't crash! " + 'But still go check https://bugsnag.com' + - ' for a new notification. Check out the User tab for the meta data' + ' for a new notification. Check out the Diagnostics tab for the meta data' when '/notify_severity' msg = "Bugsnag Rack demo says: Look at the circle on the right side. It's different" error = RuntimeError.new(msg) diff --git a/example/rack/templates/index.md b/example/rack/templates/index.md index 52695efe2..6e111829b 100644 --- a/example/rack/templates/index.md +++ b/example/rack/templates/index.md @@ -8,18 +8,14 @@ While testing the examples open [your dashboard](https://app.bugsnag.com) in ord
Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/crash_with_callback) -
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify](/notify) +2. [Notify](/notify)
Sends Bugsnag a report on demand using `bugsnag.notify`. Allows details of handled errors or information to be sent to the Bugsnag dashboard without crashing your code. -4. [Notify with data](/notify_data) +3. [Notify with data](/notify_data)
- Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the user to go into the `user` tab, and additional diagnostics as a `diagnostics` tab. + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding additional diagnostics as a `diagnostics` tab. -5. [Set the severity](/notify_severity) +4. [Set the severity](/notify_severity)
- This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. \ No newline at end of file + This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. diff --git a/example/rails-42/app/controllers/application_controller.rb b/example/rails-42/app/controllers/application_controller.rb index 33f2701ff..6d100c60f 100644 --- a/example/rails-42/app/controllers/application_controller.rb +++ b/example/rails-42/app/controllers/application_controller.rb @@ -10,19 +10,6 @@ def crash 'bugsnag.com for a new notification') end - def callback - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Rails v4.2 demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - raise RuntimeError.new('Bugsnag Rails demo says: It crashed! But, due to the attached callback' + - ' the exception has meta information. Go check' + - ' bugsnag.com for a new notification (see the Diagnostics tab)!') - end - def notify Bugsnag.notify(RuntimeError.new("Bugsnag Rails demo says: False alarm, your application didn't crash")) @text = "Bugsnag Rack demo says: It didn't crash! " + @@ -32,20 +19,15 @@ def notify def data error = RuntimeError.new("Bugsnag Rails demo says: False alarm, your application didn't crash") - Bugsnag.notify error do |report| - report.add_tab(:user, { - :username => "bob-hoskins", - :email => 'bugsnag@bugsnag.com', - :registered_user => true - }) + Bugsnag.notify(error) do |report| report.add_tab(:diagnostics, { - :message => 'Rails demo says: Everything is great', - :code => 200 + message: 'Rails demo says: Everything is great', + code: 200 }) end @text = "Bugsnag Rails demo says: It didn't crash! " + 'But still go check https://bugsnag.com' + - ' for a new notification. Check out the User tab for the meta data' + ' for a new notification. Check out the Diagnostics tab for the meta data' end def severity diff --git a/example/rails-42/app/views/index.md b/example/rails-42/app/views/index.md index 4b37c029c..5595a3980 100644 --- a/example/rails-42/app/views/index.md +++ b/example/rails-42/app/views/index.md @@ -8,18 +8,14 @@ While testing the examples open [your dashboard](https://app.bugsnag.com) in ord
Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/crash_with_callback) -
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify](/notify) +2. [Notify](/notify)
Sends Bugsnag a report on demand using `bugsnag.notify`. Allows details of handled errors or information to be sent to the Bugsnag dashboard without crashing your code. -4. [Notify with data](/notify_data) +3. [Notify with data](/notify_data)
- Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the user to go into the `user` tab, and additional diagnostics as a `diagnostics` tab. + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding additional diagnostics as a `diagnostics` tab. -5. [Set the severity](/notify_severity) +4. [Set the severity](/notify_severity)
- This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. \ No newline at end of file + This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. diff --git a/example/rails-42/config/initializers/bugsnag.rb b/example/rails-42/config/initializers/bugsnag.rb index 58c57fbf6..2b02bc66a 100644 --- a/example/rails-42/config/initializers/bugsnag.rb +++ b/example/rails-42/config/initializers/bugsnag.rb @@ -1,3 +1,11 @@ Bugsnag.configure do |config| config.api_key = "YOUR_API_KEY_HERE" + + config.add_on_error(proc do |report| + report.add_tab(:user, { + username: 'bob-hoskins', + email: 'bugsnag@bugsnag.com', + registered_user: true + }) + end) end diff --git a/example/rails-42/config/routes.rb b/example/rails-42/config/routes.rb index 9a3a1b9f0..70fe670cc 100644 --- a/example/rails-42/config/routes.rb +++ b/example/rails-42/config/routes.rb @@ -1,10 +1,8 @@ Rails.application.routes.draw do root :to => 'application#index' - + get 'crash' => 'application#crash' - get 'crash_with_callback' => 'application#callback' get 'notify' => 'application#notify' get 'notify_data' => 'application#data' get 'notify_severity' => 'application#severity' - end diff --git a/example/rails-51/README.md b/example/rails-51/README.md index f4bd52f94..f3bf4fbca 100644 --- a/example/rails-51/README.md +++ b/example/rails-51/README.md @@ -132,5 +132,5 @@ Navigate to the `/resque` page and queue any of the examples using links provide To process the queues, run the `resque:work` task as stated in the example webpage. In order to process any of the queues on a single thread start the resque worker using the command: ```shell -QUEUE=crash,callback,metadata bundle exec rake resque:work -``` \ No newline at end of file +QUEUE=crash,metadata bundle exec rake resque:work +``` diff --git a/example/rails-51/app/controllers/application_controller.rb b/example/rails-51/app/controllers/application_controller.rb index 33f2701ff..6d100c60f 100644 --- a/example/rails-51/app/controllers/application_controller.rb +++ b/example/rails-51/app/controllers/application_controller.rb @@ -10,19 +10,6 @@ def crash 'bugsnag.com for a new notification') end - def callback - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Rails v4.2 demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - raise RuntimeError.new('Bugsnag Rails demo says: It crashed! But, due to the attached callback' + - ' the exception has meta information. Go check' + - ' bugsnag.com for a new notification (see the Diagnostics tab)!') - end - def notify Bugsnag.notify(RuntimeError.new("Bugsnag Rails demo says: False alarm, your application didn't crash")) @text = "Bugsnag Rack demo says: It didn't crash! " + @@ -32,20 +19,15 @@ def notify def data error = RuntimeError.new("Bugsnag Rails demo says: False alarm, your application didn't crash") - Bugsnag.notify error do |report| - report.add_tab(:user, { - :username => "bob-hoskins", - :email => 'bugsnag@bugsnag.com', - :registered_user => true - }) + Bugsnag.notify(error) do |report| report.add_tab(:diagnostics, { - :message => 'Rails demo says: Everything is great', - :code => 200 + message: 'Rails demo says: Everything is great', + code: 200 }) end @text = "Bugsnag Rails demo says: It didn't crash! " + 'But still go check https://bugsnag.com' + - ' for a new notification. Check out the User tab for the meta data' + ' for a new notification. Check out the Diagnostics tab for the meta data' end def severity diff --git a/example/rails-51/app/controllers/que_controller.rb b/example/rails-51/app/controllers/que_controller.rb index a8de7ceb2..5b3db72b1 100644 --- a/example/rails-51/app/controllers/que_controller.rb +++ b/example/rails-51/app/controllers/que_controller.rb @@ -11,9 +11,4 @@ def crash QueCrash.enqueue @text = "Que has queued the crash task" end - - def callbacks - QueCallback.enqueue - @text = "Que has queued the callbacks task" - end end diff --git a/example/rails-51/app/controllers/resque_controller.rb b/example/rails-51/app/controllers/resque_controller.rb index c0eb75595..420a13fb1 100644 --- a/example/rails-51/app/controllers/resque_controller.rb +++ b/example/rails-51/app/controllers/resque_controller.rb @@ -16,9 +16,4 @@ def metadata Resque.enqueue(ResqueWorkers::Metadata) @text = "The metadata task has been queued. This can be run using the `QUEUE=metadata bundle exec rake resque:work` command" end - - def callbacks - Resque.enqueue(ResqueWorkers::Callback) - @text = "The callback task has been queued. This can be run using the `QUEUE=callback bundle exec rake resque:work` command" - end end diff --git a/example/rails-51/app/controllers/sidekiq_controller.rb b/example/rails-51/app/controllers/sidekiq_controller.rb index 2b814ab82..2e1ee5fa7 100644 --- a/example/rails-51/app/controllers/sidekiq_controller.rb +++ b/example/rails-51/app/controllers/sidekiq_controller.rb @@ -17,9 +17,4 @@ def metadata @text = "Sidekiq is performing a task that will notify an error with some metadata without crashing. Check out your dashboard!" end - - def callbacks - SidekiqWorkers::CallbackWorker.perform_async - @text = "Sidekiq is performing a task that will crash, but registers a callback before this to attach additional metadata." - end end diff --git a/example/rails-51/app/jobs/que_callback.rb b/example/rails-51/app/jobs/que_callback.rb deleted file mode 100644 index 55032c9d0..000000000 --- a/example/rails-51/app/jobs/que_callback.rb +++ /dev/null @@ -1,14 +0,0 @@ -class QueCallback < Que::Job - @run_at = proc { 5.seconds.from_now } - - def run(options={}) - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Que demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - raise "Oh no" - end -end diff --git a/example/rails-51/app/views/index.md b/example/rails-51/app/views/index.md index 5edbc9eb5..0f2db4363 100644 --- a/example/rails-51/app/views/index.md +++ b/example/rails-51/app/views/index.md @@ -14,18 +14,14 @@ While testing the examples open [your dashboard](https://app.bugsnag.com) in ord
Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/crash_with_callback) -
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify](/notify) +2. [Notify](/notify)
Sends Bugsnag a report on demand using `bugsnag.notify`. Allows details of handled errors or information to be sent to the Bugsnag dashboard without crashing your code. -4. [Notify with data](/notify_data) +3. [Notify with data](/notify_data)
- Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the user to go into the `user` tab, and additional diagnostics as a `diagnostics` tab. + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding additional diagnostics as a `diagnostics` tab. -5. [Set the severity](/notify_severity) +4. [Set the severity](/notify_severity)
- This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. \ No newline at end of file + This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. diff --git a/example/rails-51/app/views/que.md b/example/rails-51/app/views/que.md index 6272fd7b8..805bcb185 100644 --- a/example/rails-51/app/views/que.md +++ b/example/rails-51/app/views/que.md @@ -9,7 +9,3 @@ Make sure you have a PostgreSQL instance running that your test application can 1. [Crash](/que/crash)
Raises an error within the framework, generating a report in the Bugsnag dashboard. - -2. [Crash and use callbacks](/que/crash_with_callback) -
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. \ No newline at end of file diff --git a/example/rails-51/app/views/que/callbacks.html.erb b/example/rails-51/app/views/que/callbacks.html.erb deleted file mode 100644 index aa2786a7d..000000000 --- a/example/rails-51/app/views/que/callbacks.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= markdown(@text)%> \ No newline at end of file diff --git a/example/rails-51/app/views/resque.md b/example/rails-51/app/views/resque.md index b4ed7b0a3..221a821a3 100644 --- a/example/rails-51/app/views/resque.md +++ b/example/rails-51/app/views/resque.md @@ -9,17 +9,13 @@ Make sure you have a Redis instance running that your test application can conne While each queue can be run individually, to run Resque so that it automatically executes each task, use: ```shell -QUEUE=crash,callback,metadata bundle exec rake resque:work +QUEUE=crash,metadata bundle exec rake resque:work ``` 1. [Crash](/resque/crash)
Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/resque/crash_with_callback) +2. [Notify with data](/resque/notify_data)
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify with data](/resque/notify_data) -
- Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the queue to go into the `queue` tab, and additional diagnostics as a `diagnostics` tab. \ No newline at end of file + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding information about the queue to go into the `queue` tab, and additional diagnostics as a `diagnostics` tab. diff --git a/example/rails-51/app/views/resque/callbacks.html.erb b/example/rails-51/app/views/resque/callbacks.html.erb deleted file mode 100644 index aa2786a7d..000000000 --- a/example/rails-51/app/views/resque/callbacks.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= markdown(@text)%> \ No newline at end of file diff --git a/example/rails-51/app/views/sidekiq.md b/example/rails-51/app/views/sidekiq.md index 65edd291b..ace8f64ac 100644 --- a/example/rails-51/app/views/sidekiq.md +++ b/example/rails-51/app/views/sidekiq.md @@ -10,10 +10,6 @@ Make sure you have a Redis instance running that your test application can conne
Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/sidekiq/crash_with_callback) +2. [Notify with data](/sidekiq/notify_data)
- Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify with data](/sidekiq/notify_data) -
- Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the function being called to go into the `function` tab, and additional diagnostics as a `diagnostics` tab. \ No newline at end of file + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding information about the function being called to go into the `function` tab, and additional diagnostics as a `diagnostics` tab. diff --git a/example/rails-51/app/views/sidekiq/callbacks.html.erb b/example/rails-51/app/views/sidekiq/callbacks.html.erb deleted file mode 100644 index aa2786a7d..000000000 --- a/example/rails-51/app/views/sidekiq/callbacks.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= markdown(@text)%> \ No newline at end of file diff --git a/example/rails-51/app/workers/resque_workers.rb b/example/rails-51/app/workers/resque_workers.rb index 327a5984e..9891db8bc 100644 --- a/example/rails-51/app/workers/resque_workers.rb +++ b/example/rails-51/app/workers/resque_workers.rb @@ -7,22 +7,6 @@ def self.perform end end - # Unhandled with callback Exception example - class Callback - @queue = :callback - - def self.perform - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Resque demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - raise Exception.new "Crashed - Check the Bugsnag dashboard for diagnostic data" - end - end - # Handled example with additional data class Metadata @queue = :metadata @@ -41,4 +25,4 @@ def self.perform puts "The Resque worker hasn't crashed, but it has sent a notification, with additional data to the dashboard" end end -end \ No newline at end of file +end diff --git a/example/rails-51/app/workers/sidekiq_workers.rb b/example/rails-51/app/workers/sidekiq_workers.rb index 3aa06d2ce..8ced0c6a8 100644 --- a/example/rails-51/app/workers/sidekiq_workers.rb +++ b/example/rails-51/app/workers/sidekiq_workers.rb @@ -1,20 +1,4 @@ module SidekiqWorkers - class CallbackWorker - include Sidekiq::Worker - sidekiq_options :retry => false - - def perform - Bugsnag.before_notify_callbacks << proc { |report| - new_tab = { - message: 'Sidekiq demo says: Everything is great', - code: 200 - } - report.add_tab(:diagnostics, new_tab) - } - raise Exception.new "Sidekiq crashed, but the callback added metadata - Check your Bugsnag dashboard" - end - end - class CrashWorker include Sidekiq::Worker sidekiq_options :retry => false diff --git a/example/rails-51/config/initializers/bugsnag.rb b/example/rails-51/config/initializers/bugsnag.rb index 58c57fbf6..2b02bc66a 100644 --- a/example/rails-51/config/initializers/bugsnag.rb +++ b/example/rails-51/config/initializers/bugsnag.rb @@ -1,3 +1,11 @@ Bugsnag.configure do |config| config.api_key = "YOUR_API_KEY_HERE" + + config.add_on_error(proc do |report| + report.add_tab(:user, { + username: 'bob-hoskins', + email: 'bugsnag@bugsnag.com', + registered_user: true + }) + end) end diff --git a/example/rails-51/config/routes.rb b/example/rails-51/config/routes.rb index 1bb035f6a..bdee80b99 100644 --- a/example/rails-51/config/routes.rb +++ b/example/rails-51/config/routes.rb @@ -2,7 +2,6 @@ # Vanilla rails routing get '/', to: 'application#index' get '/crash', to: 'application#crash' - get '/crash_with_callback', to: 'application#callback' get '/notify', to: 'application#notify' get '/notify_data', to: 'application#data' get '/notify_severity', to: 'application#severity' @@ -11,16 +10,13 @@ get '/sidekiq', to: 'sidekiq#index' get '/sidekiq/crash', to: 'sidekiq#crash' get '/sidekiq/notify_data', to: 'sidekiq#metadata' - get '/sidekiq/crash_with_callback', to: 'sidekiq#callbacks' # Que routing get '/que', to: 'que#index' get '/que/crash', to: 'que#crash' get '/que/notify_data', to: 'que#metadata' - get '/que/crash_with_callback', to: 'que#callbacks' # Resque routing get '/resque', to: 'resque#index' get '/resque/crash', to: 'resque#crash' - get '/resque/crash_with_callback', to: 'resque#callbacks' end diff --git a/example/rails-60/README.md b/example/rails-60/README.md index 2eaa10f01..86b66e508 100644 --- a/example/rails-60/README.md +++ b/example/rails-60/README.md @@ -133,5 +133,5 @@ Navigate to the `/resque` page and queue any of the examples using links provide To process the queues, run the `resque:work` task as stated in the example webpage. In order to process any of the queues on a single thread start the resque worker using the command: ```shell -QUEUE=crash,callback,metadata bundle exec rake resque:work +QUEUE=crash,metadata bundle exec rake resque:work ``` diff --git a/example/rails-60/app/controllers/application_controller.rb b/example/rails-60/app/controllers/application_controller.rb index 4be69567f..1f7a9f073 100644 --- a/example/rails-60/app/controllers/application_controller.rb +++ b/example/rails-60/app/controllers/application_controller.rb @@ -4,19 +4,6 @@ def crash 'Go check bugsnag.com for a new notification' end - def callback - Bugsnag.before_notify_callbacks << proc { |report| - report.add_tab(:diagnostics, { - message: 'Rails v6.0 demo says: Everything is great', - code: 200 - }) - } - - raise 'Bugsnag Rails demo says: It crashed! But, due to the attached callback' \ - 'the exception has meta information. Go check ' \ - 'bugsnag.com for a new notification (see the Diagnostics tab)!' - end - def notify Bugsnag.notify(RuntimeError.new("Bugsnag Rails demo says: False alarm, your application didn't crash")) end @@ -25,12 +12,6 @@ def data error = RuntimeError.new("Bugsnag Rails demo says: False alarm, your application didn't crash") Bugsnag.notify(error) do |report| - report.add_tab(:user, { - username: 'bob-hoskins', - email: 'bugsnag@bugsnag.com', - registered_user: true - }) - report.add_tab(:diagnostics, { message: 'Rails demo says: Everything is great', code: 200 diff --git a/example/rails-60/app/controllers/que_controller.rb b/example/rails-60/app/controllers/que_controller.rb index eb7529196..00400cce5 100644 --- a/example/rails-60/app/controllers/que_controller.rb +++ b/example/rails-60/app/controllers/que_controller.rb @@ -1,5 +1,4 @@ require './app/jobs/que_crash' -require './app/jobs/que_callback' class QueController < ActionController::Base layout "application" @@ -7,8 +6,4 @@ class QueController < ActionController::Base def crash QueCrash.enqueue end - - def callbacks - QueCallback.enqueue - end end diff --git a/example/rails-60/app/controllers/resque_controller.rb b/example/rails-60/app/controllers/resque_controller.rb index 2bdea9196..8dc1d2534 100644 --- a/example/rails-60/app/controllers/resque_controller.rb +++ b/example/rails-60/app/controllers/resque_controller.rb @@ -8,8 +8,4 @@ def crash def metadata Resque.enqueue(ResqueWorkers::Metadata) end - - def callbacks - Resque.enqueue(ResqueWorkers::Callback) - end end diff --git a/example/rails-60/app/controllers/sidekiq_controller.rb b/example/rails-60/app/controllers/sidekiq_controller.rb index b925be414..80135f81c 100644 --- a/example/rails-60/app/controllers/sidekiq_controller.rb +++ b/example/rails-60/app/controllers/sidekiq_controller.rb @@ -10,8 +10,4 @@ def crash def metadata SidekiqWorkers::MetadataWorker.perform_async end - - def callbacks - SidekiqWorkers::CallbackWorker.perform_async - end end diff --git a/example/rails-60/app/jobs/que_callback.rb b/example/rails-60/app/jobs/que_callback.rb deleted file mode 100644 index 34efe9a48..000000000 --- a/example/rails-60/app/jobs/que_callback.rb +++ /dev/null @@ -1,14 +0,0 @@ -class QueCallback < Que::Job - @run_at = proc { 5.seconds.from_now } - - def run(_options = {}) - Bugsnag.before_notify_callbacks << proc { |report| - report.add_tab(:diagnostics, { - message: 'Que demo says: Everything is great', - code: 200 - }) - } - - raise 'Oh no!' - end -end diff --git a/example/rails-60/app/views/application/data.html.erb b/example/rails-60/app/views/application/data.html.erb index 3c050280e..e79a6e076 100644 --- a/example/rails-60/app/views/application/data.html.erb +++ b/example/rails-60/app/views/application/data.html.erb @@ -5,6 +5,6 @@ for a new notification.

-

Check out the user tab to see the metadata.

+

Check out the diagnostics tab to see the metadata.

← return to examples

diff --git a/example/rails-60/app/views/application/index.html.erb b/example/rails-60/app/views/application/index.html.erb index 098615448..ce506e105 100644 --- a/example/rails-60/app/views/application/index.html.erb +++ b/example/rails-60/app/views/application/index.html.erb @@ -23,15 +23,6 @@

-
  • -

    Crash and use callbacks

    -

    - Raises an exception within the framework, but with additional data - attached to the report. By registering a callback before the error - occurs useful data can be attached as a tab in the Bugsnag dashboard. -

    -
  • -
  • Notify

    @@ -45,12 +36,10 @@

    Notify with data

    Same as notify but allows you to attach additional data - within a block, similar to the - before_notify_callbacks example above. + within a block.

    - In this case we're adding information about the user to go into the - user tab, and additional diagnostics as a + In this case we're adding additional diagnostics as a diagnostics tab.

  • diff --git a/example/rails-60/app/views/que/callbacks.html.erb b/example/rails-60/app/views/que/callbacks.html.erb deleted file mode 100644 index d7e5095f1..000000000 --- a/example/rails-60/app/views/que/callbacks.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -

    Que

    - -

    - Que has queued the callbacks task, check - your Bugsnag dashboard for the result! -

    - -

    ← return to Que examples

    diff --git a/example/rails-60/app/views/que/index.html.erb b/example/rails-60/app/views/que/index.html.erb index 2568880cc..f22da5a78 100644 --- a/example/rails-60/app/views/que/index.html.erb +++ b/example/rails-60/app/views/que/index.html.erb @@ -22,20 +22,6 @@ Bugsnag dashboard.

    - -
  • -

    Crash and use callbacks

    - -

    - Raises an exception within the framework, but with additional data - attached to the report. -

    - -

    - By registering a callback before the error occurs useful data can - be attached as a tab in the Bugsnag dashboard. -

    -
  • ← return home

    diff --git a/example/rails-60/app/views/resque/callbacks.html.erb b/example/rails-60/app/views/resque/callbacks.html.erb deleted file mode 100644 index d730810d8..000000000 --- a/example/rails-60/app/views/resque/callbacks.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -

    Resque

    - -

    - The callback task has been queued. -

    - -

    - This can be executed by running: -

    - -
    QUEUE=callback bundle exec rake resque:work
    - -

    - Check your Bugsnag dashboard for the result! -

    - -

    ← return to Resque examples

    diff --git a/example/rails-60/app/views/resque/index.html.erb b/example/rails-60/app/views/resque/index.html.erb index 7f5e4d763..483e94910 100644 --- a/example/rails-60/app/views/resque/index.html.erb +++ b/example/rails-60/app/views/resque/index.html.erb @@ -1,6 +1,6 @@ -

    Rescue

    +

    Resque

    -

    This route demonstrates how to use Bugsnag with Rescue.

    +

    This route demonstrates how to use Bugsnag with Resque.

    While testing the examples open @@ -18,7 +18,7 @@ automatically executes each task, use:

    -
    QUEUE=crash,callback,metadata bundle exec rake resque:work
    +
    QUEUE=crash,metadata bundle exec rake resque:work
    1. @@ -30,27 +30,12 @@

    2. -
    3. -

      Crash and use callbacks

      - -

      - Raises an exception within the framework, but with additional data - attached to the report. -

      - -

      - By registering a callback before the error occurs useful data can - be attached as a tab in the Bugsnag dashboard. -

      -
    4. -
    5. Notify with data

      Same as notify but allows you to attach additional data - within a block, similar to the - "crash and use callbacks" example above. + within a block.

      diff --git a/example/rails-60/app/views/sidekiq/callbacks.html.erb b/example/rails-60/app/views/sidekiq/callbacks.html.erb deleted file mode 100644 index 14bb48406..000000000 --- a/example/rails-60/app/views/sidekiq/callbacks.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -

      Sidekiq

      - -

      - Sidekiq is performing a task that will crash, but registers a callback - before this to attach additional metadata. -

      - -

      - Check your Bugsnag dashboard for the result! -

      - -

      ← return to Sidekiq examples

      diff --git a/example/rails-60/app/views/sidekiq/index.html.erb b/example/rails-60/app/views/sidekiq/index.html.erb index 110b6746c..79313d0f3 100644 --- a/example/rails-60/app/views/sidekiq/index.html.erb +++ b/example/rails-60/app/views/sidekiq/index.html.erb @@ -20,23 +20,12 @@

      Raises an error within the framework, generating a report in the Bugsnag dashboard.

    6. -
    7. -

      Crash and use callbacks

      - -

      - Raises an exception within the framework, but with additional data - attached to the report. By registering a callback before the error - occurs useful data can be attached as a tab in the Bugsnag dashboard. -

      -
    8. -
    9. Notify with data

      Sends Bugsnag a report on demand using bugsnag.notify - and attaches additional data within a block, similar to - the "Crash and use callbacks" example above. In this case we're + and attaches additional data within a block. In this case we're adding information about the function being called to go into the function tab, and additional diagnostics as a diagnostics tab. diff --git a/example/rails-60/app/workers/resque_workers.rb b/example/rails-60/app/workers/resque_workers.rb index 1b0feee35..055cc1ad0 100644 --- a/example/rails-60/app/workers/resque_workers.rb +++ b/example/rails-60/app/workers/resque_workers.rb @@ -7,22 +7,6 @@ def self.perform end end - # Unhandled with callback Exception example - class Callback - @queue = :callback - - def self.perform - Bugsnag.before_notify_callbacks << proc { |report| - report.add_tab(:diagnostics, { - message: 'Resque demo says: Everything is great', - code: 200 - }) - } - - raise 'Crashed - Check the Bugsnag dashboard for diagnostic data' - end - end - # Handled example with additional data class Metadata @queue = :metadata diff --git a/example/rails-60/app/workers/sidekiq_workers.rb b/example/rails-60/app/workers/sidekiq_workers.rb index 59bfddef1..7e78ed17b 100644 --- a/example/rails-60/app/workers/sidekiq_workers.rb +++ b/example/rails-60/app/workers/sidekiq_workers.rb @@ -1,21 +1,4 @@ module SidekiqWorkers - class CallbackWorker - include Sidekiq::Worker - - sidekiq_options :retry => false - - def perform - Bugsnag.before_notify_callbacks << proc { |report| - report.add_tab(:diagnostics, { - message: 'Sidekiq demo says: Everything is great', - code: 200 - }) - } - - raise 'Sidekiq crashed, but the callback added metadata - Check your Bugsnag dashboard' - end - end - class CrashWorker include Sidekiq::Worker sidekiq_options :retry => false diff --git a/example/rails-60/config/initializers/bugsnag.rb b/example/rails-60/config/initializers/bugsnag.rb index 58c57fbf6..2b02bc66a 100644 --- a/example/rails-60/config/initializers/bugsnag.rb +++ b/example/rails-60/config/initializers/bugsnag.rb @@ -1,3 +1,11 @@ Bugsnag.configure do |config| config.api_key = "YOUR_API_KEY_HERE" + + config.add_on_error(proc do |report| + report.add_tab(:user, { + username: 'bob-hoskins', + email: 'bugsnag@bugsnag.com', + registered_user: true + }) + end) end diff --git a/example/rails-60/config/routes.rb b/example/rails-60/config/routes.rb index b5f1747c3..8e1e16d07 100644 --- a/example/rails-60/config/routes.rb +++ b/example/rails-60/config/routes.rb @@ -2,7 +2,6 @@ # Vanilla rails routing get '/', to: 'application#index' get '/crash', to: 'application#crash' - get '/crash_with_callback', to: 'application#callback' get '/notify', to: 'application#notify' get '/notify_data', to: 'application#data' get '/notify_severity', to: 'application#severity' @@ -11,17 +10,14 @@ get '/sidekiq', to: 'sidekiq#index' get '/sidekiq/crash', to: 'sidekiq#crash' get '/sidekiq/notify_data', to: 'sidekiq#metadata' - get '/sidekiq/crash_with_callback', to: 'sidekiq#callbacks' # Que routing get '/que', to: 'que#index' get '/que/crash', to: 'que#crash' get '/que/notify_data', to: 'que#metadata' - get '/que/crash_with_callback', to: 'que#callbacks' # Resque routing get '/resque', to: 'resque#index' get '/resque/crash', to: 'resque#crash' - get '/resque/crash_with_callback', to: 'resque#callbacks' get '/resque/notify_data', to: 'resque#metadata' end diff --git a/example/rails-60/tmp/.keep b/example/rails-60/tmp/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/example/rails-60/tmp/pids/.keep b/example/rails-60/tmp/pids/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/example/sinatra/config.ru b/example/sinatra/config.ru index 68efa300b..da677dabb 100644 --- a/example/sinatra/config.ru +++ b/example/sinatra/config.ru @@ -8,6 +8,14 @@ set :show_exceptions, false Bugsnag.configure do |config| config.api_key = 'YOUR_API_KEY' + + config.add_on_error(proc do |report| + report.add_tab(:user, { + username: 'bob-hoskins', + email: 'bugsnag@bugsnag.com', + registered_user: true + }) + end) end get '/' do @@ -23,21 +31,6 @@ get '/crash' do 'bugsnag.com for a new notification!') end -get '/crash_with_callback' do - Bugsnag.before_notify_callbacks << proc { |notification| - new_tab = { - message: 'Sinatra demo says: Everything is great', - code: 200 - } - notification.add_tab(:diagnostics, new_tab) - } - - msg = 'Bugsnag Sinatra demo says: It crashed! But, due to the attached callback' + - ' the exception has meta information. Go check' + - ' bugsnag.com for a new notification (see the Diagnostics tab)!' - raise RuntimeError.new(msg) -end - get '/notify' do Bugsnag.notify(RuntimeError.new("Bugsnag Sinatra demo says: False alarm, your application didn't crash")) @@ -48,21 +41,16 @@ end get '/notify_data' do error = RuntimeError.new("Bugsnag Sinatra demo says: False alarm, your application didn't crash") - Bugsnag.notify error do |report| - report.add_tab(:user, { - :username => "bob-hoskins", - :email => 'bugsnag@bugsnag.com', - :registered_user => true - }) + Bugsnag.notify(error) do |report| report.add_tab(:diagnostics, { - :message => 'Sinatra demo says: Everything is great', - :code => 200 + message: 'Sinatra demo says: Everything is great', + code: 200 }) end "Bugsnag Sinatra demo says: It didn't crash! " + 'But still go check https://bugsnag.com' + - ' for a new notification. Check out the User tab for the meta data' + ' for a new notification. Check out the Diagnostics tab for the meta data' end get '/notify_severity' do diff --git a/example/sinatra/templates/index.md b/example/sinatra/templates/index.md index 52695efe2..6e111829b 100644 --- a/example/sinatra/templates/index.md +++ b/example/sinatra/templates/index.md @@ -8,18 +8,14 @@ While testing the examples open [your dashboard](https://app.bugsnag.com) in ord
      Raises an error within the framework, generating a report in the Bugsnag dashboard. -2. [Crash and use callbacks](/crash_with_callback) -
      - Raises an exception within the framework, but with additional data attached to the report. By registering a callback before the error occurs useful data can be attached as a tab in the Bugsnag dashboard. - -3. [Notify](/notify) +2. [Notify](/notify)
      Sends Bugsnag a report on demand using `bugsnag.notify`. Allows details of handled errors or information to be sent to the Bugsnag dashboard without crashing your code. -4. [Notify with data](/notify_data) +3. [Notify with data](/notify_data)
      - Same as `notify` but allows you to attach additional data within a `block`, similar to the `before_notify_callbacks` example above. In this case we're adding information about the user to go into the `user` tab, and additional diagnostics as a `diagnostics` tab. + Same as `notify` but allows you to attach additional data within a `block`. In this case we're adding additional diagnostics as a `diagnostics` tab. -5. [Set the severity](/notify_severity) +4. [Set the severity](/notify_severity)
      - This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. \ No newline at end of file + This uses the same mechanism as adding meta-data, but allows you to set he `severity` when notifying Bugsnag of the error. Valid severities are `error`, `warning`, and `info`. Have a look on the dashboard to see the difference in these severities. diff --git a/features/fixtures/docker-compose.yml b/features/fixtures/docker-compose.yml index 3352a421a..0357d5fb5 100644 --- a/features/fixtures/docker-compose.yml +++ b/features/fixtures/docker-compose.yml @@ -120,6 +120,7 @@ services: - BUGSNAG_TIMEOUT - CALLBACK_INITIATOR - SQL_ONLY_BREADCRUMBS + - ADD_ON_ERROR - USE_DEFAULT_AUTO_CAPTURE_SESSIONS restart: "no" @@ -155,6 +156,7 @@ services: - BUGSNAG_TIMEOUT - CALLBACK_INITIATOR - SQL_ONLY_BREADCRUMBS + - ADD_ON_ERROR - USE_DEFAULT_AUTO_CAPTURE_SESSIONS restart: "no" @@ -190,6 +192,7 @@ services: - BUGSNAG_TIMEOUT - CALLBACK_INITIATOR - SQL_ONLY_BREADCRUMBS + - ADD_ON_ERROR - USE_DEFAULT_AUTO_CAPTURE_SESSIONS restart: "no" @@ -225,6 +228,7 @@ services: - BUGSNAG_TIMEOUT - CALLBACK_INITIATOR - SQL_ONLY_BREADCRUMBS + - ADD_ON_ERROR - USE_DEFAULT_AUTO_CAPTURE_SESSIONS restart: "no" networks: @@ -296,4 +300,4 @@ services: networks: default: - name: ${NETWORK_NAME} \ No newline at end of file + name: ${NETWORK_NAME} diff --git a/features/fixtures/plain/app/report_modification/initiators/handled_on_error.rb b/features/fixtures/plain/app/report_modification/initiators/handled_on_error.rb new file mode 100644 index 000000000..16e8af4d2 --- /dev/null +++ b/features/fixtures/plain/app/report_modification/initiators/handled_on_error.rb @@ -0,0 +1,10 @@ +require 'bugsnag' +require './app' + +configure_basics + +def run(callback) + Bugsnag.add_on_error(callback) + + Bugsnag.notify(RuntimeError.new("Oh no")) +end diff --git a/features/fixtures/plain/app/report_modification/initiators/unhandled_on_error.rb b/features/fixtures/plain/app/report_modification/initiators/unhandled_on_error.rb new file mode 100644 index 000000000..0b949961c --- /dev/null +++ b/features/fixtures/plain/app/report_modification/initiators/unhandled_on_error.rb @@ -0,0 +1,11 @@ +require 'bugsnag' +require './app' + +configure_basics +add_at_exit + +def run(callback) + Bugsnag.add_on_error(callback) + + raise RuntimeError.new "Oh no" +end diff --git a/features/fixtures/plain/app/stack_frame_modification/initiators/handled_on_error.rb b/features/fixtures/plain/app/stack_frame_modification/initiators/handled_on_error.rb new file mode 100644 index 000000000..012c6fca8 --- /dev/null +++ b/features/fixtures/plain/app/stack_frame_modification/initiators/handled_on_error.rb @@ -0,0 +1,29 @@ +require 'bugsnag' +require './app' + +configure_basics + +def run(callback) + Bugsnag.add_on_error(callback) + step_one +end + +def step_one + step_two +end + +def step_two + step_three +end + +def step_three + crash +end + +def crash + begin + "Test".insrt(-1, "!") + rescue Exception => e + Bugsnag.notify(e) + end +end diff --git a/features/fixtures/plain/app/stack_frame_modification/initiators/unhandled_on_error.rb b/features/fixtures/plain/app/stack_frame_modification/initiators/unhandled_on_error.rb new file mode 100644 index 000000000..7f323503c --- /dev/null +++ b/features/fixtures/plain/app/stack_frame_modification/initiators/unhandled_on_error.rb @@ -0,0 +1,26 @@ +require 'bugsnag' +require './app' + +configure_basics +add_at_exit + +def run(callback) + Bugsnag.add_on_error(callback) + step_one +end + +def step_one + step_two +end + +def step_two + step_three +end + +def step_three + crash +end + +def crash + raise RuntimeError.new "Oh no" +end diff --git a/features/fixtures/rails3/app/config/initializers/bugsnag.rb b/features/fixtures/rails3/app/config/initializers/bugsnag.rb index b849c0995..8a1ac08ea 100644 --- a/features/fixtures/rails3/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails3/app/config/initializers/bugsnag.rb @@ -18,4 +18,12 @@ breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load" end end + + if ENV["ADD_ON_ERROR"] == "true" + config.add_on_error(proc do |report| + report.add_tab(:on_error, { + source: report.unhandled ? 'on_error unhandled' : 'on_error handled' + }) + end) + end end diff --git a/features/fixtures/rails4/app/config/initializers/bugsnag.rb b/features/fixtures/rails4/app/config/initializers/bugsnag.rb index b849c0995..8a1ac08ea 100644 --- a/features/fixtures/rails4/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails4/app/config/initializers/bugsnag.rb @@ -18,4 +18,12 @@ breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load" end end + + if ENV["ADD_ON_ERROR"] == "true" + config.add_on_error(proc do |report| + report.add_tab(:on_error, { + source: report.unhandled ? 'on_error unhandled' : 'on_error handled' + }) + end) + end end diff --git a/features/fixtures/rails5/app/config/initializers/bugsnag.rb b/features/fixtures/rails5/app/config/initializers/bugsnag.rb index b849c0995..8a1ac08ea 100644 --- a/features/fixtures/rails5/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails5/app/config/initializers/bugsnag.rb @@ -18,4 +18,12 @@ breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load" end end + + if ENV["ADD_ON_ERROR"] == "true" + config.add_on_error(proc do |report| + report.add_tab(:on_error, { + source: report.unhandled ? 'on_error unhandled' : 'on_error handled' + }) + end) + end end diff --git a/features/fixtures/rails6/app/config/initializers/bugsnag.rb b/features/fixtures/rails6/app/config/initializers/bugsnag.rb index b849c0995..8a1ac08ea 100644 --- a/features/fixtures/rails6/app/config/initializers/bugsnag.rb +++ b/features/fixtures/rails6/app/config/initializers/bugsnag.rb @@ -18,4 +18,12 @@ breadcrumb.ignore! unless breadcrumb.meta_data[:event_name] == "sql.active_record" && breadcrumb.meta_data[:name] == "User Load" end end + + if ENV["ADD_ON_ERROR"] == "true" + config.add_on_error(proc do |report| + report.add_tab(:on_error, { + source: report.unhandled ? 'on_error unhandled' : 'on_error handled' + }) + end) + end end diff --git a/features/plain_features/add_tab.feature b/features/plain_features/add_tab.feature index 6dd380a9e..6e2218b8a 100644 --- a/features/plain_features/add_tab.feature +++ b/features/plain_features/add_tab.feature @@ -15,6 +15,8 @@ Scenario Outline: Metadata can be added to a report using add_tab | handled_before_notify | | handled_block | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | Scenario Outline: Metadata can be added to an existing tab using add_tab Given I set environment variable "CALLBACK_INITIATOR" to "" @@ -33,6 +35,8 @@ Scenario Outline: Metadata can be added to an existing tab using add_tab | handled_before_notify | | handled_block | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | Scenario Outline: Metadata can be overwritten using add_tab Given I set environment variable "CALLBACK_INITIATOR" to "" @@ -46,4 +50,6 @@ Scenario Outline: Metadata can be overwritten using add_tab | initiator | | handled_before_notify | | handled_block | - | unhandled_before_notify | \ No newline at end of file + | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | diff --git a/features/plain_features/ignore_report.feature b/features/plain_features/ignore_report.feature index e0087bbc7..c5c837829 100644 --- a/features/plain_features/ignore_report.feature +++ b/features/plain_features/ignore_report.feature @@ -10,3 +10,5 @@ Scenario Outline: A reports severity can be modified | initiator | | handled_before_notify | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | diff --git a/features/plain_features/report_api_key.feature b/features/plain_features/report_api_key.feature index b4252071e..6821c6125 100644 --- a/features/plain_features/report_api_key.feature +++ b/features/plain_features/report_api_key.feature @@ -11,4 +11,6 @@ Scenario Outline: A report can have its api_key modified | initiator | | handled_before_notify | | handled_block | - | unhandled_before_notify | \ No newline at end of file + | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | diff --git a/features/plain_features/report_severity.feature b/features/plain_features/report_severity.feature index 84e181ba3..c96dc258c 100644 --- a/features/plain_features/report_severity.feature +++ b/features/plain_features/report_severity.feature @@ -13,3 +13,5 @@ Scenario Outline: A reports severity can be modified | handled_before_notify | | handled_block | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | diff --git a/features/plain_features/report_stack_frames.feature b/features/plain_features/report_stack_frames.feature index 1cb3635de..ce34204e1 100644 --- a/features/plain_features/report_stack_frames.feature +++ b/features/plain_features/report_stack_frames.feature @@ -12,6 +12,8 @@ Scenario Outline: Stack frames can be removed | initiator | lineNumber | | handled_before_notify | 20 | | unhandled_before_notify | 21 | + | handled_on_error | 20 | + | unhandled_on_error | 21 | Scenario: Stack frames can be removed from a notified string Given I set environment variable "CALLBACK_INITIATOR" to "handled_block" @@ -36,6 +38,8 @@ Scenario Outline: Stack frames can be marked as in project | initiator | | handled_before_notify | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | Scenario: Stack frames can be marked as in project with a handled string Given I set environment variable "CALLBACK_INITIATOR" to "handled_block" diff --git a/features/plain_features/report_user.feature b/features/plain_features/report_user.feature index 9e3919273..4f4a34fe5 100644 --- a/features/plain_features/report_user.feature +++ b/features/plain_features/report_user.feature @@ -14,6 +14,8 @@ Scenario Outline: A report can have a user name, email, and id set | handled_before_notify | | handled_block | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | Scenario Outline: A report can have custom info set Given I set environment variable "CALLBACK_INITIATOR" to "" @@ -30,6 +32,8 @@ Scenario Outline: A report can have custom info set | handled_before_notify | | handled_block | | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | Scenario Outline: A report can have its user info removed Given I set environment variable "CALLBACK_INITIATOR" to "" @@ -42,4 +46,6 @@ Scenario Outline: A report can have its user info removed | initiator | | handled_before_notify | | handled_block | - | unhandled_before_notify | \ No newline at end of file + | unhandled_before_notify | + | handled_on_error | + | unhandled_on_error | diff --git a/features/rails_features/on_error.feature b/features/rails_features/on_error.feature new file mode 100644 index 000000000..35166ade6 --- /dev/null +++ b/features/rails_features/on_error.feature @@ -0,0 +1,29 @@ +Feature: On error callbacks + +@rails3 @rails4 @rails5 @rails6 +Scenario: Rails on_error works on handled errors + Given I set environment variable "ADD_ON_ERROR" to "true" + And I start the rails service + When I navigate to the route "/handled/unthrown" on the rails app + And I wait to receive a request + Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And the exception "errorClass" equals "RuntimeError" + And the exception "message" starts with "handled unthrown error" + And the event "unhandled" is false + And the event "app.type" equals "rails" + And the event "metaData.request.url" ends with "/handled/unthrown" + And the event "metaData.on_error.source" equals "on_error handled" + +@rails3 @rails4 @rails5 @rails6 +Scenario: Rails on_error works on unhandled errors + Given I set environment variable "ADD_ON_ERROR" to "true" + And I start the rails service + When I navigate to the route "/unhandled/error" on the rails app + And I wait to receive a request + Then the request is valid for the error reporting API version "4.0" for the "Ruby Bugsnag Notifier" + And the exception "errorClass" equals "NameError" + And the exception "message" starts with "undefined local variable or method `generate_unhandled_error' for # string of code for that line + # + # @return [void] + def extract! + @files.each do |path, traces| + begin + line_numbers = Set.new + + traces.each do |trace| + trace[:first_line_number].upto(trace[:last_line_number]) do |line_number| + line_numbers << line_number + end + end + + extract_from(path, traces, line_numbers) + rescue StandardError => e + # Clean up after ourselves + traces.each do |trace| + trace[:code] ||= nil + trace.delete(:first_line_number) + trace.delete(:last_line_number) + end + + @configuration.warn("Error extracting code: #{e.inspect}") + end + end + end + + private + + ## + # @param path [String] + # @param traces [Array] + # @param line_numbers [Set] + # @return [void] + def extract_from(path, traces, line_numbers) + code = {} + + File.open(path) do |file| + current_line_number = 0 + + file.each_line do |line| + current_line_number += 1 + + next unless line_numbers.include?(current_line_number) + + code[current_line_number] = line[0...200].rstrip + end + end + + associate_code_with_trace(code, traces) + end + + ## + # @param code [Hash{Integer => String}] + # @param traces [Array] + # @return [void] + def associate_code_with_trace(code, traces) + traces.each do |trace| + trace[:code] = {} + + code.each do |line_number, line| + # If we've gone past the last line we care about, we can stop iterating + break if line_number > trace[:last_line_number] + + # Skip lines that aren't in the range we want + next unless line_number >= trace[:first_line_number] + + trace[:code][line_number] = line + end + + trim_excess_lines(trace[:code], trace[:lineNumber]) + trace.delete(:first_line_number) + trace.delete(:last_line_number) + end + end + + ## + # @param code [Hash{Integer => String}] + # @param line_number [Integer] + # @return [void] + def trim_excess_lines(code, line_number) + while code.length > MAXIMUM_LINES_TO_KEEP + last_line = code.keys.max + first_line = code.keys.min + + if (last_line - line_number) > (line_number - first_line) + code.delete(last_line) + else + code.delete(first_line) + end + end + end + end +end diff --git a/lib/bugsnag/configuration.rb b/lib/bugsnag/configuration.rb index d017c111e..b8e8954f8 100644 --- a/lib/bugsnag/configuration.rb +++ b/lib/bugsnag/configuration.rb @@ -318,6 +318,33 @@ def disable_sessions @enable_sessions = false end + ## + # Add the given callback to the list of on_error callbacks + # + # The on_error callbacks will be called when an error is captured or reported + # and are passed a {Bugsnag::Report} object + # + # Returning false from an on_error callback will cause the error to be ignored + # and will prevent any remaining callbacks from being called + # + # @param callback [Proc] + # @return [void] + def add_on_error(callback) + middleware.use(callback) + end + + ## + # Remove the given callback from the list of on_error callbacks + # + # Note that this must be the same Proc instance that was passed to + # {#add_on_error}, otherwise it will not be removed + # + # @param callback [Proc] + # @return [void] + def remove_on_error(callback) + middleware.remove(callback) + end + private attr_writer :scopes_to_filter diff --git a/lib/bugsnag/middleware_stack.rb b/lib/bugsnag/middleware_stack.rb index a28ee0053..d9dcd95bc 100644 --- a/lib/bugsnag/middleware_stack.rb +++ b/lib/bugsnag/middleware_stack.rb @@ -1,3 +1,5 @@ +require "bugsnag/on_error_callbacks" + module Bugsnag class MiddlewareStack ## @@ -65,11 +67,26 @@ def insert_before(before, new_middleware) end end + ## + # Disable the given middleware. This removes them from the list of + # middleware and ensures they cannot be added again + # + # See also {#remove} def disable(*middlewares) @mutex.synchronize do @disabled_middleware += middlewares - @middlewares.delete_if {|m| @disabled_middleware.include?(m)} + @middlewares.delete_if {|m| @disabled_middleware.include?(m) } + end + end + + ## + # Remove the given middleware from the list of middleware + # + # This is like {#disable} but allows the middleware to be added again + def remove(*middlewares) + @mutex.synchronize do + @middlewares.delete_if {|m| middlewares.include?(m) } end end @@ -91,7 +108,7 @@ def run(report) begin # We reverse them, so we can call "call" on the first middleware - middleware_procs.reverse.inject(notify_lambda) { |n,e| e.call(n) }.call(report) + middleware_procs.reverse.inject(notify_lambda) {|n, e| e.call(n) }.call(report) rescue StandardError => e # KLUDGE: Since we don't re-raise middleware exceptions, this breaks rspec raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError" @@ -107,10 +124,28 @@ def run(report) end private + + ## # Generates a list of middleware procs that are ready to be run # Pass each one a reference to the next in the queue + # + # @return [Array] def middleware_procs - @middlewares.map{|middleware| proc { |next_middleware| middleware.new(next_middleware) } } + # Split the middleware into separate lists of Procs and Classes + procs, classes = @middlewares.partition {|middleware| middleware.is_a?(Proc) } + + # Wrap the classes in a proc that, when called, news up the middleware and + # passes the next middleware in the queue + middleware_instances = classes.map do |middleware| + proc {|next_middleware| middleware.new(next_middleware) } + end + + # Wrap the list of procs in a proc that, when called, wraps them in an + # 'OnErrorCallbacks' instance that also has a reference to the next middleware + wrapped_procs = proc {|next_middleware| OnErrorCallbacks.new(next_middleware, procs) } + + # Return the combined middleware and wrapped procs + middleware_instances.push(wrapped_procs) end end end diff --git a/lib/bugsnag/on_error_callbacks.rb b/lib/bugsnag/on_error_callbacks.rb new file mode 100644 index 000000000..70fa7f5f0 --- /dev/null +++ b/lib/bugsnag/on_error_callbacks.rb @@ -0,0 +1,33 @@ +module Bugsnag + # @api private + class OnErrorCallbacks + def initialize(next_middleware, callbacks) + @next_middleware = next_middleware + @callbacks = callbacks + end + + ## + # @param report [Report] + def call(report) + @callbacks.each do |callback| + begin + should_continue = callback.call(report) + rescue StandardError => e + Bugsnag.configuration.warn("Error occurred in on_error callback: '#{e}'") + Bugsnag.configuration.warn("on_error callback stacktrace: #{e.backtrace.inspect}") + end + + # If a callback returns false, we ignore the report and stop running callbacks + # Note that we explicitly check for 'false' so that callbacks don't need + # to return anything (i.e. can return 'nil') and we still continue + next unless should_continue == false + + report.ignore! + + break + end + + @next_middleware.call(report) + end + end +end diff --git a/lib/bugsnag/report.rb b/lib/bugsnag/report.rb index 66b3ed7fd..b42fed8d2 100644 --- a/lib/bugsnag/report.rb +++ b/lib/bugsnag/report.rb @@ -182,7 +182,7 @@ def generate_exception_list { errorClass: error_class(exception), message: exception.message, - stacktrace: Stacktrace.new(exception.backtrace, configuration).to_a + stacktrace: Stacktrace.process(exception.backtrace, configuration) } end end diff --git a/lib/bugsnag/session_tracker.rb b/lib/bugsnag/session_tracker.rb index d57e227c7..65c0f98a5 100644 --- a/lib/bugsnag/session_tracker.rb +++ b/lib/bugsnag/session_tracker.rb @@ -1,11 +1,9 @@ require 'thread' require 'time' require 'securerandom' -require 'concurrent' module Bugsnag class SessionTracker - THREAD_SESSION = "bugsnag_session" SESSION_PAYLOAD_VERSION = "1.0" MUTEX = Mutex.new @@ -27,6 +25,8 @@ def self.get_current_session ## # Initializes the session tracker. def initialize + require 'concurrent' + @session_counts = Concurrent::Hash.new(0) end @@ -139,4 +139,4 @@ def deliver(session_payload) Bugsnag::Delivery[Bugsnag.configuration.delivery_method].deliver(Bugsnag.configuration.session_endpoint, payload, Bugsnag.configuration, options) end end -end \ No newline at end of file +end diff --git a/lib/bugsnag/stacktrace.rb b/lib/bugsnag/stacktrace.rb index ac31bb535..2ce131a96 100644 --- a/lib/bugsnag/stacktrace.rb +++ b/lib/bugsnag/stacktrace.rb @@ -1,6 +1,7 @@ -module Bugsnag - class Stacktrace +require_relative 'code_extractor' +module Bugsnag + module Stacktrace # e.g. "org/jruby/RubyKernel.java:1264:in `catch'" BACKTRACE_LINE_REGEX = /^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$/ @@ -10,13 +11,15 @@ class Stacktrace ## # Process a backtrace and the configuration into a parsed stacktrace. # - # rubocop:todo Metrics/CyclomaticComplexity - def initialize(backtrace, configuration) - @configuration = configuration + # @param backtrace [Array, nil] If nil, 'caller' will be used instead + # @param configuration [Configuration] + # @return [Array] + def self.process(backtrace, configuration) + code_extractor = CodeExtractor.new(configuration) backtrace = caller if !backtrace || backtrace.empty? - @processed_backtrace = backtrace.map do |trace| + processed_backtrace = backtrace.map do |trace| # Parse the stacktrace line if trace.match(BACKTRACE_LINE_REGEX) file, line_str, method = [$1, $2, $3] @@ -24,27 +27,25 @@ def initialize(backtrace, configuration) method, file, line_str = [$1, $2, $3] end - next(nil) if file.nil? + next if file.nil? # Expand relative paths file = File.realpath(file) rescue file # Generate the stacktrace line hash - trace_hash = {} - trace_hash[:lineNumber] = line_str.to_i + trace_hash = { lineNumber: line_str.to_i } - if configuration.send_code - trace_hash[:code] = code(file, trace_hash[:lineNumber]) - end + # Save a copy of the file path as we're about to modify it but need the + # raw version when extracting code (otherwise we can't open the file) + raw_file_path = file.dup # Clean up the file path in the stacktrace - if defined?(@configuration.project_root) && @configuration.project_root.to_s != '' - trace_hash[:inProject] = true if file.start_with?(@configuration.project_root.to_s) - file.sub!(/#{@configuration.project_root}\//, "") - trace_hash.delete(:inProject) if file.match(@configuration.vendor_path) + if defined?(configuration.project_root) && configuration.project_root.to_s != '' + trace_hash[:inProject] = true if file.start_with?(configuration.project_root.to_s) + file.sub!(/#{configuration.project_root}\//, "") + trace_hash.delete(:inProject) if file.match(configuration.vendor_path) end - # Strip common gem path prefixes if defined?(Gem) file = Gem.path.inject(file) {|line, path| line.sub(/#{path}\//, "") } @@ -55,60 +56,16 @@ def initialize(backtrace, configuration) # Add a method if we have it trace_hash[:method] = method if method && (method =~ /^__bind/).nil? - if trace_hash[:file] && !trace_hash[:file].empty? - trace_hash - else - nil - end - end.compact - end - # rubocop:enable Metrics/CyclomaticComplexity - - ## - # Returns the processed backtrace - def to_a - @processed_backtrace - end - - private + # If we're going to send code then record the raw file path and the + # trace_hash, so we can extract from it later + code_extractor.add_file(raw_file_path, trace_hash) if configuration.send_code - def code(file, line_number, num_lines = 7) - code_hash = {} - - from_line = [line_number - num_lines, 1].max - - # don't try and open '(irb)' or '-e' - return unless File.exist?(file) - - # Populate code hash with line numbers and code lines - File.open(file) do |f| - current_line_number = 0 - f.each_line do |line| - current_line_number += 1 - - next if current_line_number < from_line - - code_hash[current_line_number] = line[0...200].rstrip - - break if code_hash.length >= ( num_lines * 1.5 ).ceil - end - end - - while code_hash.length > num_lines - last_line = code_hash.keys.max - first_line = code_hash.keys.min + trace_hash + end.compact - if (last_line - line_number) > (line_number - first_line) - code_hash.delete(last_line) - else - code_hash.delete(first_line) - end - end + code_extractor.extract! if configuration.send_code - code_hash - rescue - @configuration.warn("Error fetching code: #{$!.inspect}") - nil + processed_backtrace end end end diff --git a/spec/code_extractor_spec.rb b/spec/code_extractor_spec.rb new file mode 100644 index 000000000..17c26bf6a --- /dev/null +++ b/spec/code_extractor_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe Bugsnag::CodeExtractor do + it "extracts code from a file and adds it to the given hash" do + file1_hash = { lineNumber: 5 } + file2_hash = { lineNumber: 7 } + + code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new) + code_extractor.add_file("spec/fixtures/crashes/file1.rb", file1_hash) + code_extractor.add_file("spec/fixtures/crashes/file2.rb", file2_hash) + + code_extractor.extract! + + expect(file1_hash).to eq({ + lineNumber: 5, + code: { + 2 => "", + 3 => "module File1", + 4 => " def self.foo1", + 5 => " File2.foo2", + 6 => " end", + 7 => "", + 8 => " def self.bar1" + } + }) + + expect(file2_hash).to eq({ + lineNumber: 7, + code: { + 4 => " end", + 5 => "", + 6 => " def self.bar2", + 7 => " File1.baz1", + 8 => " end", + 9 => "", + 10 => " def self.baz2" + } + }) + end + + it "handles extracting code from the first & last line in a file" do + file1_hash = { lineNumber: 1 } + file2_hash = { lineNumber: 25 } + + code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new) + code_extractor.add_file("spec/fixtures/crashes/file1.rb", file1_hash) + code_extractor.add_file("spec/fixtures/crashes/file2.rb", file2_hash) + + code_extractor.extract! + + expect(file1_hash).to eq({ + lineNumber: 1, + code: { + 1 => "require_relative 'file2'", + 2 => "", + 3 => "module File1", + 4 => " def self.foo1", + 5 => " File2.foo2", + 6 => " end", + 7 => "" + } + }) + + expect(file2_hash).to eq({ + lineNumber: 25, + code: { + 19 => " puts 'abcdef2'", + 20 => " end", + 21 => "", + 22 => " def self.abcdefghi2", + 23 => " puts 'abcdefghi2'", + 24 => " end", + 25 => "end" + } + }) + end + + it "truncates lines to a maximum of 200 characters" do + hash = { lineNumber: 4 } + + code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new) + code_extractor.add_file("spec/fixtures/crashes/file_with_long_lines.rb", hash) + + code_extractor.extract! + + # rubocop:disable Layout/LineLength + expect(hash).to eq({ + lineNumber: 4, + code: { + 1 => "# rubocop:disable Layout/LineLength", + 2 => "def a_super_long_function_name_that_would_be_really_impractical_to_use_but_luckily_this_is_just_for_a_test_to_prove_we_can_handle_really_long_lines_of_code_that_go_over_200_characters_and_some_more_pa", + 3 => " puts 'This is a shorter string'", + 4 => " puts 'A more realistic example of when a line would be really long is long strings such as this one, which extends over the 200 character limit by containing a lot of excess words for padding its le", + 5 => " puts 'and another shorter string for comparison'", + 6 => "end", + 7 => "# rubocop:enable Layout/LineLength", + } + }) + # rubocop:enable Layout/LineLength + end + + it "rescues exceptions raised in extract!" do + file1_hash = { lineNumber: 1 } + file2_hash = { lineNumber: 25 } + + code_extractor = Bugsnag::CodeExtractor.new(Bugsnag::Configuration.new) + code_extractor.add_file("spec/fixtures/crashes/file1.rb", file1_hash) + code_extractor.add_file("spec/fixtures/crashes/file2.rb", file2_hash) + + file1_hash[:first_line_number] = nil + + code_extractor.extract! + + expect(file1_hash).to eq({ lineNumber: 1, code: nil }) + + expect(file2_hash).to eq({ + lineNumber: 25, + code: { + 19 => " puts 'abcdef2'", + 20 => " end", + 21 => "", + 22 => " def self.abcdefghi2", + 23 => " puts 'abcdefghi2'", + 24 => " end", + 25 => "end" + } + }) + end +end diff --git a/spec/fixtures/crashes/file1.rb b/spec/fixtures/crashes/file1.rb new file mode 100644 index 000000000..f1043647d --- /dev/null +++ b/spec/fixtures/crashes/file1.rb @@ -0,0 +1,29 @@ +require_relative 'file2' + +module File1 + def self.foo1 + File2.foo2 + end + + def self.bar1 + File2.bar2 + end + + def self.baz1 + File2.baz2 + end + + def self.abc1 + puts 'abc' + end + + def self.abcdef1 + puts 'abcdef1' + end + + def self.abcdefghi1 + puts 'abcdefghi1' + end +end + +File1.foo1 diff --git a/spec/fixtures/crashes/file2.rb b/spec/fixtures/crashes/file2.rb new file mode 100644 index 000000000..66c195d43 --- /dev/null +++ b/spec/fixtures/crashes/file2.rb @@ -0,0 +1,25 @@ +module File2 + def self.foo2 + File1.bar1 + end + + def self.bar2 + File1.baz1 + end + + def self.baz2 + raise 'uh oh' + end + + def self.abc2 + puts 'abc' + end + + def self.abcdef2 + puts 'abcdef2' + end + + def self.abcdefghi2 + puts 'abcdefghi2' + end +end diff --git a/spec/fixtures/crashes/file_with_long_lines.rb b/spec/fixtures/crashes/file_with_long_lines.rb new file mode 100644 index 000000000..0150e4a4b --- /dev/null +++ b/spec/fixtures/crashes/file_with_long_lines.rb @@ -0,0 +1,7 @@ +# rubocop:disable Layout/LineLength +def a_super_long_function_name_that_would_be_really_impractical_to_use_but_luckily_this_is_just_for_a_test_to_prove_we_can_handle_really_long_lines_of_code_that_go_over_200_characters_and_some_more_padding + puts 'This is a shorter string' + puts 'A more realistic example of when a line would be really long is long strings such as this one, which extends over the 200 character limit by containing a lot of excess words for padding its length so that it is super long' + puts 'and another shorter string for comparison' +end +# rubocop:enable Layout/LineLength diff --git a/spec/fixtures/crashes/functions.rb b/spec/fixtures/crashes/functions.rb new file mode 100644 index 000000000..44c11fb0a --- /dev/null +++ b/spec/fixtures/crashes/functions.rb @@ -0,0 +1,29 @@ +def foo + bar +end + +def bar + baz +end + +def baz + xyz +end + +def xyz + raise 'uh oh' +end + +def abc + puts 'abc' +end + +def abcdef + puts 'abcdef' +end + +def abcdefghi + puts 'abcdefghi' +end + +foo diff --git a/spec/fixtures/crashes/short_file.rb b/spec/fixtures/crashes/short_file.rb index c3e6d19a4..bc95df820 100644 --- a/spec/fixtures/crashes/short_file.rb +++ b/spec/fixtures/crashes/short_file.rb @@ -1 +1,3 @@ +# raise 'hell' +# diff --git a/spec/on_error_spec.rb b/spec/on_error_spec.rb new file mode 100644 index 000000000..ab982944d --- /dev/null +++ b/spec/on_error_spec.rb @@ -0,0 +1,332 @@ +require "spec_helper" + +describe "on_error callbacks" do + it "runs callbacks on notify" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "hello" => "world" }) + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + end) + end + + it "can add callbacks in a configure block" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + Bugsnag.configure do |config| + config.add_on_error(callback1) + config.add_on_error(callback2) + end + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "hello" => "world" }) + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + end) + end + + it "can remove an already registered callback" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + + Bugsnag.remove_on_error(callback1) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to be_nil + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + end) + end + + it "can remove all registered callbacks" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + + Bugsnag.remove_on_error(callback2) + Bugsnag.remove_on_error(callback1) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to be_nil + expect(event["metaData"]["significant"]).to be_nil + end) + end + + it "does not remove an identical callback if it is not the same Proc" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback1_duplicate = proc {|report| report.add_tab(:important, { hello: "world" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.remove_on_error(callback1_duplicate) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "hello" => "world" }) + end) + end + + it "can re-add callbacks that have previously been removed" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + + Bugsnag.remove_on_error(callback1) + + Bugsnag.add_on_error(callback1) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "hello" => "world" }) + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + end) + end + + it "will only add a callback once" do + called_count = 0 + + callback = proc do |report| + called_count += 1 + + report.add_tab(:important, { called: called_count }) + end + + 1.upto(10) do |i| + Bugsnag.add_on_error(callback) + end + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(called_count).to be(1) + expect(event["metaData"]["important"]).to eq({ "called" => 1 }) + end) + end + + it "will ignore the report and stop calling callbacks if one returns false" do + logger = spy('logger') + Bugsnag.configuration.logger = logger + + called_count = 0 + + callback1 = proc { called_count += 1 } + callback2 = proc { called_count += 1 } + callback3 = proc { false } + callback4 = proc { called_count += 1 } + callback5 = proc { called_count += 1 } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + Bugsnag.add_on_error(callback3) + Bugsnag.add_on_error(callback4) + Bugsnag.add_on_error(callback5) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).not_to have_sent_notification + expect(called_count).to be(2) + + expect(logger).to have_received(:debug).with("[Bugsnag]") do |&block| + expect(block.call).to eq("Not notifying RuntimeError due to ignore being signified in user provided middleware") + end + end + + it "callbacks are called in the same order they are added (FIFO)" do + callback1 = proc do |report| + expect(report.meta_data[:important]).to be_nil + + report.add_tab(:important, { magic_number: 9 }) + end + + callback2 = proc do |report| + expect(report.meta_data[:important]).to eq({ magic_number: 9 }) + + report.add_tab(:important, { magic_number: 99 }) + end + + callback3 = proc do |report| + expect(report.meta_data[:important]).to eq({ magic_number: 99 }) + + report.add_tab(:important, { magic_number: 999 }) + end + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + Bugsnag.add_on_error(callback3) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "magic_number" => 999 }) + end) + end + + it "callbacks continue being called after a callback raises" do + logger = spy('logger') + Bugsnag.configuration.logger = logger + + callback1 = proc {|report| report.add_tab(:important, { a: "b" }) } + callback2 = proc {|_report| raise "bad things" } + callback3 = proc {|report| report.add_tab(:important, { c: "d" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + Bugsnag.add_on_error(callback3) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "a" => "b", "c" => "d" }) + end) + + message_index = 0 + expected_messages = [ + /^Error occurred in on_error callback: 'bad things'$/, + /^on_error callback stacktrace:/ + ] + + expect(logger).to have_received(:warn).with("[Bugsnag]").twice do |&block| + expect(block.call).to match(expected_messages[message_index]) + message_index += 1 + end + end + + it "runs callbacks even if no other middleware is registered" do + # Reset the middleware stack so any default middleware are removed + Bugsnag.configuration.middleware = Bugsnag::MiddlewareStack.new + + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + Bugsnag.add_on_error(callback1) + Bugsnag.add_on_error(callback2) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "hello" => "world" }) + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + end) + end + + describe "using callbacks across threads" do + it "runs callbacks that are added in different threads" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + callback3 = proc {|report| report.add_tab(:crucial, { magic_number: 999 }) } + + Bugsnag.add_on_error(callback1) + + threads = [ + Thread.new { Bugsnag.add_on_error(callback2) }, + Thread.new { Bugsnag.add_on_error(callback3) } + ] + + threads.each(&:join) + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "hello" => "world" }) + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + expect(event["metaData"]["crucial"]).to eq({ "magic_number" => 999 }) + end) + end + + it "can remove callbacks that are added in different threads" do + callback1 = proc {|report| report.add_tab(:important, { hello: "world" }) } + callback2 = proc {|report| report.add_tab(:significant, { hey: "earth" }) } + + # We need to create & join these one at a time so that callback1 has + # definitely been added before it is removed, otherwise this test will fail + # at random + Thread.new { Bugsnag.add_on_error(callback1) }.join + Thread.new { Bugsnag.remove_on_error(callback1) }.join + Thread.new { Bugsnag.add_on_error(callback2) }.join + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to be_nil + expect(event["metaData"]["significant"]).to eq({ "hey" => "earth" }) + end) + end + + it "callbacks are called in FIFO order when added in separate threads" do + callback1 = proc do |report| + expect(report.meta_data[:important]).to be_nil + + report.add_tab(:important, { magic_number: 9 }) + end + + callback2 = proc do |report| + expect(report.meta_data[:important]).to eq({ magic_number: 9 }) + + report.add_tab(:important, { magic_number: 99 }) + end + + callback3 = proc do |report| + expect(report.meta_data[:important]).to eq({ magic_number: 99 }) + + report.add_tab(:important, { magic_number: 999 }) + end + + # As above, we need to create & join these one at a time so that each + # callback is added in sequence + Thread.new { Bugsnag.add_on_error(callback1) }.join + Thread.new { Bugsnag.add_on_error(callback2) }.join + Thread.new { Bugsnag.add_on_error(callback3) }.join + + Bugsnag.notify(RuntimeError.new("Oh no!")) + + expect(Bugsnag).to(have_sent_notification do |payload, _headers| + event = get_event_from_payload(payload) + + expect(event["metaData"]["important"]).to eq({ "magic_number" => 999 }) + end) + end + end +end diff --git a/spec/report_spec.rb b/spec/report_spec.rb index 91126274d..04fa63330 100644 --- a/spec/report_spec.rb +++ b/spec/report_spec.rb @@ -1373,27 +1373,30 @@ def to_s notify_test_exception expect(Bugsnag).to have_sent_notification{ |payload, headers| exception = get_exception_from_payload(payload) + bugsnag_count = 0 + exception["stacktrace"].each do |frame| if /.*lib\/bugsnag.*\.rb/.match(frame["file"]) bugsnag_count += 1 expect(frame["inProject"]).to be_nil end end - # 7 is used here as the called bugsnag frames for a `notify` call should be: + + # 6 is used here as the called bugsnag frames for a `notify` call should be: # - Bugsnag.notify # - Report.new # - Report.initialize # - Report.generate_exceptions_list # - Report.generate_exceptions_list | raw_exceptions.map # - Report.generate_exceptions_list | raw_exceptions.map | block - # - Report.generate_exceptions_list | raw_exceptions.map | block | Stacktrace.new - # However, JRUBY does not include the two `new` frames, resulting in 5 bugsnag frames + # However, JRUBY does not include the `Report.new` frame, resulting in 5 bugsnag frames if defined?(JRUBY_VERSION) frame_count = 5 else - frame_count = 7 + frame_count = 6 end + expect(bugsnag_count).to equal frame_count } end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b172d183b..1b869e377 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -30,6 +30,14 @@ def get_exception_from_payload(payload) event["exceptions"].last end +def get_code_from_payload(payload, index = 0) + exception = get_exception_from_payload(payload) + + expect(exception["stacktrace"].size).to be > index + + exception["stacktrace"][index]["code"] +end + def notify_test_exception(*args) Bugsnag.notify(RuntimeError.new("test message"), *args) end diff --git a/spec/stacktrace_spec.rb b/spec/stacktrace_spec.rb index 909d4e025..cf4344d0a 100644 --- a/spec/stacktrace_spec.rb +++ b/spec/stacktrace_spec.rb @@ -15,10 +15,10 @@ Bugsnag.notify(e) end - expect(Bugsnag).to have_sent_notification{ |payload, headers| - exception = get_exception_from_payload(payload) - starting_line = __LINE__ - 13 - expect(exception["stacktrace"][0]["code"]).to eq({ + expect(Bugsnag).to have_sent_notification { |payload, headers| + starting_line = __LINE__ - 12 + + expect(get_code_from_payload(payload).to_a).to eq({ (starting_line + 0).to_s => ' _a = 1', (starting_line + 1).to_s => ' _b = 2', (starting_line + 2).to_s => ' _c = 3', @@ -26,7 +26,7 @@ (starting_line + 4).to_s => ' _d = 4', (starting_line + 5).to_s => ' _e = 5', (starting_line + 6).to_s => ' _f = 6' - }) + }.to_a) } end @@ -35,19 +35,16 @@ notify_test_exception - expect(Bugsnag).to have_sent_notification{ |payload, headers| - exception = get_exception_from_payload(payload) - expect(exception["stacktrace"][1]["code"]).to eq(nil) + expect(Bugsnag).to have_sent_notification { |payload, headers| + expect(get_code_from_payload(payload)).to eq(nil) } end it 'should send the first 7 lines of the file for exceptions near the top' do load 'spec/fixtures/crashes/start_of_file.rb' rescue Bugsnag.notify $! - expect(Bugsnag).to have_sent_notification{ |payload, headers| - exception = get_exception_from_payload(payload) - - expect(exception["stacktrace"][0]["code"]).to eq({ + expect(Bugsnag).to have_sent_notification { |payload, headers| + expect(get_code_from_payload(payload).to_a).to eq({ "1" => "#", "2" => "raise 'hell'", "3" => "#", @@ -55,17 +52,15 @@ "5" => "#", "6" => "#", "7" => "#" - }) + }.to_a) } end it 'should send the last 7 lines of the file for exceptions near the bottom' do load 'spec/fixtures/crashes/end_of_file.rb' rescue Bugsnag.notify $! - expect(Bugsnag).to have_sent_notification{ |payload, headers| - exception = get_exception_from_payload(payload) - - expect(exception["stacktrace"][0]["code"]).to eq({ + expect(Bugsnag).to have_sent_notification { |payload, headers| + expect(get_code_from_payload(payload)).to eq({ "3" => "#", "4" => "#", "5" => "#", @@ -77,17 +72,226 @@ } end - it 'should send the last 7 lines of the file for exceptions near the bottom' do + it 'should send every line of a very short file' do load 'spec/fixtures/crashes/short_file.rb' rescue Bugsnag.notify $! - expect(Bugsnag).to have_sent_notification{ |payload, headers| - exception = get_exception_from_payload(payload) + expect(Bugsnag).to have_sent_notification { |payload, headers| + expect(get_code_from_payload(payload).to_a).to eq({ + "1" => "#", + "2" => "raise 'hell'", + "3" => "#" + }.to_a) + } + end - expect(exception["stacktrace"][0]["code"]).to eq({ - "1" => "raise 'hell'" - }) + it 'should send code for each line in the stacktrace' do + load 'spec/fixtures/crashes/functions.rb' rescue Bugsnag.notify $! + + expected_code = [ + # The topmost frame is centered on where the exception was raised + { + "11" => "end", + "12" => "", + "13" => "def xyz", + "14" => " raise 'uh oh'", + "15" => "end", + "16" => "", + "17" => "def abc" + }, + # then we get 'baz' which is where 'xyz' was called + { + "7" => "end", + "8" => "", + "9" => "def baz", + "10" => " xyz", + "11" => "end", + "12" => "", + "13" => "def xyz" + }, + # then we get 'bar' which is where 'baz' was called + { + "3" => "end", + "4" => "", + "5" => "def bar", + "6" => " baz", + "7" => "end", + "8" => "", + "9" => "def baz" + }, + # then we get 'foo' which is where 'bar' was called - this is the first + # 7 lines because the call to 'bar' is on line 2 + { + "1" => "def foo", + "2" => " bar", + "3" => "end", + "4" => "", + "5" => "def bar", + "6" => " baz", + "7" => "end" + }, + # finally we get the call to 'foo' - this is the last 7 lines because + # the call is on the last line of the file + { + "23" => "end", + "24" => "", + "25" => "def abcdefghi", + "26" => " puts 'abcdefghi'", + "27" => "end", + "28" => "", + "29" => "foo" + } + ] + + expect(Bugsnag).to have_sent_notification { |payload, headers| + (0...expected_code.size).each do |index| + expect(get_code_from_payload(payload, index).to_a).to eq(expected_code[index].to_a) + end + } + end + + it 'should send code for each line in the stacktrace when split over multiple files' do + load 'spec/fixtures/crashes/file1.rb' rescue Bugsnag.notify $! + + expected_code = [ + { + "8" => " end", + "9" => "", + "10" => " def self.baz2", + "11" => " raise 'uh oh'", + "12" => " end", + "13" => "", + "14" => " def self.abc2" + }, + { + "10" => " end", + "11" => "", + "12" => " def self.baz1", + "13" => " File2.baz2", + "14" => " end", + "15" => "", + "16" => " def self.abc1" + }, + { + "4" => " end", + "5" => "", + "6" => " def self.bar2", + "7" => " File1.baz1", + "8" => " end", + "9" => "", + "10" => " def self.baz2" + }, + { + "6" => " end", + "7" => "", + "8" => " def self.bar1", + "9" => " File2.bar2", + "10" => " end", + "11" => "", + "12" => " def self.baz1", + }, + { + "1" => "module File2", + "2" => " def self.foo2", + "3" => " File1.bar1", + "4" => " end", + "5" => "", + "6" => " def self.bar2", + "7" => " File1.baz1" + }, + { + "2" => "", + "3" => "module File1", + "4" => " def self.foo1", + "5" => " File2.foo2", + "6" => " end", + "7" => "", + "8" => " def self.bar1" + }, + { + "23" => "", + "24" => " def self.abcdefghi1", + "25" => " puts 'abcdefghi1'", + "26" => " end", + "27" => "end", + "28" => "", + "29" => "File1.foo1" + } + ] + + expect(Bugsnag).to have_sent_notification { |payload, headers| + (0...expected_code.size).each do |index| + expect(get_code_from_payload(payload, index).to_a).to eq(expected_code[index].to_a) + end } end + + it "can extract code from paths that will be mangled by the project root" do + # Set the project root to a nested directory, which will then be stripped + # from the file paths in the API call. This ensures that we read the files + # based off of the original path, rather than the final file path, e.g. + # "spec/fixtures/crashes/file1.rb" will be "file1.rb" in the API call, which + # isn't a path that's possible to read + project_root = "#{File.dirname(File.dirname(__FILE__))}/spec/fixtures/crashes" + + configuration = Bugsnag::Configuration.new + configuration.project_root = project_root + + backtrace = [ + "spec/fixtures/crashes/file1.rb:13:in `baz1'", + "./spec/fixtures/crashes/functions.rb:17:in `abc'", + "#{project_root}/file2.rb:19:in `abcdef2'", + ] + + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) + + expect(stacktrace).to eq([ + { + file: "file1.rb", + lineNumber: 13, + method: "baz1", + inProject: true, + code: { + 10 => " end", + 11 => "", + 12 => " def self.baz1", + 13 => " File2.baz2", + 14 => " end", + 15 => "", + 16 => " def self.abc1" + } + }, + { + file: "functions.rb", + lineNumber: 17, + method: "abc", + inProject: true, + code: { + 14 => " raise 'uh oh'", + 15 => "end", + 16 => "", + 17 => "def abc", + 18 => " puts 'abc'", + 19 => "end", + 20 => "" + }, + }, + { + file: "file2.rb", + lineNumber: 19, + method: "abcdef2", + inProject: true, + code: { + 16 => " end", + 17 => "", + 18 => " def self.abcdef2", + 19 => " puts 'abcdef2'", + 20 => " end", + 21 => "", + 22 => " def self.abcdefghi2" + }, + }, + ]) + end end context "file paths" do @@ -102,7 +306,7 @@ "/foo/bar/.bundle/lib/ignore_me.rb:4:in `to_s'", ] - stacktrace = Bugsnag::Stacktrace.new(backtrace, configuration).to_a + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) expect(stacktrace).to eq([ { file: "/foo/bar/app/models/user.rb", lineNumber: 1, method: "something" }, @@ -122,7 +326,7 @@ "abc.rb:1:in `defg'", ] - stacktrace = Bugsnag::Stacktrace.new(backtrace, configuration).to_a + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) expect(stacktrace).to eq([ { code: nil, file: "./foo/bar/baz.rb", lineNumber: 1, method: "something" }, @@ -145,7 +349,7 @@ "#{dir}/../spec/stacktrace_spec.rb:5:in `something_else'", ] - stacktrace = Bugsnag::Stacktrace.new(backtrace, configuration).to_a + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) expect(stacktrace).to eq([ { file: "#{dir}/spec_helper.rb", lineNumber: 1, method: "something" }, @@ -154,6 +358,48 @@ { file: "#{dir}/stacktrace_spec.rb", lineNumber: 5, method: "something_else" }, ]) end + + it "ignores lines in backtrace that it can't parse" do + configuration = Bugsnag::Configuration.new + configuration.send_code = false + + backtrace = [ + "/foo/bar/baz.rb:2:in `to_s'", + "this is not formatted correctly :O", + "/abc/xyz.rb:4:in `to_s'", + ] + + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) + + expect(stacktrace).to eq([ + { file: "/foo/bar/baz.rb", lineNumber: 2, method: "to_s" }, + { file: "/abc/xyz.rb", lineNumber: 4, method: "to_s" }, + ]) + end + + it "trims Gem prefix from paths" do + gem_path = Gem.path.first + + # Sanity check that we have a gem path to strip + expect(gem_path).not_to be_empty + + configuration = Bugsnag::Configuration.new + configuration.send_code = false + + backtrace = [ + "/foo/bar/baz.rb:2:in `to_s'", + "#{gem_path}/abc/xyz.rb:4:in `to_s'", + "/not/gem/path/but/has/gem.rb:6:in `to_s'" + ] + + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) + + expect(stacktrace).to eq([ + { file: "/foo/bar/baz.rb", lineNumber: 2, method: "to_s" }, + { file: "abc/xyz.rb", lineNumber: 4, method: "to_s" }, + { file: "/not/gem/path/but/has/gem.rb", lineNumber: 6, method: "to_s" }, + ]) + end end context "with configurable vendor_path" do @@ -173,13 +419,13 @@ end def out_project_trace(stacktrace) - stacktrace.to_a.map do |trace_line| - trace_line[:file] if !trace_line[:inProject] + stacktrace.map do |trace_line| + trace_line[:file] unless trace_line[:inProject] end.compact end it "marks vendor/ and .bundle/ as out-project by default" do - stacktrace = Bugsnag::Stacktrace.new(backtrace, configuration) + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) expect(out_project_trace(stacktrace)).to eq([ "vendor/lib/ignore_me.rb", @@ -189,7 +435,7 @@ def out_project_trace(stacktrace) it "allows vendor_path to be configured and filters out backtrace file paths" do configuration.vendor_path = /other_vendor\// - stacktrace = Bugsnag::Stacktrace.new(backtrace, configuration) + stacktrace = Bugsnag::Stacktrace.process(backtrace, configuration) expect(out_project_trace(stacktrace)).to eq(["other_vendor/lib/dont.rb"]) end