sentry-raven 2.13.0 → 3.0.4

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.craft.yml +15 -0
  3. data/.github/workflows/test.yml +87 -0
  4. data/.github/workflows/zeus_upload.yml +32 -0
  5. data/.gitignore +2 -0
  6. data/.rubocop.yml +44 -9
  7. data/.scripts/bump-version.sh +9 -0
  8. data/{changelog.md → CHANGELOG.md} +38 -1
  9. data/CONTRIUTING.md +26 -0
  10. data/Gemfile +20 -25
  11. data/README.md +24 -14
  12. data/lib/raven/backtrace.rb +7 -5
  13. data/lib/raven/base.rb +5 -2
  14. data/lib/raven/breadcrumbs.rb +1 -1
  15. data/lib/raven/breadcrumbs/activesupport.rb +10 -10
  16. data/lib/raven/breadcrumbs/logger.rb +3 -3
  17. data/lib/raven/cli.rb +9 -20
  18. data/lib/raven/client.rb +9 -4
  19. data/lib/raven/configuration.rb +20 -6
  20. data/lib/raven/core_ext/object/deep_dup.rb +57 -0
  21. data/lib/raven/core_ext/object/duplicable.rb +153 -0
  22. data/lib/raven/event.rb +4 -2
  23. data/lib/raven/instance.rb +6 -3
  24. data/lib/raven/integrations/delayed_job.rb +13 -14
  25. data/lib/raven/integrations/rack-timeout.rb +2 -3
  26. data/lib/raven/integrations/rack.rb +4 -3
  27. data/lib/raven/integrations/rails.rb +1 -0
  28. data/lib/raven/integrations/rails/active_job.rb +5 -4
  29. data/lib/raven/integrations/rails/overrides/debug_exceptions_catcher.rb +2 -2
  30. data/lib/raven/interface.rb +2 -2
  31. data/lib/raven/interfaces/stack_trace.rb +1 -1
  32. data/lib/raven/linecache.rb +5 -2
  33. data/lib/raven/logger.rb +3 -2
  34. data/lib/raven/processor/cookies.rb +16 -6
  35. data/lib/raven/processor/post_data.rb +2 -0
  36. data/lib/raven/processor/removecircularreferences.rb +3 -1
  37. data/lib/raven/processor/sanitizedata.rb +65 -17
  38. data/lib/raven/processor/utf8conversion.rb +2 -0
  39. data/lib/raven/transports.rb +4 -0
  40. data/lib/raven/transports/http.rb +5 -5
  41. data/lib/raven/utils/exception_cause_chain.rb +1 -0
  42. data/lib/raven/utils/real_ip.rb +1 -1
  43. data/lib/raven/version.rb +2 -2
  44. data/sentry-raven.gemspec +2 -2
  45. metadata +11 -11
  46. data/.travis.yml +0 -47
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'socket'
3
4
  require 'securerandom'
4
5
 
@@ -76,7 +77,7 @@ module Raven
76
77
  end
77
78
 
78
79
  def message
79
- @interfaces[:logentry] && @interfaces[:logentry].unformatted_message
80
+ @interfaces[:logentry]&.unformatted_message
80
81
  end
81
82
 
82
83
  def message=(args)
@@ -96,12 +97,13 @@ module Raven
96
97
  end
97
98
 
98
99
  def level=(new_level) # needed to meet the Sentry spec
99
- @level = new_level == "warn" || new_level == :warn ? :warning : new_level
100
+ @level = new_level.to_s == "warn" ? :warning : new_level
100
101
  end
101
102
 
102
103
  def interface(name, value = nil, &block)
103
104
  int = Interface.registered[name]
104
105
  raise(Error, "Unknown interface: #{name}") unless int
106
+
105
107
  @interfaces[int.sentry_alias] = int.new(value, &block) if value || block
106
108
  @interfaces[int.sentry_alias]
107
109
  end
@@ -50,7 +50,9 @@ module Raven
50
50
 
51
51
  # Tell the log that the client is good to go
52
52
  def report_status
53
+ return unless configuration.enabled_in_current_env?
53
54
  return if configuration.silence_ready
55
+
54
56
  if configuration.capture_allowed?
55
57
  logger.info "Raven #{VERSION} ready to catch errors"
