bugsnag 4.2.1 → 6.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +12 -0
  3. data/CHANGELOG.md +814 -0
  4. data/README.md +21 -25
  5. data/VERSION +1 -1
  6. data/bugsnag.gemspec +19 -8
  7. data/lib/bugsnag/breadcrumb_type.rb +14 -0
  8. data/lib/bugsnag/breadcrumbs/breadcrumb.rb +109 -0
  9. data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +13 -0
  10. data/lib/bugsnag/breadcrumbs/on_breadcrumb_callback_list.rb +48 -0
  11. data/lib/bugsnag/breadcrumbs/validator.rb +29 -0
  12. data/lib/bugsnag/cleaner.rb +170 -59
  13. data/lib/bugsnag/code_extractor.rb +137 -0
  14. data/lib/bugsnag/configuration.rb +670 -45
  15. data/lib/bugsnag/delivery/synchronous.rb +31 -14
  16. data/lib/bugsnag/delivery/thread_queue.rb +23 -6
  17. data/lib/bugsnag/delivery.rb +13 -0
  18. data/lib/bugsnag/endpoint_configuration.rb +11 -0
  19. data/lib/bugsnag/endpoint_validator.rb +80 -0
  20. data/lib/bugsnag/error.rb +25 -0
  21. data/lib/bugsnag/event.rb +5 -0
  22. data/lib/bugsnag/feature_flag.rb +74 -0
  23. data/lib/bugsnag/helpers.rb +121 -25
  24. data/lib/bugsnag/integrations/delayed_job.rb +51 -0
  25. data/lib/bugsnag/integrations/mailman.rb +43 -0
  26. data/lib/bugsnag/integrations/mongo.rb +133 -0
  27. data/lib/bugsnag/integrations/que.rb +53 -0
  28. data/lib/bugsnag/integrations/rack.rb +83 -0
  29. data/lib/bugsnag/integrations/rails/active_job.rb +100 -0
  30. data/lib/bugsnag/{rails → integrations/rails}/active_record_rescue.rb +10 -1
  31. data/lib/bugsnag/{rails → integrations/rails}/controller_methods.rb +1 -9
  32. data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +115 -0
  33. data/lib/bugsnag/integrations/railtie.rb +153 -0
  34. data/lib/bugsnag/integrations/rake.rb +74 -0
  35. data/lib/bugsnag/integrations/resque.rb +94 -0
  36. data/lib/bugsnag/integrations/shoryuken.rb +50 -0
  37. data/lib/bugsnag/integrations/sidekiq.rb +68 -0
  38. data/lib/bugsnag/meta_data.rb +1 -0
  39. data/lib/bugsnag/middleware/active_job.rb +18 -0
  40. data/lib/bugsnag/middleware/breadcrumbs.rb +21 -0
  41. data/lib/bugsnag/middleware/callbacks.rb +6 -8
  42. data/lib/bugsnag/middleware/classify_error.rb +50 -0
  43. data/lib/bugsnag/middleware/clearance_user.rb +33 -0
  44. data/lib/bugsnag/middleware/delayed_job.rb +93 -0
  45. data/lib/bugsnag/middleware/discard_error_class.rb +30 -0
  46. data/lib/bugsnag/middleware/exception_meta_data.rb +42 -0
  47. data/lib/bugsnag/middleware/ignore_error_class.rb +26 -0
  48. data/lib/bugsnag/middleware/mailman.rb +6 -4
  49. data/lib/bugsnag/middleware/rack_request.rb +126 -30
  50. data/lib/bugsnag/middleware/rails3_request.rb +15 -17
  51. data/lib/bugsnag/middleware/rake.rb +7 -5
  52. data/lib/bugsnag/middleware/session_data.rb +25 -0
  53. data/lib/bugsnag/middleware/sidekiq.rb +9 -4
  54. data/lib/bugsnag/middleware/suggestion_data.rb +34 -0
  55. data/lib/bugsnag/middleware/warden_user.rb +11 -6
  56. data/lib/bugsnag/middleware_stack.rb +62 -9
  57. data/lib/bugsnag/on_error_callbacks.rb +33 -0
  58. data/lib/bugsnag/report.rb +516 -0
  59. data/lib/bugsnag/session_tracker.rb +182 -0
  60. data/lib/bugsnag/stacktrace.rb +82 -0
  61. data/lib/bugsnag/tasks/bugsnag.rake +2 -70
  62. data/lib/bugsnag/utility/circular_buffer.rb +62 -0
  63. data/lib/bugsnag/utility/duplicator.rb +124 -0
  64. data/lib/bugsnag/utility/feature_data_store.rb +41 -0
  65. data/lib/bugsnag/utility/feature_flag_delegate.rb +89 -0
  66. data/lib/bugsnag/utility/metadata_delegate.rb +102 -0
  67. data/lib/bugsnag.rb +528 -80
  68. metadata +61 -123
  69. data/.document +0 -5
  70. data/.gitignore +0 -52
  71. data/.rspec +0 -3
  72. data/.travis.yml +0 -14
  73. data/CONTRIBUTING.md +0 -47
  74. data/Gemfile +0 -2
  75. data/Rakefile +0 -29
  76. data/lib/bugsnag/capistrano.rb +0 -7
  77. data/lib/bugsnag/capistrano2.rb +0 -32
  78. data/lib/bugsnag/delay/resque.rb +0 -21
  79. data/lib/bugsnag/delayed_job.rb +0 -57
  80. data/lib/bugsnag/deploy.rb +0 -34
  81. data/lib/bugsnag/mailman.rb +0 -28
  82. data/lib/bugsnag/middleware/rails2_request.rb +0 -52
  83. data/lib/bugsnag/notification.rb +0 -459
  84. data/lib/bugsnag/rack.rb +0 -53
  85. data/lib/bugsnag/rails/action_controller_rescue.rb +0 -62
  86. data/lib/bugsnag/rails.rb +0 -66
  87. data/lib/bugsnag/railtie.rb +0 -80
  88. data/lib/bugsnag/rake.rb +0 -25
  89. data/lib/bugsnag/resque.rb +0 -40
  90. data/lib/bugsnag/sidekiq.rb +0 -42
  91. data/lib/bugsnag/tasks/bugsnag.cap +0 -48
  92. data/rails/init.rb +0 -7
  93. data/spec/cleaner_spec.rb +0 -138
  94. data/spec/code_spec.rb +0 -86
  95. data/spec/fixtures/crashes/end_of_file.rb +0 -9
  96. data/spec/fixtures/crashes/short_file.rb +0 -1
  97. data/spec/fixtures/crashes/start_of_file.rb +0 -9
  98. data/spec/fixtures/middleware/internal_info_setter.rb +0 -11
  99. data/spec/fixtures/middleware/public_info_setter.rb +0 -11
  100. data/spec/fixtures/tasks/Rakefile +0 -15
  101. data/spec/helper_spec.rb +0 -163
  102. data/spec/integration_spec.rb +0 -132
  103. data/spec/middleware_spec.rb +0 -181
  104. data/spec/notification_spec.rb +0 -877
  105. data/spec/rack_spec.rb +0 -56
  106. data/spec/spec_helper.rb +0 -53
