rollbar 2.12.0 → 2.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -6
  3. data/README.md +58 -8
  4. data/docs/configuration.md +12 -0
  5. data/gemfiles/rails30.gemfile +1 -0
  6. data/gemfiles/rails31.gemfile +1 -0
  7. data/gemfiles/rails32.gemfile +1 -0
  8. data/gemfiles/rails40.gemfile +3 -0
  9. data/gemfiles/rails41.gemfile +1 -0
  10. data/gemfiles/rails42.gemfile +7 -1
  11. data/gemfiles/rails50.gemfile +2 -1
  12. data/gemfiles/ruby_1_8_and_1_9_2.gemfile +3 -1
  13. data/lib/rollbar.rb +70 -654
  14. data/lib/rollbar/configuration.rb +32 -0
  15. data/lib/rollbar/item.rb +16 -6
  16. data/lib/rollbar/item/backtrace.rb +26 -17
  17. data/lib/rollbar/item/frame.rb +112 -0
  18. data/lib/rollbar/middleware/js.rb +39 -35
  19. data/lib/rollbar/middleware/rails/rollbar.rb +3 -3
  20. data/lib/rollbar/notifier.rb +645 -0
  21. data/lib/rollbar/plugins/delayed_job/job_data.rb +40 -21
  22. data/lib/rollbar/plugins/rails.rb +2 -2
  23. data/lib/rollbar/plugins/rake.rb +32 -6
  24. data/lib/rollbar/plugins/resque.rb +11 -0
  25. data/lib/rollbar/plugins/resque/failure.rb +39 -0
  26. data/lib/rollbar/plugins/validations.rb +10 -0
  27. data/lib/rollbar/request_data_extractor.rb +36 -18
  28. data/lib/rollbar/scrubbers/params.rb +2 -1
  29. data/lib/rollbar/truncation.rb +1 -1
  30. data/lib/rollbar/truncation/frames_strategy.rb +2 -1
  31. data/lib/rollbar/truncation/min_body_strategy.rb +2 -1
  32. data/lib/rollbar/truncation/strings_strategy.rb +1 -1
  33. data/lib/rollbar/version.rb +1 -1
  34. data/spec/controllers/home_controller_spec.rb +13 -24
  35. data/spec/delayed/backend/test.rb +1 -0
  36. data/spec/requests/home_spec.rb +1 -1
  37. data/spec/rollbar/configuration_spec.rb +22 -0
  38. data/spec/rollbar/item/backtrace_spec.rb +26 -0
  39. data/spec/rollbar/item/frame_spec.rb +267 -0
  40. data/spec/rollbar/item_spec.rb +27 -2
  41. data/spec/rollbar/middleware/js_spec.rb +23 -0
  42. data/spec/rollbar/middleware/sinatra_spec.rb +7 -7
  43. data/spec/rollbar/notifier_spec.rb +43 -0
  44. data/spec/rollbar/plugins/delayed_job/{job_data.rb → job_data_spec.rb} +15 -2
  45. data/spec/rollbar/plugins/rack_spec.rb +7 -7
  46. data/spec/rollbar/plugins/rake_spec.rb +1 -2
  47. data/spec/rollbar/plugins/resque/failure_spec.rb +36 -0
  48. data/spec/rollbar/request_data_extractor_spec.rb +103 -1
  49. data/spec/rollbar/truncation/min_body_strategy_spec.rb +1 -1
  50. data/spec/rollbar/truncation/strings_strategy_spec.rb +2 -2
  51. data/spec/rollbar_bc_spec.rb +4 -4
  52. data/spec/rollbar_spec.rb +99 -37
  53. data/spec/spec_helper.rb +2 -2
  54. data/spec/support/notifier_helpers.rb +2 -0
  55. metadata +16 -4
@@ -2,6 +2,7 @@ require 'logger'
2
2
 
3
3
  module Rollbar
4
4
  class Configuration
5
+ SEND_EXTRA_FRAME_DATA_OPTIONS = [:none, :app, :all].freeze
5
6
 
6
7
  attr_accessor :access_token
7
8
  attr_accessor :async_handler
@@ -52,6 +53,7 @@ module Rollbar
52
53
  attr_accessor :use_eventmachine