56
58
  else
@@ -109,17 +111,18 @@ module Raven
109
111
  end
110
112
 
111
113
  message_or_exc = obj.is_a?(String) ? "message" : "exception"
114
+ options = options.deep_dup
112
115
  options[:configuration] = configuration
113
116
  options[:context] = context
114
- if (evt = Event.send("from_" + message_or_exc, obj, options))
117
+ if evt = Event.send("from_" + message_or_exc, obj, options)
115
118
  yield evt if block_given?
116
119
  if configuration.async?
117
120
  begin
118
121
  # We have to convert to a JSON-like hash, because background job
119
122
  # processors (esp ActiveJob) may not like weird types in the event hash
120
123
  configuration.async.call(evt.to_json_compatible)
121
- rescue => ex
122
- logger.error("async event sending failed: #{ex.message}")
124
+ rescue => e
125
+ logger.error("async event sending failed: #{e.message}")
123
126
  send_event(evt, make_hint(obj))
124
127
  end
125
128
  else
@@ -8,19 +8,18 @@ module Delayed
8
8
  begin
9
9
  # Forward the call to the next callback in the callback chain
10
10
  block.call(job, *args)
11
-
12
- rescue Exception => exception
11
+ rescue Exception => e
13
12
  # Log error to Sentry
14
13
  extra = {
15
14
  :delayed_job => {
16
- :id => job.id.to_s,
17
- :priority => job.priority,
18
- :attempts => job.attempts,
19
- :run_at => job.run_at,
20
- :locked_at => job.locked_at,
21
- :locked_by => job.locked_by,
22
- :queue => job.queue,
23
- :created_at => job.created_at
15
+ :id => job.id.to_s,
16
+ :priority => job.priority,
17
+ :attempts => job.attempts,
18
+ :run_at => job.run_at,
19
+ :locked_at => job.locked_at,
20
+ :locked_by => job.locked_by,
21
+ :queue => job.queue,
22
+ :created_at => job.created_at
24
23
  }
25
24
  }
26
25
  # last_error can be nil
@@ -32,16 +31,16 @@ module Delayed
32
31
  if job.respond_to?('payload_object') && job.payload_object.respond_to?('job_data')
33
32
  extra[:active_job] = job.payload_object.job_data
34
33
  end