@@ -1,24 +1,37 @@
1
1
  module Bugsnag::Middleware
2
+ ##
3
+ # Extracts and attaches rack data to an error report
2
4
  class RackRequest
5
+ SPOOF = "[SPOOF]".freeze
6
+ COOKIE_HEADER = "Cookie".freeze
7
+
3
8
  def initialize(bugsnag)
4
9
  @bugsnag = bugsnag
5
10
  end
6
11
 
7
- def call(notification)
8
- if notification.request_data[:rack_env]
9
- env = notification.request_data[:rack_env]
12
+ def call(report)
13
+ if report.request_data[:rack_env]
14
+ env = report.request_data[:rack_env]
10
15
 
11
16
  request = ::Rack::Request.new(env)
12
17
 
13
- params = request.params rescue {}
18
+ params =
19
+ # if the request body isn't rewindable then we can't read request.POST
20
+ # which is used internally by request.params
21
+ if request.body.respond_to?(:rewind)
22
+ request.params rescue {}
23
+ else
24
+ request.GET rescue {}
25
+ end
14
26
 
27
+ client_ip = request.ip.to_s rescue SPOOF
15
28
  session = env["rack.session"]
16
29
 
17
- # Set the context
18
- notification.context = "#{request.request_method} #{request.path}"
30
+ # Set the automatic context
31
+ report.automatic_context = "#{request.request_method} #{request.path}"
19
32
 