53
54
  attr_accessor :web_base
54
55
  attr_accessor :write_to_file
56
+ attr_reader :send_extra_frame_data
55
57
 
56
58
  attr_reader :project_gem_paths
57
59
 
@@ -111,6 +113,8 @@ module Rollbar
111
113
  @verify_ssl_peer = true
112
114
  @web_base = DEFAULT_WEB_BASE
113
115
  @write_to_file = false
116
+ @send_extra_frame_data = :none
117
+ @project_gem_paths = []
114
118
  end
115
119
 
116
120
  def initialize_copy(orig)
@@ -122,6 +126,24 @@ module Rollbar
122
126
  end
123
127
  end
124
128
 
129
+ def merge(options)
130
+ new_configuration = clone
131
+ new_configuration.merge!(options)
132
+
133
+ new_configuration
134
+ end
135
+
136
+ def merge!(options)
137
+ options.each do |name, value|
138
+ variable_name = "@#{name}"
139
+ next unless instance_variable_defined?(variable_name)
140
+
141
+ instance_variable_set(variable_name, value)
142
+ end
143
+
144
+ self
145
+ end
146
+
125
147
  def use_delayed_job
126
148
  require 'rollbar/delay/delayed_job'
127
149
  @use_async = true
@@ -192,6 +214,16 @@ module Rollbar
192
214
  @transform = Array(handler)
193
215
  end
194
216
 
217
+ def send_extra_frame_data=(value)
218
+ unless SEND_EXTRA_FRAME_DATA_OPTIONS.include?(value)
219
+ logger.warning("Wrong 'send_extra_frame_data' value, :none, :app or :full is expected")
220
+
221
+ return
222
+ end
223
+
224
+ @send_extra_frame_data = value
225
+ end
226
+
195
227
  # allow params to be read like a hash
196
228
  def [](option)
197
229
  send(option)
data/lib/rollbar/item.rb CHANGED
@@ -10,6 +10,8 @@ end
10
10
  require 'rollbar/item/backtrace'
11
11
  require 'rollbar/util'
12
12
  require 'rollbar/encoding'
13
+ require 'rollbar/truncation'
14
+ require 'rollbar/json'
13
15
 
14
16
  module Rollbar
15
17
  # This class represents the payload to be sent to the API.
@@ -82,7 +84,7 @@ module Rollbar
82
84
  },
83
85
  :body => build_body
84
86
  }
85
- data[:project_package_paths] = configuration.project_gem_paths if configuration.project_gem_paths
87
+ data[:project_package_paths] = configuration.project_gem_paths if configuration.project_gem_paths.any?
86
88
  data[:code_version] = configuration.code_version if configuration.code_version
87
89
  data[:uuid] = SecureRandom.uuid if defined?(SecureRandom) && SecureRandom.respond_to?(:uuid)
88
90
 
@@ -101,16 +103,24 @@ module Rollbar
101
103
  # from an async handler job, which can be serialized.
102
104
  stringified_payload = Util::Hash.deep_stringify_keys(payload)
103
105
  result = Truncation.truncate(stringified_payload)
106
+
104
107
  return result unless Truncation.truncate?(result)
105
108
 
106
- original_size = Rollbar::JSON.dump(payload).bytesize
107
- final_size = result.bytesize
108
- notifier.send_failsafe("Could not send payload due to it being too large after truncating attempts. Original size: #{original_size} Final size: #{final_size}", nil)
109
- logger.error("[Rollbar] Payload too large to be sent: #{Rollbar::JSON.dump(payload)}")
109
+ handle_too_large_payload(stringified_payload, result)
110
110
 
111
111
  nil
112
112
  end
113
113
 
114
+ def handle_too_large_payload(stringified_payload, final_payload)
115
+ original_size = Rollbar::JSON.dump(stringified_payload).bytesize
116
+ final_size = final_payload.bytesize
117
+ uuid = stringified_payload['data']['uuid']
118
+ host = stringified_payload['data'].fetch('server', {})['host']
119
+
120
+ notifier.send_failsafe("Could not send payload due to it being too large after truncating attempts. Original size: #{original_size} Final size: #{final_size}", nil, uuid, host)
121
+ logger.error("[Rollbar] Payload too large to be sent for UUID #{uuid}: #{Rollbar::JSON.dump(payload)}")
122
+ end
123
+
114
124
  def ignored?