35
- ::Raven.capture_exception(exception,
36
- :logger => 'delayed_job',
37
- :tags => {
34
+ ::Raven.capture_exception(e,
35
+ :logger => 'delayed_job',
36
+ :tags => {
38
37
  :delayed_job_queue => job.queue,
39
38
  :delayed_job_id => job.id.to_s
40
39
  },
41
40
  :extra => extra)
42
41
 
43
42
  # Make sure we propagate the failure!
44
- raise exception
43
+ raise e
45
44
  ensure
46
45
  ::Raven::Context.clear!
47
46
  ::Raven::BreadcrumbBuffer.clear!
@@ -14,6 +14,5 @@ module RackTimeoutExtensions
14
14
  end
15
15
  end
16
16
 
17
- # Include is private in Ruby 1.9
18
- Rack::Timeout::Error.__send__(:include, RackTimeoutExtensions)
19
- Rack::Timeout::RequestTimeoutException.__send__(:include, RackTimeoutExtensions)
17
+ Rack::Timeout::Error.include(RackTimeoutExtensions)
18
+ Rack::Timeout::RequestTimeoutException.include(RackTimeoutExtensions)
@@ -92,8 +92,8 @@ module Raven
92
92
  request.body.rewind
93
93
  data
94
94
  end
95
- rescue IOError => ex
96
- ex.message
95
+ rescue IOError => e
96
+ e.message
97
97
  end
98
98
 
99
99
  def format_headers_for_sentry(env_hash)
@@ -112,8 +112,9 @@ module Raven
112
112
  next if key == 'HTTP_COOKIE' # Cookies don't go here, they go somewhere else
113
113
 
114
114
  next unless key.start_with?('HTTP_') || %w(CONTENT_TYPE CONTENT_LENGTH).include?(key)
115
+
115
116
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
116
- key = key.gsub("HTTP_", "")
117
+ key = key.sub(/^HTTP_/, "")
117
118
  key = key.split('_').map(&:capitalize).join('-')
118
119
  memo[key] = value
119
120
  rescue StandardError => e
@@ -5,6 +5,7 @@ module Raven
5
5
  require 'raven/integrations/rails/overrides/streaming_reporter'
6
6
  require 'raven/integrations/rails/controller_methods'
7
7
  require 'raven/integrations/rails/controller_transaction'
8
+ require 'raven/integrations/rack'
8
9
 
9
10
  initializer "raven.use_rack_middleware" do |app|
10
11
  app.config.middleware.insert 0, Raven::Rack
@@ -20,10 +20,11 @@ module Raven
20
20
 
21
21
  def capture_and_reraise_with_sentry(job, block)
22
22
  block.call
23
- rescue Exception => exception # rubocop:disable Lint/RescueException
24
- return if rescue_with_handler(exception)
25
- Raven.capture_exception(exception, :extra => raven_context(job))
26
- raise exception
23
+ rescue Exception => e # rubocop:disable Lint/RescueException
24
+ return if rescue_with_handler(e)
25
+
26
+ Raven.capture_exception(e, :extra => raven_context(job))
27
+ raise e
27
28
  ensure
28
29
  Context.clear!
29
30
  BreadcrumbBuffer.clear!
@@ -6,7 +6,7 @@ module Raven
6
6
  begin
7
7
  env = env_or_request.respond_to?(:env) ? env_or_request.env : env_or_request
8
8
  Raven::Rack.capture_exception(exception, env)
9
- rescue # rubocop:disable Lint/HandleExceptions
9
+ rescue
10
10
  end
11
11
  super
12
12
  end
@@ -21,7 +21,7 @@ module Raven
21
21
  begin
22
22
  env = env_or_request.respond_to?(:env) ? env_or_request.env : env_or_request
23
23
  Raven::Rack.capture_exception(exception, env)
24
- rescue # rubocop:disable Lint/HandleExceptions
24
+ rescue
25
25
  end
26
26
  render_exception_without_raven(env_or_request, exception)
27
27
  end
@@ -1,9 +1,9 @@
1
1
  module Raven
2
2
  class Interface
3
3
  def initialize(attributes = nil)
4
- attributes.each do |attr, value|
4
+ attributes&.each do |attr, value|
5
5
  public_send "#{attr}=", value
6
- end if attributes
6
+ end
7
7
 
8
8
  yield self if block_given?
9
9
  end
@@ -58,7 +58,7 @@ module Raven
58
58
  end
59
59
 
60
60
  def project_root
61
- @project_root ||= Raven.configuration.project_root && Raven.configuration.project_root.to_s
61
+ @project_root ||= Raven.configuration.project_root&.to_s
62
62
  end
63
63
 
64
64
  def longest_load_path
@@ -10,6 +10,7 @@ module Raven
10
10
  # line should be the line requested by lineno. See specs for more information.
11
11
  def get_file_context(filename, lineno, context)
12
12
  return nil, nil, nil unless valid_path?(filename)
13
+
13
14
  lines = Array.new(2 * context + 1) do |i|
14
15
  getline(filename, lineno - context + i)
15
16
  end
@@ -26,15 +27,17 @@ module Raven
26
27
  def getlines(path)
27
28
  @cache[path] ||= begin
28
29
  IO.readlines(path)
29
- rescue
30
- nil
30
+ rescue
31
+ nil
31
32
  end
32
33
  end
33
34
 
34
35
  def getline(path, n)
35
36
  return nil if n < 1
37
+
36
38
  lines = getlines(path)
37
39
  return nil if lines.nil?
40
+
38
41
  lines[n - 1]
39
42
  end
40
43
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'logger'
3
4
 
4
5
  module Raven
5
6
  class Logger < ::Logger
6
- LOG_PREFIX = "** [Raven] ".freeze
7
- PROGNAME = "sentry".freeze
7
+ LOG_PREFIX = "** [Raven] "
8
+ PROGNAME = "sentry"
8
9
 
9
10
  def initialize(*)
10
11
  super
@@ -10,17 +10,27 @@ module Raven
10
10
  private
11
11
 
12
12
  def process_if_symbol_keys(data)
13
- data[:request][:cookies] = STRING_MASK if data[:request][:cookies]
13
+ if cookies = data.dig(:request, :cookies)
14
+ data[:request][:cookies] = generate_masked_cookies(cookies)
15
+ end
14
16
 
15
- return unless data[:request][:headers] && data[:request][:headers]["Cookie"]
16
- data[:request][:headers]["Cookie"] = STRING_MASK
17
+ if cookies_header = data[:request][:headers]["Cookie"]
18
+ data[:request][:headers]["Cookie"] = generate_masked_cookies(cookies_header)
19
+ end
17
20
  end
18
21
 
19
22
  def process_if_string_keys(data)
20
- data["request"]["cookies"] = STRING_MASK if data["request"]["cookies"]
23
+ if cookies = data.dig("request", "cookies")
24
+ data["request"]["cookies"] = generate_masked_cookies(cookies)
25
+ end
21
26
 
22
- return unless data["request"]["headers"] && data["request"]["headers"]["Cookie"]
23
- data["request"]["headers"]["Cookie"] = STRING_MASK
27
+ if cookies_header = data.dig("request", "headers", "Cookie")
28
+ data["request"]["headers"]["Cookie"] = generate_masked_cookies(cookies_header)
29
+ end
30
+ end
31
+
32
+ def generate_masked_cookies(cookies)
33
+ cookies.merge(cookies) { STRING_MASK } if cookies.respond_to?(:merge)
24
34
  end
25
35
  end
26
36
  end
@@ -11,11 +11,13 @@ module Raven
11
11
 
12
12
  def process_if_symbol_keys(data)
13
13
  return unless data[:request][:method] == "POST"
14
+
14
15
  data[:request][:data] = STRING_MASK
15
16
  end
16
17
 
17
18
  def process_if_string_keys(data)
18
19
  return unless data["request"]["method"] == "POST"
20
+
19
21
  data["request"]["data"] = STRING_MASK
20
22
  end
21
23
  end
@@ -1,7 +1,9 @@
1
1
  module Raven
2
2
  class Processor::RemoveCircularReferences < Processor
3
+ ELISION_STRING = "(...)".freeze
3
4
  def process(value, visited = [])
4
- return "(...)" if visited.include?(value.__id__)
5
+ return ELISION_STRING if visited.include?(value.__id__)
6
+
5
7
  visited << value.__id__ if value.is_a?(Array) || value.is_a?(Hash)
6
8
 
7
9
  case value
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'json'
3
4
 
4
5
  module Raven
5
6
  class Processor::SanitizeData < Processor
6
7
  DEFAULT_FIELDS = %w(authorization password passwd secret ssn social(.*)?sec).freeze
7
- CREDIT_CARD_RE = /\b(?:3[47]\d|(?:4\d|5[1-5]|65)\d{2}|6011)\d{12}\b/
8
+ CREDIT_CARD_RE = /\b(?:3[47]\d|(?:4\d|5[1-5]|65)\d{2}|6011)\d{12}\b/.freeze
8
9
  QUERY_STRING = ['query_string', :query_string].freeze
9
10
  JSON_STARTS_WITH = ["[", "{"].freeze
10
11
 
@@ -20,22 +21,13 @@ module Raven
20
21
  def process(value, key = nil)
21
22
  case value
22
23
  when Hash
23
- !value.frozen? ? value.merge!(value) { |k, v| process v, k } : value.merge(value) { |k, v| process v, k }
24
+ sanitize_hash_value(key, value)
24
25
  when Array
25
- !value.frozen? ? value.map! { |v| process v, key } : value.map { |v| process v, key }
26
+ sanitize_array_value(key, value)
26
27
  when Integer
27
28
  matches_regexes?(key, value.to_s) ? INT_MASK : value
28
29
  when String
29
- if value =~ fields_re && (json = parse_json_or_nil(value))
30
- # if this string is actually a json obj, convert and sanitize
31
- process(json).to_json
32
- elsif matches_regexes?(key, value)
33
- STRING_MASK
34
- elsif QUERY_STRING.include?(key)
35
- sanitize_query_string(value)
36
- else
37
- value
38
- end
30
+ sanitize_string_value(key, value)
39
31
  else
40
32
  value
41
33
  end
@@ -49,6 +41,39 @@ module Raven
49
41
  @utf8_processor ||= Processor::UTF8Conversion.new
50
42
  end
51
43
 
44
+ def sanitize_hash_value(key, value)
45
+ if key =~ sensitive_fields
46
+ STRING_MASK
47
+ elsif value.frozen?
48
+ value.merge(value) { |k, v| process v, k }
49
+ else
50
+ value.merge!(value) { |k, v| process v, k }
51
+ end
52
+ end
53
+
54
+ def sanitize_array_value(key, value)
55
+ if value.frozen?
56
+ value.map { |v| process v, key }
57
+ else
58
+ value.map! { |v| process v, key }
59
+ end
60
+ end
61
+
62
+ def sanitize_string_value(key, value)
63
+ if value =~ sensitive_fields && (json = parse_json_or_nil(value))
64
+ # if this string is actually a json obj, convert and sanitize
65
+ process(json).to_json
66
+ elsif matches_regexes?(key, value)
67
+ STRING_MASK
68
+ elsif QUERY_STRING.include?(key)
69
+ sanitize_query_string(value)
70
+ elsif value =~ sensitive_fields
71
+ sanitize_sensitive_string_content(value)
72
+ else
73
+ value
74
+ end
75
+ end
76
+
52
77
  def sanitize_query_string(query_string)
53
78
  query_hash = CGI.parse(query_string)
54
79
  sanitized = utf8_processor.process(query_hash)
@@ -56,16 +81,38 @@ module Raven
56
81
  URI.encode_www_form(processed_query_hash)
57
82
  end
58
83
 
84
+ # this scrubs some sensitive info from the string content. for example:
85
+ #
86
+ # ```
87
+ # unexpected token at '{
88
+ # "role": "admin","password": "Abc@123","foo": "bar"
89
+ # }'
90
+ # ```
91
+ #
92
+ # will become
93
+ #
94
+ # ```
95
+ # unexpected token at '{
96
+ # "role": "admin","password": *******,"foo": "bar"
97
+ # }'
98
+ # ```
99
+ #
100
+ # it's particularly useful in hash or param-parsing related errors
101
+ def sanitize_sensitive_string_content(value)
102
+ value.gsub(/(#{sensitive_fields}['":]\s?(:|=>)?\s?)(".*?"|'.*?')/, '\1' + STRING_MASK)
103
+ end
104
+
59
105
  def matches_regexes?(k, v)
60
106
  (sanitize_credit_cards && v =~ CREDIT_CARD_RE) ||
61
- k =~ fields_re
107
+ k =~ sensitive_fields
62
108
  end
63
109
 
64
- def fields_re
65
- return @fields_re if instance_variable_defined?(:@fields_re)
110
+ def sensitive_fields
111
+ return @sensitive_fields if instance_variable_defined?(:@sensitive_fields)
112
+
66
113
  fields = DEFAULT_FIELDS | sanitize_fields
67
114
  fields -= sanitize_fields_excluded
68
- @fields_re = /#{fields.map do |f|
115
+ @sensitive_fields = /#{fields.map do |f|
69
116
  use_boundary?(f) ? "\\b#{f}\\b" : f
70
117
  end.join("|")}/i
71
118
  end
@@ -80,6 +127,7 @@ module Raven
80
127
 
81
128
  def parse_json_or_nil(string)
82
129
  return unless string.start_with?(*JSON_STARTS_WITH)
130
+
83
131
  JSON.parse(string)
84
132
  rescue JSON::ParserError, NoMethodError
85
133
  nil
@@ -14,6 +14,7 @@ module Raven
14
14
  !value.frozen? ? value.map! { |v| process v } : value.map { |v| process v }
15
15
  when Exception
16
16
  return value if value.message.valid_encoding?
17
+
17
18
  clean_exc = value.class.new(remove_invalid_bytes(value.message))
18
19
  clean_exc.set_backtrace(value.backtrace)
19
20
  clean_exc
@@ -27,6 +28,7 @@ module Raven
27
28
  value.force_encoding(Encoding::UTF_8)
28
29
  end
29
30
  return value if value.valid_encoding?
31
+
30
32
  remove_invalid_bytes(value)
31
33
  else
32
34
  value