20
33
  # Set a sensible default for user_id
21
- notification.user_id = request.ip
34
+ report.user["id"] = request.ip
22
35
 
23
36
  # Build the clean url (hide the port if it is obvious)
24
37
  url = "#{request.scheme}://#{request.host}"
@@ -26,53 +39,136 @@ module Bugsnag::Middleware
26
39
 
27
40
  # If app is passed a bad URL, this code will crash attempting to clean it
28
41
  begin
29
- url << Bugsnag::Cleaner.new(notification.configuration.params_filters).clean_url(request.fullpath)
42
+ url << Bugsnag.cleaner.clean_url(request.fullpath)
30
43
  rescue StandardError => stde
31
- Bugsnag.log "RackRequest - Rescued error while cleaning request.fullpath: #{stde}"
44
+ Bugsnag.configuration.warn "RackRequest - Rescued error while cleaning request.fullpath: #{stde}"
32
45
  end
33
46
 
34
- headers = {}
35
-
36
- env.each_pair do |key, value|
37
- if key.to_s.start_with?("HTTP_")
38
- header_key = key[5..-1]
39
- elsif ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(key)
40
- header_key = key
41
- else
42
- next
43
- end
44
-
45
- headers[header_key.split("_").map {|s| s.capitalize}.join("-")] = value
47
+ referer = nil
48
+ begin
49
+ referer = Bugsnag.cleaner.clean_url(request.referer) if request.referer
50
+ rescue StandardError => stde
51
+ Bugsnag.configuration.warn "RackRequest - Rescued error while cleaning request.referer: #{stde}"
46
52
  end
47
53
 
48
54
  # Add a request tab
