bugsnag 1.1.5 → 1.2.0.beta

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.
@@ -1,34 +1,48 @@
1
+ module HTTParty
2
+ class Parser
3
+ def json
4
+ Bugsnag::Helpers.load_json(body)
5
+ end
6
+ end
7
+ end
8
+
1
9
  module Bugsnag
2
10
  module Helpers
3
11
  MAX_STRING_LENGTH = 4096
4
12
 
5
- def self.cleanup_hash(hash)
6
- return nil unless hash
7
- hash.inject({}) do |h, (k, v)|
8
- h[k.to_s.gsub(/\./, "-")] = v.to_s.slice(0, MAX_STRING_LENGTH)
9
- h
10
- end
11
- end
12
-
13
- def self.apply_filters(hash, filters)
14
- return nil unless hash
15
- return hash unless filters
13
+ def self.cleanup_obj(obj, filters = nil)
14
+ return nil unless obj
16
15
 
17
- hash.each do |k, v|
18
- if filters.any? {|f| k.to_s.include?(f.to_s) }
19
- hash[k] = "[FILTERED]"
20
- elsif v.respond_to?(:to_hash)
21
- apply_filters(hash[k])
16
+ if obj.is_a?(Hash)
17
+ clean_hash = {}
18
+ obj.each do |k,v|
19
+ if filters && filters.any? {|f| k.to_s.include?(f.to_s)}
20
+ clean_hash[k] = "[FILTERED]"
21
+ else
22
+ clean_obj = cleanup_obj(v, filters)
23
+ clean_hash[k] = clean_obj unless clean_obj.nil?
24
+ end
22
25
  end
26
+ clean_hash
27
+ elsif obj.is_a?(Array) || obj.is_a?(Set)
28
+ obj.map { |el| cleanup_obj(el, filters) }.compact
29
+ elsif obj.is_a?(Integer) || obj.is_a?(Float)
30
+ obj
31
+ else
32
+ obj.to_s unless obj.to_s =~ /#<.*>/
23
33
  end
24
34
  end
25
35
 
26
- def self.param_context(params)
27
- "#{params[:controller]}##{params[:action]}" if params && params[:controller] && params[:action]
28
- end
36
+ def self.reduce_hash_size(hash)
37
+ hash.inject({}) do |h, (k,v)|
38
+ if v.is_a?(Hash)
39
+ h[k] = reduce_hash_size(v)
40
+ else
41
+ h[k] = v.to_s.slice(0, MAX_STRING_LENGTH)
42
+ end
29
43
 
30
- def self.request_context(request)
31
- "#{request.request_method} #{request.path}" if request
44
+ h
45
+ end
32
46
  end
33
47
 
34
48
  # Helper functions to work around MultiJson changes in 1.3+
