bugsnag 1.1.5 → 1.2.0.beta

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