49
- notification.add_tab(:request, {
55
+ report.add_tab(:request, {
50
56
  :url => url,
51
57
  :httpMethod => request.request_method,
52
58
  :params => params.to_hash,
53
- :referer => request.referer,
54
- :clientIp => request.ip,
55
- :headers => headers
59
+ :referer => referer,
60
+ :clientIp => client_ip,
61
+ :headers => format_headers(env, referer)
56
62
  })
57
63
 
64
+ # add the HTTP version if present
65
+ if env["SERVER_PROTOCOL"]
66
+ report.add_metadata(:request, :httpVersion, env["SERVER_PROTOCOL"])
67
+ end
68
+
69
+ add_request_body(report, request, env)
70
+ add_cookies(report, request)
71
+
58
72
  # Add an environment tab
59
- if notification.configuration.send_environment
60
- notification.add_tab(:environment, env)
73
+ if report.configuration.send_environment
74
+ report.add_tab(:environment, env)
61
75
  end
62
76
 
63
77
  # Add a session tab
64
78
  if session
65
79
  if session.is_a?(Hash)
66
80
  # Rails 3
67
- notification.add_tab(:session, session)
81
+ report.add_tab(:session, session)
68
82
  elsif session.respond_to?(:to_hash)
69
83
  # Rails 4
70
- notification.add_tab(:session, session.to_hash)
84
+ report.add_tab(:session, session.to_hash)
71
85
  end
72
86
  end
73
87
  end
74
88
 
75
- @bugsnag.call(notification)
89
+ @bugsnag.call(report)
90
+ end
91
+
92
+ private
93
+
94
+ def format_headers(env, referer)
95
+ headers = {}
96
+
97
+ env.each_pair do |key, value|
98
+ if key.to_s.start_with?("HTTP_")
99
+ header_key = key[5..-1]
100
+ elsif ["CONTENT_TYPE", "CONTENT_LENGTH"].include?(key)
101
+ header_key = key
102
+ else
103
+ next
104
+ end
105
+
106
+ headers[header_key.split("_").map {|s| s.capitalize}.join("-")] = value
107
+ end
108
+
109
+ headers["Referer"] = referer if headers["Referer"]
110
+
111
+ headers
112
+ end
113
+
114
+ def add_request_body(report, request, env)
115
+ begin
116
+ body = parsed_request_body(request, env)
117
+ rescue StandardError
118
+ return nil
119
+ end
120
+
121
+ # this request may not have a body
122
+ return unless body.is_a?(Hash) && !body.empty?
123
+
124
+ report.add_metadata(:request, :body, body)
125
+ end
126
+
127
+ def parsed_request_body(request, env)
128
+ # if the request is not rewindable then either:
129
+ # - it's been read already and so is impossible to read
130
+ # - it hasn't been read yet and us reading it will prevent the user from
131
+ # reading it themselves
132
+ # in either case we should avoid attempting to
133
+ return nil unless request.body.respond_to?(:rewind)
134
+
135
+ if request.form_data?
136
+ begin
137
+ return request.POST
138
+ ensure
139
+ request.body.rewind
140
+ end
141
+ end
142
+
143
+ content_type = env["CONTENT_TYPE"]
144
+
145
+ return nil if content_type.nil?
146
+ return nil unless content_type.include?('/json') || content_type.include?('+json')
147
+
148
+ begin
149
+ body = request.body
150
+
151
+ JSON.parse(body.read)
152
+ ensure
153
+ # the body must be rewound so other things can read it after we do
154
+ body.rewind
155
+ end
156
+ end
157
+
158
+ def add_cookies(report, request)
159
+ return unless record_cookies?
160
+
161
+ cookies = request.cookies rescue nil
162
+
163
+ return unless cookies.is_a?(Hash) && !cookies.empty?
164
+
165
+ report.add_metadata(:request, :cookies, cookies)
166
+ end
167
+
168
+ def record_cookies?
169
+ # only record cookies in the request if none of the filters match "Cookie"
170
+ # the "Cookie" header will be filtered as normal
171
+ !Bugsnag.cleaner.filters_match?(COOKIE_HEADER)
76
172
  end
77
173
  end
78
174
  end
@@ -1,42 +1,40 @@
1
1
  module Bugsnag::Middleware
2
+ ##
3
+ # Extracts and attaches rails and rack environment data to an error report
2
4
  class Rails3Request
5
+ SPOOF = "[SPOOF]".freeze
6
+
3
7
  def initialize(bugsnag)
4
8
  @bugsnag = bugsnag
5
9
  end
6
10
 
7
- def call(notification)
8
- if notification.request_data[:rack_env]
9
- env = notification.request_data[:rack_env]
11
+ def call(report)
12
+ if report.request_data[:rack_env]
13
+ env = report.request_data[:rack_env]
10
14
  params = env["action_dispatch.request.parameters"]
15
+ client_ip = env["action_dispatch.remote_ip"].to_s rescue SPOOF
11
16
 
12
17
  if params
13
- # Set the context
14
- notification.context = "#{params[:controller]}##{params[:action]}"
18
+ # Set the automatic context
19
+ report.automatic_context = "#{params[:controller]}##{params[:action]}"
15
20
 
16
21
  # Augment the request tab
17
- notification.add_tab(:request, {
22
+ report.add_tab(:request, {
18
23
  :railsAction => "#{params[:controller]}##{params[:action]}",
19
24
  :params => params
20
25
  })
21
26
  end
22
27
 
23
28
  # Use action_dispatch.remote_ip for IP address fields and send request id
24
- notification.add_tab(:request, {
25
- :clientIp => env["action_dispatch.remote_ip"],
29
+ report.add_tab(:request, {
30
+ :clientIp => client_ip,
26
31
  :requestId => env["action_dispatch.request_id"]
27
32
  })
28
33
 
29
- notification.user_id = env["action_dispatch.remote_ip"]
30
-
31
- # Add the rails version
32
- if notification.configuration.send_environment
33
- notification.add_tab(:environment, {
34
- :railsVersion => Rails::VERSION::STRING
35
- })
36
- end
34
+ report.user["id"] = client_ip
37
35
  end
38
36
 
39
- @bugsnag.call(notification)
37
+ @bugsnag.call(report)
40
38
  end
41
39
  end
42
40
  end
@@ -1,23 +1,25 @@
1
1
  module Bugsnag::Middleware
2
+ ##
3
+ # Extracts and attaches rake task information to an error report
2
4
  class Rake
3
5
  def initialize(bugsnag)
4
6
  @bugsnag = bugsnag
5
7
  end
6
8
 
7
- def call(notification)
8
- task = notification.request_data[:bugsnag_running_task]
9
+ def call(report)
10
+ task = report.request_data[:bugsnag_running_task]
9
11
 
10
12
  if task
11
- notification.add_tab(:rake_task, {
13
+ report.add_tab(:rake_task, {
12
14
  :name => task.name,
13
15
  :description => task.full_comment,
14
16
  :arguments => task.arg_description
15
17
  })
16
18
 
17
- notification.context ||= task.name
19
+ report.automatic_context ||= task.name
18
20
  end
19
21
 
20
- @bugsnag.call(notification)
22
+ @bugsnag.call(report)
21
23
  end
22
24
  end
23
25
  end
@@ -0,0 +1,25 @@
1
+ module Bugsnag::Middleware
2
+ ##
3
+ # Attaches information about current session to an error report
4
+ class SessionData
5
+ def initialize(bugsnag)
6
+ @bugsnag = bugsnag
7
+ end
8
+
9
+ def call(report)
10
+ session = Bugsnag::SessionTracker.get_current_session
11
+
12
+ if session && !session[:paused?]
13
+ if report.unhandled
14
+ session[:events][:unhandled] += 1
15
+ else
16
+ session[:events][:handled] += 1
17
+ end
18
+
19
+ report.session = session
20
+ end
21
+
22
+ @bugsnag.call(report)
23
+ end
24
+ end
25
+ end
@@ -1,13 +1,18 @@
1
1
  module Bugsnag::Middleware
2
+ ##
3
+ # Attaches Sidekiq job information to an error report
2
4
  class Sidekiq
3
5
  def initialize(bugsnag)
4
6
  @bugsnag = bugsnag
5
7
  end
6
8
 
7
- def call(notification)
8
- sidekiq = notification.request_data[:sidekiq]
9
- notification.add_tab(:sidekiq, sidekiq) if sidekiq
10
- @bugsnag.call(notification)
9
+ def call(report)
10
+ sidekiq = report.request_data[:sidekiq]
11
+ if sidekiq
12
+ report.add_tab(:sidekiq, sidekiq)
13
+ report.automatic_context ||= "#{sidekiq[:msg]['wrapped'] || sidekiq[:msg]['class']}@#{sidekiq[:msg]['queue']}"
14
+ end
15
+ @bugsnag.call(report)
11
16
  end
12
17
  end
13
18
  end
@@ -0,0 +1,34 @@
1
+ module Bugsnag::Middleware
2
+ ##
3
+ # Attaches any "Did you mean?" suggestions to the report
4
+ class SuggestionData
5
+
6
+ CAPTURE_REGEX = /Did you mean\?([\s\S]+)$/
7
+ DELIMITER = "\n"
8
+
9
+ def initialize(bugsnag)
10
+ @bugsnag = bugsnag
11
+ end
12
+
13
+ def call(event)
14
+ matches = []
15
+
16
+ event.errors.each do |error|
17
+ match = CAPTURE_REGEX.match(error.error_message)
18
+
19
+ next unless match
20
+
21
+ suggestions = match.captures[0].split(DELIMITER)
22
+ matches.concat(suggestions.map(&:strip))
23
+ end
24
+
25
+ if matches.size == 1
26
+ event.add_metadata(:error, { suggestion: matches.first })
27
+ elsif matches.size > 1
28
+ event.add_metadata(:error, { suggestions: matches })
29
+ end
30
+
31
+ @bugsnag.call(event)
32
+ end
33
+ end
34
+ end
@@ -1,4 +1,6 @@
1
1
  module Bugsnag::Middleware
2
+ ##
3
+ # Extracts and attaches user information from Warden to an error report
2
4
  class WardenUser
3
5
  SCOPE_PATTERN = /^warden\.user\.([^.]+)\.key$/
4
6
  COMMON_USER_FIELDS = [:email, :name, :first_name, :last_name, :created_at, :id]
@@ -7,9 +9,9 @@ module Bugsnag::Middleware
7
9
  @bugsnag = bugsnag
8
10
  end
9
11
 
10
- def call(notification)
11
- if notification.request_data[:rack_env] && notification.request_data[:rack_env]["warden"]
12
- env = notification.request_data[:rack_env]
12
+ def call(report)
13
+ if report.request_data[:rack_env] && report.request_data[:rack_env]["warden"]
14
+ env = report.request_data[:rack_env]
13
15
  session = env["rack.session"] || {}
14
16
 
15
17
  # Find all warden user scopes
@@ -21,7 +23,10 @@ module Bugsnag::Middleware
21
23
  # Extract useful user information
22
24
  user = {}
23
25
  user_object = env["warden"].user({:scope => best_scope, :run_callbacks => false}) rescue nil
26
+
24
27
  if user_object
28
+ user[:warden_scope] = best_scope
29
+
25
30
  # Build the user info for this scope
26
31
  COMMON_USER_FIELDS.each do |field|
27
32
  user[field] = user_object.send(field) if user_object.respond_to?(field)
@@ -29,11 +34,11 @@ module Bugsnag::Middleware
29
34
  end
30
35
 
31
36
  # We merge the first warden scope down, so that it is the main "user" for the request
32
- notification.user = user unless user.empty?
37
+ report.user = user unless user.empty?
33
38
  end
34
39
  end
35
40
 
36
- @bugsnag.call(notification)
41
+ @bugsnag.call(report)
37
42
  end
38
43
  end
39
- end
44
+ end
@@ -1,11 +1,19 @@
1
+ require "bugsnag/on_error_callbacks"
2
+
1
3
  module Bugsnag
2
4
  class MiddlewareStack
5
+ ##
6
+ # Creates the middleware stack.
3
7
  def initialize
4
8
  @middlewares = []
5
9
  @disabled_middleware = []
6
10
  @mutex = Mutex.new
7
11
  end
8
12
 
13
+ ##
14
+ # Defines a new middleware to use in the middleware call sequence.
15
+ #
16
+ # Will return early if given middleware is disabled or already included.
9
17
  def use(new_middleware)
10
18
  @mutex.synchronize do
11
19
  return if @disabled_middleware.include?(new_middleware)
@@ -15,6 +23,11 @@ module Bugsnag
15
23
  end
16
24
  end
17
25
 
26
+ ##
27
+ # Inserts a new middleware to use after a given middleware already added.
28
+ #
29
+ # Will return early if given middleware is disabled or already added.
30
+ # New middleware will be inserted last if the existing middleware is not already included.
18
31
  def insert_after(after, new_middleware)
19
32
  @mutex.synchronize do
20
33
  return if @disabled_middleware.include?(new_middleware)
@@ -34,6 +47,11 @@ module Bugsnag
34
47
  end
35
48
  end
36
49
 
50
+ ##
51
+ # Inserts a new middleware to use before a given middleware already added.
52
+ #
53
+ # Will return early if given middleware is disabled or already added.
54
+ # New middleware will be inserted last if the existing middleware is not already included.
37
55
  def insert_before(before, new_middleware)
38
56
  @mutex.synchronize do
39
57
  return if @disabled_middleware.include?(new_middleware)
@@ -49,21 +67,38 @@ module Bugsnag
49
67
  end
50
68
  end
51
69
 
70
+ ##
71
+ # Disable the given middleware. This removes them from the list of
72
+ # middleware and ensures they cannot be added again
73
+ #
74
+ # See also {#remove}
52
75
  def disable(*middlewares)
53
76
  @mutex.synchronize do
54
77
  @disabled_middleware += middlewares
55
78
 
56
- @middlewares.delete_if {|m| @disabled_middleware.include?(m)}
79
+ @middlewares.delete_if {|m| @disabled_middleware.include?(m) }
80
+ end
81
+ end
82
+
83
+ ##
84
+ # Remove the given middleware from the list of middleware
85
+ #
86
+ # This is like {#disable} but allows the middleware to be added again
87
+ def remove(*middlewares)
88
+ @mutex.synchronize do
89
+ @middlewares.delete_if {|m| middlewares.include?(m) }
57
90
  end
58
91
  end
59
92
 
60
- # This allows people to proxy methods to the array if they want to do more complex stuff
93
+ ##
94
+ # Allows the user to proxy methods for more complex functionality.
61
95
  def method_missing(method, *args, &block)
62
96
  @middlewares.send(method, *args, &block)
63
97
  end
64
98
 
65
- # Runs the middleware stack and calls
66
- def run(notification)
99
+ ##
100
+ # Runs the middleware stack.
101
+ def run(report)
67
102
  # The final lambda is the termination of the middleware stack. It calls deliver on the notification
68
103
  lambda_has_run = false
69
104
  notify_lambda = lambda do |notif|
@@ -73,26 +108,44 @@ module Bugsnag
73
108
 
74
109
  begin
75
110
  # We reverse them, so we can call "call" on the first middleware
76
- middleware_procs.reverse.inject(notify_lambda) { |n,e| e.call(n) }.call(notification)
111
+ middleware_procs.reverse.inject(notify_lambda) {|n, e| e.call(n) }.call(report)
77
112
  rescue StandardError => e
78
113
  # KLUDGE: Since we don't re-raise middleware exceptions, this breaks rspec
79
114
  raise if e.class.to_s == "RSpec::Expectations::ExpectationNotMetError"
80
115
 
81
116
  # We dont notify, as we dont want to loop forever in the case of really broken middleware, we will
82
117
  # still send this notify
83
- Bugsnag.warn "Bugsnag middleware error: #{e}"
84
- Bugsnag.log "Middleware error stacktrace: #{e.backtrace.inspect}"
118
+ Bugsnag.configuration.warn "Bugsnag middleware error: #{e}"
119
+ Bugsnag.configuration.warn "Middleware error stacktrace: #{e.backtrace.inspect}"
85
120
  end
86
121
 
87
122
  # Ensure that the deliver has been performed, and no middleware has botched it
88
- notify_lambda.call(notification) unless lambda_has_run
123
+ notify_lambda.call(report) unless lambda_has_run
89
124
  end
90
125
 
91
126
  private
127
+
128
+ ##
92
129
  # Generates a list of middleware procs that are ready to be run
93
130
  # Pass each one a reference to the next in the queue
131
+ #
132
+ # @return [Array<Proc>]
94
133
  def middleware_procs
95
- @middlewares.map{|middleware| proc { |next_middleware| middleware.new(next_middleware) } }
134
+ # Split the middleware into separate lists of callables (e.g. Proc, Lambda, Method) and Classes
135
+ callables, classes = @middlewares.partition {|middleware| middleware.respond_to?(:call) }
136
+
137
+ # Wrap the classes in a proc that, when called, news up the middleware and
138
+ # passes the next middleware in the queue
139
+ middleware_instances = classes.map do |middleware|
140
+ proc {|next_middleware| middleware.new(next_middleware) }
141
+ end
142
+
143
+ # Wrap the list of callables in a proc that, when called, wraps them in an
144
+ # 'OnErrorCallbacks' instance that also has a reference to the next middleware
145
+ wrapped_callables = proc {|next_middleware| OnErrorCallbacks.new(next_middleware, callables) }
146
+
147
+ # Return the combined middleware and wrapped callables
148
+ middleware_instances.push(wrapped_callables)
96
149
  end
97
150
  end
98
151
  end
@@ -0,0 +1,33 @@
1
+ module Bugsnag
2
+ # @api private
3
+ class OnErrorCallbacks
4
+ def initialize(next_middleware, callbacks)
5
+ @next_middleware = next_middleware
6
+ @callbacks = callbacks
7
+ end
8
+
9
+ ##
10
+ # @param report [Report]
11
+ def call(report)
12
+ @callbacks.each do |callback|
13
+ begin
14
+ should_continue = callback.call(report)
15
+ rescue StandardError => e
16
+ Bugsnag.configuration.warn("Error occurred in on_error callback: '#{e}'")
17
+ Bugsnag.configuration.warn("on_error callback stacktrace: #{e.backtrace.inspect}")
18
+ end
19
+
20
+ # If a callback returns false, we ignore the report and stop running callbacks
21
+ # Note that we explicitly check for 'false' so that callbacks don't need
22
+ # to return anything (i.e. can return 'nil') and we still continue
23
+ next unless should_continue == false
24
+
25
+ report.ignore!
26
+
27
+ break
28
+ end
29
+
30
+ @next_middleware.call(report)
31
+ end
32
+ end
33
+ end