rollbar 2.12.0 → 2.13.0

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 (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