115
125
  data = payload['data']
116
126
 
@@ -140,7 +150,7 @@ module Rollbar
140
150
  :configuration => configuration
141
151
  )
142
152
 
143
- backtrace.build
153
+ backtrace.to_h
144
154
  end
145
155
 
146
156
  def build_extra
@@ -1,3 +1,5 @@
1
+ require 'rollbar/item/frame'
2
+
1
3
  module Rollbar
2
4
  class Item
3
5
  class Backtrace
@@ -5,15 +7,19 @@ module Rollbar
5
7
  attr_reader :message
6
8
  attr_reader :extra
7
9
  attr_reader :configuration
10
+ attr_reader :files
11
+
12
+ private :files
8
13
 
9
14
  def initialize(exception, options = {})
10
15
  @exception = exception
11
16
  @message = options[:message]
12
17
  @extra = options[:extra]
13
18
  @configuration = options[:configuration]
19
+ @files = {}
14
20
  end
15
21
 
16
- def build
22
+ def to_h
17
23
  traces = trace_chain
18
24
 
19
25
  traces[0][:exception][:description] = message if message
@@ -26,10 +32,23 @@ module Rollbar
26
32
  end
27
33
  end
28
34
 
35
+ alias_method :build, :to_h
36
+
37
+ def get_file_lines(filename)
38
+ files[filename] ||= read_file(filename)
39
+ end
40
+
29
41
  private
30
42
 
43
+ def read_file(filename)
44
+ return unless File.exist?(filename)
45
+
46
+ File.read(filename).split("\n")
47
+ rescue
48
+ nil
49
+ end
50
+
31
51
  def trace_chain
32
- exception
33
52
  traces = [trace_data(exception)]
34
53
  visited = [exception]
35
54
 
@@ -45,12 +64,8 @@ module Rollbar
45
64
  end
46
65
 
47
66
  def trace_data(current_exception)