@@ -0,0 +1,19 @@
1
+ module Bugsnag::Middleware
2
+ class Callbacks
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ if notification.request_data[:before_callbacks]
9
+ notification.request_data[:before_callbacks].each {|c| c.call(*[notification][0...c.arity]) }
10
+ end
11
+
12
+ @bugsnag.call(notification)
13
+
14
+ if notification.request_data[:after_callbacks]
15
+ notification.request_data[:after_callbacks].each {|c| c.call(*[notification][0...c.arity]) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ module Bugsnag::Middleware
2
+ class RackRequest
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ if notification.request_data[:rack_env]
9
+ env = notification.request_data[:rack_env]
10
+
11
+ request = ::Rack::Request.new(env)
12
+ params = request.params
13
+ session = env["rack.session"]
14
+
15
+ # Set the context
16
+ notification.context = "#{request.request_method} #{request.path}"
17
+
18
+ # Set a sensible default for user_id
19
+ notification.user_id = request.ip
20
+
21
+ # Build the clean url (hide the port if it is obvious)
22
+ url = "#{request.scheme}://#{request.host}"
23
+ url << ":#{request.port}" unless [80, 443].include?(request.port)
24
+ url << request.fullpath
25
+
26
+ # Add a request tab
27
+ notification.add_tab(:request, {
28
+ :url => url,
29
+ :params => params.to_hash,
30
+ :userAgent => request.user_agent,
31
+ :clientIp => request.ip
32
+ })
33
+
34
+ # Add an environment tab
35
+ notification.add_tab(:environment, env)
36
+
37
+ # Add a session tab
38
+ notification.add_tab(:session, session) if session
39
+
40
+ # Add a cookies tab
41
+ cookies = request.cookies
42
+ notification.add_tab(:cookies, cookies) if cookies
43
+ end
44
+
45
+ @bugsnag.call(notification)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ module Bugsnag::Middleware
2
+ class Rails2Request
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ if notification.request_data[:rails2_request]
9
+ request = notification.request_data[:rails2_request]
10
+ params = request.parameters || {}
11
+ session_data = request.session.respond_to?(:to_hash) ? request.session.to_hash : request.session.data
12
+
13
+ # Set the context
14
+ notification.context = "#{params[:controller]}##{params[:action]}"
15
+
16
+ # Set a sensible default for user_id
17
+ notification.user_id = request.remote_ip if request.respond_to?(:remote_ip)
18
+
19
+ # Build the clean url
20
+ url = "#{request.protocol}#{request.host}"
21
+ url << ":#{request.port}" unless [80, 443].include?(request.port)
22
+ url << request.fullpath
23
+
24
+ # Add a request tab
25
+ notification.add_tab(:request, {
26
+ :url => url,
27
+ :params => params.to_hash,
28
+ :controller => params[:controller],
29
+ :action => params[:action]
30
+ })
31
+
32
+ # Add an environment tab
33
+ notification.add_tab(:environment, request.env) if request.env
34
+
35
+ # Add a session tab
36
+ notification.add_tab(:session, session_data) if session_data
37
+
38
+ # Add a cookies tab
39
+ notification.add_tab(:cookies, request.cookies) if request.cookies
40
+ end
41
+
42
+ @bugsnag.call(notification)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module Bugsnag::Middleware
2
+ class Rails3Request
3
+ def initialize(bugsnag)
4
+ @bugsnag = bugsnag
5
+ end
6
+
7
+ def call(notification)
8
+ if notification.request_data[:rack_env]
9
+ env = notification.request_data[:rack_env]
10
+ params = env["action_dispatch.request.parameters"]
11
+
12
+ # Set the context
13
+ notification.context = "#{params[:controller]}##{params[:action]}"
14
+
15
+ # Augment the request tab
16
+ if params
17
+ notification.add_tab(:request, {
18
+ :railsAction => "#{params[:controller]}##{params[:action]}",
19
+ :params => params
20
+ })
21
+ end
22
+ end
23
+
24
+ @bugsnag.call(notification)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ module Bugsnag::Middleware
2
+ class WardenUser
3
+ SCOPE_PATTERN = /^warden\.user\.([^.]+)\.key$/
4
+ COMMON_USER_FIELDS = [:email, :name, :first_name, :last_name, :created_at]
5
+
6
+ def initialize(bugsnag)
7
+ @bugsnag = bugsnag
8
+ end
9
+
10
+ def call(notification)
11
+ if notification.request_data[:rack_env] && notification.request_data[:rack_env]["warden"]
12
+ env = notification.request_data[:rack_env]
13
+ session = env["rack.session"] || {}
14
+
15
+ # Find all warden user scopes
16
+ warden_scopes = session.keys.select {|k| k.match(SCOPE_PATTERN)}.map {|k| k.gsub(SCOPE_PATTERN, '\1')}
17
+ unless warden_scopes.empty?
18
+ # Pick the best scope for unique id (the default is "user")
19
+ best_scope = warden_scopes.include?("user") ? "user" : warden_scopes.first
20
+
21
+ # Set the user_id
22
+ if best_scope
23
+ user_id = session[best_scope][1][0] rescue nil
24
+ notification.user_id = user_id unless user_id.nil?
25
+ end
26
+
27
+ # Extract useful user information
28
+ warden_tab = {}
29
+ warden_scopes.each do |scope|
30
+ user_object = env["warden"].user({:scope => scope, :run_callbacks => false}) rescue nil
31
+ if user_object
32
+ # Build the user info for this scope
33
+ scope_hash = warden_tab["Warden #{scope.capitalize}"] = {}
34
+ COMMON_USER_FIELDS.each do |field|
35
+ scope_hash[field] = user_object.send(field) if user_object.respond_to?(field)
36
+ end
37
+ end
38
+ end
39
+
40
+ notification.add_tab(:user, warden_tab) unless warden_tab.empty?
41
+ end
42
+ end
43
+
44
+ @bugsnag.call(notification)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ module Bugsnag
2
+ class MiddlewareStack
3
+ def initialize
4
+ @middlewares = []
5
+ end
6
+
7
+ def use(new_middleware)
8
+ @middlewares << new_middleware
9
+ end
10
+
11
+ def delete(middleware)
12
+ @middlewares.delete(middleware)
13
+ end
14
+
15
+ def insert_after(after, new_middleware)
16
+ index = (@middlewares.rindex(after) + 1)
17
+ if index >= @middlewares.length
18
+ @middlewares << new_middleware
19
+ else
20
+ @middlewares.insert index, new_middleware
21
+ end
22
+ end
23
+
24
+ def insert_before(before, new_middleware)
25
+ index = @middlewares.index(before) || @middlewares.length
26
+ @middlewares.insert index, new_middleware
27
+ end
28
+
29
+ # This allows people to proxy methods to the array if they want to do more complex stuff
30
+ def method_missing(method, *args, &block)
31
+ @middlewares.send(method, *args, &block)
32
+ end
33
+
34
+ # Runs the middleware stack and calls
35
+ def run(notification)
36
+ # The final lambda is the termination of the middleware stack. It calls deliver on the notification
37
+ lambda_has_run = false
38
+ notify_lambda = lambda do |notification|
39
+ lambda_has_run = true
40
+ yield
41
+ end
42
+
43
+ begin
44
+ # We reverse them, so we can call "call" on the first middleware
45
+ middleware_procs.reverse.inject(notify_lambda) { |n,e| e[n] }.call(notification)
46
+ rescue Exception => e
47
+ # We dont notify, as we dont want to loop forever in the case of really broken middleware, we will
48
+ # still send this notify
49
+ Bugsnag.warn "Bugsnag middleware error: #{e}"
50
+ Bugsnag.log "Middleware error stacktrace: #{e.backtrace.inspect}"
51
+ end
52
+
53
+ # Ensure that the deliver has been performed, and no middleware has botched it
54
+ notify_lambda.call(notification) unless lambda_has_run
55
+ end
56
+
57
+ private
58
+ # Generates a list of middleware procs that are ready to be run
59
+ # Pass each one a reference to the next in the queue
60
+ def middleware_procs
61
+ @middlewares.map{|middleware| proc { |next_middleware| middleware.new(next_middleware) } }
62
+ end
63
+ end
64
+ end
@@ -1,5 +1,6 @@
1
1
  require "httparty"
2
2
  require "multi_json"
3
+ require "pathname"
3
4
 
4
5
  module Bugsnag
5
6
  class Notification
@@ -9,37 +10,43 @@ module Bugsnag
9
10
  NOTIFIER_VERSION = Bugsnag::VERSION
10
11
  NOTIFIER_URL = "http://www.bugsnag.com"
11
12
 
12
- DEFAULT_ENDPOINT = "notify.bugsnag.com"
13
-
14
13
  # HTTParty settings
15
14
  headers "Content-Type" => "application/json"
16
15
  default_timeout 5
17
-
18
- # Basic notification attributes
19
- attr_accessor :exceptions
20
-
21
- # Attributes from session
22
- attr_accessor :user_id, :context, :meta_data
23
-
24
- # Attributes from configuration
25
- attr_accessor :api_key, :params_filters, :stacktrace_filters,
26
- :ignore_classes, :endpoint, :app_version, :release_stage,
27
- :notify_release_stages, :project_root, :use_ssl
28
-
29
-
30
- def self.deliver_exception_payload(endpoint, payload_string)
31
- begin
32
- response = post(endpoint, {:body => payload_string})
33
- rescue Exception => e
34
- Bugsnag.log("Notification to #{endpoint} failed, #{e.inspect}")
16
+
17
+ attr_accessor :context
18
+ attr_accessor :user_id
19
+
20
+ class << self
21
+ def deliver_exception_payload(endpoint, payload)
22
+ begin
23
+ payload_string = Bugsnag::Helpers.dump_json(payload)
24
+
25
+ # If the payload is going to be too long, we trim the hashes to send
26
+ # a minimal payload instead
27
+ if payload_string.length > 512000
28
+ payload = Bugsnag::Helpers.reduce_hash_size(payload)
29
+ payload_string = Bugsnag::Helpers.dump_json(payload)
30
+ end
31
+
32
+ response = post(endpoint, {:body => payload_string})
33
+ rescue Exception => e
34
+ Bugsnag.log("Notification to #{endpoint} failed, #{e.inspect}")
35
+ end
35
36
  end
36
37
  end
37
38
 
38
- def initialize(exception, opts={})
39
- self.exceptions = []
39
+ def initialize(exception, configuration, overrides = nil, request_data = nil)
40
+ @configuration = configuration
41
+ @overrides = overrides || {}
42
+ @request_data = request_data
43
+ @meta_data = {}
44
+
45
+ # Unwrap exceptions
46
+ @exceptions = []
40
47
  ex = exception
41
48
  while ex != nil
42
- self.exceptions << ex
49
+ @exceptions << ex
43
50
 
44
51
  if ex.respond_to?(:continued_exception) && ex.continued_exception
45
52
  ex = ex.continued_exception
@@ -49,60 +56,126 @@ module Bugsnag
49
56
  ex = nil
50
57
  end
51
58
  end
59
+ end
60
+
61
+ # Add a single value as custom data, to this notification
62
+ def add_custom_data(name, value)
63
+ @meta_data[:custom] ||= {}
64
+ @meta_data[:custom][name.to_sym] = value
65
+ end
66
+
67
+ # Add a new tab to this notification
68
+ def add_tab(name, value)
69
+ return if name.nil?
52
70
 
53
- opts.reject! {|k,v| v.nil?}.each do |k,v|
54
- self.send("#{k}=", v) if self.respond_to?("#{k}=")
71
+ if value.is_a? Hash
72
+ @meta_data[name.to_sym] ||= {}
73
+ @meta_data[name.to_sym].merge! value
74
+ else
75
+ self.add_custom_data(name, value)
76
+ Bugsnag.warn "Adding a tab requires a hash, adding to custom tab instead (name=#{name})"
55
77
  end
56
78
  end
57
79
 
80
+ # Remove a tab from this notification
81
+ def remove_tab(name)
82
+ return if name.nil?
83
+
84
+ @meta_data.delete(name.to_sym)
85
+ end
86
+
87
+ # Deliver this notification to bugsnag.com Also runs through the middleware as required.
58
88
  def deliver
59
- return unless self.notify_release_stages.include?(self.release_stage)
60
-
61
- unless self.api_key
89
+ return unless @configuration.should_notify?
90
+
91
+ # Check we have at least and api_key
92
+ unless @configuration.api_key
62
93
  Bugsnag.warn "No API key configured, couldn't notify"
63
94
  return
64
95
  end
65
96
 
66
- endpoint = (self.use_ssl ? "https://" : "http://") + (self.endpoint || DEFAULT_ENDPOINT)
97
+ @meta_data = {}
98
+
99
+ # Run the middleware here, at the end of the middleware stack, execute the actual delivery
100
+ @configuration.middleware.run(self) do
101
+ # Now override the required fields
102
+ [:user_id, :context].each do |symbol|
103
+ if @overrides[symbol]
104
+ self.send("#{symbol}=", @overrides[symbol] )
105
+ @overrides.delete symbol
106
+ end
107
+ end
67
108
 
68
- Bugsnag.log("Notifying #{endpoint} of exception")
109
+ # Build the endpoint url
110
+ endpoint = (@configuration.use_ssl ? "https://" : "http://") + @configuration.endpoint
111
+ Bugsnag.log("Notifying #{endpoint} of #{@exceptions.last.class}")
69
112
 
70
- payload = {
71
- :apiKey => self.api_key,
72
- :notifier => notifier_identification,
73
- :events => [{
74
- :userId => self.user_id,
75
- :appVersion => self.app_version,
76
- :releaseStage => self.release_stage,
113
+ # Build the payload's exception event
114
+ payload_event = {
115
+ :releaseStage => @configuration.release_stage,
116
+ :appVersion => @configuration.app_version,
77
117
  :context => self.context,
118
+ :userId => self.user_id,
78
119
  :exceptions => exception_list,
79
- :metaData => self.meta_data
80
- }.reject {|k,v| v.nil? }]
81
- }
120
+ :metaData => Bugsnag::Helpers.cleanup_obj(generate_meta_data(@overrides), @configuration.params_filters)
121
+ }.reject {|k,v| v.nil? }
122
+
123
+ # Build the payload hash
124
+ payload = {
125
+ :apiKey => @configuration.api_key,
126
+ :notifier => {
127
+ :name => NOTIFIER_NAME,
128
+ :version => NOTIFIER_VERSION,
129
+ :url => NOTIFIER_URL
130
+ },
131
+ :events => [payload_event]
132
+ }
82
133
 
83
- self.class.deliver_exception_payload(endpoint, Bugsnag::Helpers.dump_json(payload))
134
+ self.class.deliver_exception_payload(endpoint, payload)
135
+ end
84
136
  end
85
137
 
86
138
  def ignore?
87
- self.ignore_classes.include?(error_class(self.exceptions.last))
139
+ @configuration.ignore_classes.include?(error_class(@exceptions.last))
88
140
  end
89
141
 
142
+ def request_data
143
+ @request_data || Bugsnag.configuration.request_data
144
+ end
145
+
146
+ def exceptions
147
+ @exceptions
148
+ end
90
149
 
91
150
  private
92
- def notifier_identification
93
- unless @notifier
94
- @notifier = {
95
- :name => NOTIFIER_NAME,
96
- :version => NOTIFIER_VERSION,
97
- :url => NOTIFIER_URL
98
- }
151
+ # Generate the meta data from both the request configuration and the overrides for this notification
152
+ def generate_meta_data(overrides)
153
+ # Copy the request meta data so we dont edit it by mistake
154
+ meta_data = @meta_data.dup
155
+
156
+ overrides.each do |key, value|
157
+ # If its a hash, its a tab so we can just add it providing its not reserved
158
+ if value.is_a? Hash
159
+ key = key.to_sym
160
+
161
+ if meta_data[key]
162
+ # If its a clash, merge with the existing data
163
+ meta_data[key].merge! value
164
+ else
165
+ # Add it as is if its not special
166
+ meta_data[key] = value
167
+ end
168
+ else
169
+ meta_data[:custom] ||= {}
170
+ meta_data[:custom][key] = value
171
+ end
99
172
  end
100
-
101
- @notifier
173
+
174
+ meta_data
102
175
  end
103
-
104
- def exception_list
105
- self.exceptions.map do |exception|
176
+
177
+ def exception_list
178
+ @exceptions.map do |exception|
106
179
  {
107
180
  :errorClass => error_class(exception),
108
181
  :message => exception.message,
@@ -118,16 +191,32 @@ module Bugsnag
118
191
  end
119
192
 
120
193
  def stacktrace(exception)
121
- (exception.backtrace || caller).map do |trace|
194
+ (exception.backtrace || caller).map do |trace|
122
195
  method = nil
123
196
  file, line_str, method_str = trace.split(":")
124
197
 
198
+ next(nil) if file =~ %r{lib/bugsnag}
199
+
200
+ # Expand relative paths
201
+ file = Pathname.new(file).realpath.to_s rescue file
202
+
125
203
  # Generate the stacktrace line hash
126
204
  trace_hash = {}
127
- trace_hash[:inProject] = true if self.project_root && file.match(/^#{self.project_root}/) && !file.match(/vendor\//)
128
- trace_hash[:file] = self.stacktrace_filters.inject(file) {|file, proc| proc.call(file) }
205
+ trace_hash[:inProject] = true if @configuration.project_root && file.match(/^#{@configuration.project_root}/) && !file.match(/vendor\//)
129
206
  trace_hash[:lineNumber] = line_str.to_i
130
207
 
208
+ # Clean up the file path in the stacktrace
209
+ if defined?(Bugsnag.configuration.project_root) && Bugsnag.configuration.project_root.to_s != ''
210
+ file.sub!(/#{Bugsnag.configuration.project_root}\//, "")
211
+ end
212
+
213
+ # Strip common gem path prefixes
214
+ if defined?(Gem)
215
+ file = Gem.path.inject(file) {|line, path| line.sub(/#{path}\//, "") }
216
+ end
217
+
218
+ trace_hash[:file] = file
219
+
131
220
  # Add a method if we have it
132
221
  if method_str
133
222
  method_match = /in `([^']+)'/.match(method_str)