48
- frames = reduce_frames(current_exception)
49
- # reverse so that the order is as rollbar expects
50
- frames.reverse!
51
-
52
67
  {
53
- :frames => frames,
68
+ :frames => map_frames(current_exception),
54
69
  :exception => {
55
70
  :class => current_exception.class.name,
56
71
  :message => current_exception.message
@@ -58,16 +73,10 @@ module Rollbar
58
73
  }
59
74
  end
60
75
 
61
- def reduce_frames(current_exception)
62
- exception_backtrace(current_exception).map do |frame|
63
- # parse the line
64
- match = frame.match(/(.*):(\d+)(?::in `([^']+)')?/)
65
-
66
- if match
67
- { :filename => match[1], :lineno => match[2].to_i, :method => match[3] }
68
- else
69
- { :filename => '<unknown>', :lineno => 0, :method => frame }
70
- end
76
+ def map_frames(current_exception)
77
+ exception_backtrace(current_exception).reverse.map do |frame|
78
+ Rollbar::Item::Frame.new(self, frame,
79
+ :configuration => configuration).to_h
71
80
  end
72
81
  end
73
82
 
@@ -0,0 +1,112 @@
1
+ # We want to use Gem.path
2
+ require 'rubygems'
3
+
4
+ module Rollbar
5
+ class Item
6
+ # Representation of the trace data per frame in the payload
7
+ class Frame
8
+ attr_reader :backtrace
9
+ attr_reader :frame
10
+ attr_reader :configuration
11
+
12
+ MAX_CONTEXT_LENGTH = 4
13
+
14
+ def initialize(backtrace, frame, options = {})
15
+ @backtrace = backtrace
16
+ @frame = frame
17
+ @configuration = options[:configuration]
18
+ end
19
+
20
+ def to_h
21
+ # parse the line
22
+ match = frame.match(/(.*):(\d+)(?::in `([^']+)')?/)
23
+
24
+ return unknown_frame unless match
25
+
26
+ filename = match[1]
27
+ lineno = match[2].to_i
28
+ frame_data = {
29
+ :filename => filename,
30
+ :lineno => lineno,
31
+ :method => match[3]
32
+ }
33
+
34
+ frame_data.merge(extra_frame_data(filename, lineno))
35
+ end
36
+
37
+ private
38
+
39
+ def unknown_frame
40
+ { :filename => '<unknown>', :lineno => 0, :method => frame }
41
+ end
42
+
43
+ def extra_frame_data(filename, lineno)
44
+ file_lines = backtrace.get_file_lines(filename)
45
+
46
+ return {} if skip_extra_frame_data?(filename, file_lines)
47
+
48
+ {
49
+ :code => code_data(file_lines, lineno),
50
+ :context => context_data(file_lines, lineno)
51
+ }
52
+ end
53
+
54
+ def skip_extra_frame_data?(filename, file_lines)
55
+ config = configuration.send_extra_frame_data
56
+ missing_file_lines = !file_lines || file_lines.empty?
57
+
58
+ return false if !missing_file_lines && config == :all
59
+
60
+ missing_file_lines ||
61
+ config == :none ||
62
+ config == :app && outside_project?(filename)
63
+ end
64
+
65
+ def outside_project?(filename)
66
+ project_gem_paths = configuration.project_gem_paths
67
+ inside_project_gem_paths = project_gem_paths.any? do |path|
68
+ filename.start_with?(path)
69
+ end
70
+
71
+ # The file is inside the configuration.project_gem_paths,
72
+ return false if inside_project_gem_paths
73
+
74
+ root = configuration.root
75
+ inside_root = root && filename.start_with?(root.to_s)
76
+
77
+ # The file is outside the configuration.root
78
+ return true unless inside_root
79
+
80
+ # At this point, the file is inside the configuration.root.
81
+ # Since it's common to have gems installed in {root}/vendor/bundle,
82
+ # let's check it's in any of the Gem.path paths
83
+ Gem.path.any? { |path| filename.start_with?(path) }
84
+ end
85
+
86
+ def code_data(file_lines, lineno)
87
+ file_lines[lineno - 1]
88
+ end
89
+
90
+ def context_data(file_lines, lineno)
91
+ {
92
+ :pre => pre_data(file_lines, lineno),
93
+ :post => post_data(file_lines, lineno)
94
+ }
95
+ end
96
+
97
+ def post_data(file_lines, lineno)
98
+ from_line = lineno
99
+ number_of_lines = [from_line + MAX_CONTEXT_LENGTH, file_lines.size].min - from_line
100
+
101
+ file_lines[from_line, number_of_lines]
102
+ end
103
+
104
+ def pre_data(file_lines, lineno)
105
+ to_line = lineno - 2
106
+ from_line = [to_line - MAX_CONTEXT_LENGTH + 1, 0].max
107
+
108
+ file_lines[from_line, (to_line - from_line + 1)].select(&:present?)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -3,6 +3,7 @@ require 'rack/response'
3
3
 
4
4
  module Rollbar
5
5
  module Middleware
6
+ # Middleware to inject the rollbar.js snippet into a 200 html response
6
7
  class Js
7
8
  attr_reader :app
8
9
  attr_reader :config
@@ -18,38 +19,24 @@ module Rollbar
18
19
  def call(env)
19
20
  result = app.call(env)
20
21
 
21
- _call(env, result)
22
- end
23
-
24
- private
25
-
26
- def _call(env, result)
27
- return result unless should_add_js?(env, result[0], result[1])
22
+ begin
23
+ return result unless add_js?(env, result[0], result[1])
28
24
 
29
- if response_string = add_js(env, result[2])
30
- env[JS_IS_INJECTED_KEY] = true
31
- response = ::Rack::Response.new(response_string, result[0], result[1])
32
-
33
- response.finish
34
- else
25
+ response_string = add_js(env, result[2])
26
+ build_response(env, result, response_string)
27
+ rescue => e
28
+ Rollbar.log_error("[Rollbar] Rollbar.js could not be added because #{e} exception")
35
29
  result
36
30
  end
37
- rescue => e
38
- Rollbar.log_error("[Rollbar] Rollbar.js could not be added because #{e} exception")
39
- result
40
31
  end
41
32
 
42
33
  def enabled?
43
34
  !!config[:enabled]
44
35
  end
45
36
 
46
- def should_add_js?(env, status, headers)
47
- enabled? &&
48
- status == 200 &&
49
- !env[JS_IS_INJECTED_KEY] &&
50
- html?(headers) &&
51
- !attachment?(headers) &&
52
- !streaming?(env)
37
+ def add_js?(env, status, headers)
38
+ enabled? && status == 200 && !env[JS_IS_INJECTED_KEY] &&
39
+ html?(headers) && !attachment?(headers) && !streaming?(env)
53
40
  end
54
41
 
55
42
  def html?(headers)
@@ -75,28 +62,39 @@ module Rollbar
75
62
  head_open_end = find_end_of_head_open(body)
76
63
  return nil unless head_open_end
77
64
 
78
- if head_open_end
79
- body = body[0..head_open_end] <<
80
- config_js_tag(env) <<
81
- snippet_js_tag(env) <<
82
- body[head_open_end + 1..-1]
83
- end
84
-
85
- body
65
+ build_body_with_js(env, body, head_open_end)
86
66
  rescue => e
87
67
  Rollbar.log_error("[Rollbar] Rollbar.js could not be added because #{e} exception")
88
68
  nil
89
69
  end
90
70
 
71
+ def build_response(env, app_result, response_string)
72
+ return result unless response_string
73
+
74
+ env[JS_IS_INJECTED_KEY] = true
75
+ response = ::Rack::Response.new(response_string, app_result[0],
76
+ app_result[1])
77
+
78
+ response.finish
79
+ end
80
+
81
+ def build_body_with_js(env, body, head_open_end)
82
+ return body unless head_open_end
83
+
84
+ body[0..head_open_end] << config_js_tag(env) << snippet_js_tag(env) <<
85
+ body[head_open_end + 1..-1]
86
+ end
87
+
91
88
  def find_end_of_head_open(body)
92
89
  head_open = body.index(/<head\W/)
93
90
  body.index('>', head_open) if head_open
94
91
  end
95
92
 
96
93
  def join_body(response)
97
- source = nil
98
- response.each { |fragment| source ? (source << fragment.to_s) : (source = fragment.to_s)}
99
- source
94
+ response.to_enum.reduce('') do |acc, fragment|
95
+ acc << fragment.to_s
96
+ acc
97
+ end
100
98
  end
101
99
 
102
100
  def close_old_response(response)
@@ -116,7 +114,7 @@ module Rollbar
116
114
  end
117
115
 
118
116
  def script_tag(content, env)
119
- if defined?(::SecureHeaders) && ::SecureHeaders.respond_to?(:content_security_policy_script_nonce)
117
+ if append_nonce?
120
118
  nonce = ::SecureHeaders.content_security_policy_script_nonce(::Rack::Request.new(env))
121
119
  script_tag_content = "\n<script type=\"text/javascript\" nonce=\"#{nonce}\">#{content}</script>"
122
120
  else
@@ -130,6 +128,12 @@ module Rollbar
130
128
  string = string.html_safe if string.respond_to?(:html_safe)
131
129
  string
132
130
  end
131
+
132
+ def append_nonce?
133
+ defined?(::SecureHeaders) && ::SecureHeaders.respond_to?(:content_security_policy_script_nonce) &&
134
+ defined?(::SecureHeaders::Configuration) &&
135
+ !::SecureHeaders::Configuration.get.current_csp[:script_src].to_a.include?("'unsafe-inline'")
136
+ end
133
137
  end
134
138
  end
135
139
  end
@@ -66,11 +66,11 @@ module Rollbar
66
66
  end
67
67
 
68
68
  def context(request_data)
69
- return unless request_data[:route]
69
+ return unless request_data[:params]
70
70
 
71
- route = request_data[:route]
71
+ route_params = request_data[:params]
72
72
  # make sure route is a hash built by RequestDataExtractor
73
- return "#{route[:controller]}" + '#' + "#{route[:action]}" if route.is_a?(Hash) && !route.empty?
73
+ return route_params[:controller].to_s + '#' + route_params[:action].to_s if route_params.is_a?(Hash) && !route_params.empty?
74
74
  end
75
75
  end
76
76
  end