sentry-ruby 4.0.1 → 4.1.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.
@@ -82,16 +82,14 @@ module Sentry
82
82
 
83
83
  # holder for an Array of Backtrace::Line instances
84
84
  attr_reader :lines
85
- attr_reader :configuration
86
85
 
87
- def self.parse(backtrace, configuration:)
86
+ def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
88
87
  ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
89
88
 
90
- ruby_lines = configuration.backtrace_cleanup_callback.call(ruby_lines) if configuration&.backtrace_cleanup_callback
89
+ ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
91
90
 
92
91
  in_app_pattern ||= begin
93
- project_root = configuration.project_root&.to_s
94
- Regexp.new("^(#{project_root}/)?#{configuration.app_dirs_pattern || APP_DIRS_PATTERN}")
92
+ Regexp.new("^(#{project_root}/)?#{app_dirs_pattern || APP_DIRS_PATTERN}")
95
93
  end
96
94
 
97
95
  lines = ruby_lines.to_a.map do |unparsed_line|
@@ -20,35 +20,51 @@ module Sentry
20
20
  end
21
21
  end
22
22
 
23
- def capture_event(event, scope, hint = nil)
23
+ def capture_event(event, scope, hint = {})
24
+ return unless configuration.sending_allowed?
25
+
24
26
  scope.apply_to_event(event, hint)
25
27
 
26
- if configuration.async?
28
+ if async_block = configuration.async
27
29
  begin
28
30
  # We have to convert to a JSON-like hash, because background job
29
31
  # processors (esp ActiveJob) may not like weird types in the event hash
30
- configuration.async.call(event.to_json_compatible)
32
+ event_hash = event.to_json_compatible
33
+
34
+ if async_block.arity == 2
35
+ async_block.call(event_hash, hint)
36
+ else
37
+ async_block.call(event_hash)
38
+ end
31
39
  rescue => e
32
40
  configuration.logger.error(LOGGER_PROGNAME) { "async event sending failed: #{e.message}" }
33
41
  send_event(event, hint)
34
42
  end
35
43
  else
36
- send_event(event, hint)
44
+ if hint.fetch(:background, true)
45
+ Sentry.background_worker.perform do
46
+ send_event(event, hint)
47
+ end
48
+ else
49
+ send_event(event, hint)
50
+ end
37
51
  end
38
52
 
39
53
  event
40
54
  end
41
55
 
42
- def event_from_exception(exception)
56
+ def event_from_exception(exception, hint = {})
57
+ integration_meta = Sentry.integrations[hint[:integration]]
43
58
  return unless @configuration.exception_class_allowed?(exception)
44
59
 
45
- Event.new(configuration: configuration).tap do |event|
60
+ Event.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
46
61
  event.add_exception_interface(exception)
47
62
  end
48
63
  end
49
64
 
50
- def event_from_message(message)
51
- Event.new(configuration: configuration, message: message)
65
+ def event_from_message(message, hint = {})
66
+ integration_meta = Sentry.integrations[hint[:integration]]
67
+ Event.new(configuration: configuration, integration_meta: integration_meta, message: message)
52
68
  end
53
69
 
54
70
  def event_from_transaction(transaction)
@@ -64,9 +80,9 @@ module Sentry
64
80
  end
65
81
 
66
82
  def send_event(event, hint = nil)
67
- return false unless configuration.sending_allowed?
83
+ event_type = event.is_a?(Event) ? event.type : event["type"]
84
+ event = configuration.before_send.call(event, hint) if configuration.before_send && event_type == "event"
68
85
 
69
- event = configuration.before_send.call(event, hint) if configuration.before_send
70
86
  if event.nil?
71
87
  configuration.logger.info(LOGGER_PROGNAME) { "Discarded event because before_send returned nil" }
72
88
  return
@@ -1,3 +1,5 @@
1
+ require "concurrent/utility/processor_counter"
2
+
1
3
  require "sentry/utils/exception_cause_chain"
2
4
  require "sentry/dsn"
3
5
  require "sentry/transport/configuration"
@@ -13,7 +15,15 @@ module Sentry
13
15
  # Provide an object that responds to `call` to send events asynchronously.
14
16
  # E.g.: lambda { |event| Thread.new { Sentry.send_event(event) } }
15
17
  attr_reader :async
16
- alias async? async
18
+
19
+ # to send events in a non-blocking way, sentry-ruby has its own background worker
20
+ # by default, the worker holds a thread pool that has [the number of processors] threads
21
+ # but you can configure it with this configuration option
22
+ # E.g.: config.background_worker_threads = 5
23
+ #
24
+ # if you want to send events synchronously, set the value to 0
25
+ # E.g.: config.background_worker_threads = 0
26
+ attr_accessor :background_worker_threads
17
27
 
18
28
  # a proc/lambda that takes an array of stack traces
19
29
  # it'll be used to silence (reduce) backtrace of the exception
@@ -123,7 +133,6 @@ module Sentry
123
133
  # Most of these errors generate 4XX responses. In general, Sentry clients
124
134
  # only automatically report 5xx responses.
125
135
  IGNORE_DEFAULT = [
126
- 'CGI::Session::CookieStore::TamperedWithCookie',
127
136
  'Mongoid::Errors::DocumentNotFound',
128
137
  'Rack::QueryParser::InvalidParameterError',
129
138
  'Rack::QueryParser::ParameterTypeError',
@@ -145,7 +154,7 @@ module Sentry
145
154
  AVAILABLE_BREADCRUMBS_LOGGERS = [:sentry_logger, :active_support_logger].freeze
146
155
 
147
156
  def initialize
148
- self.async = false
157
+ self.background_worker_threads = Concurrent.processor_count
149
158
  self.breadcrumbs_logger = []
150
159
  self.context_lines = 3
151
160
  self.environment = environment_from_env
@@ -182,8 +191,8 @@ module Sentry
182
191
 
183
192
 
184
193
  def async=(value)
185
- unless value == false || value.respond_to?(:call)
186
- raise(ArgumentError, "async must be callable (or false to disable)")
194
+ if value && !value.respond_to?(:call)
195
+ raise(ArgumentError, "async must be callable")
187
196
  end
188
197
 
189
198
  @async = value
@@ -20,7 +20,7 @@ module Sentry
20
20
  attr_accessor(*ATTRIBUTES)
21
21
  attr_reader :configuration, :request, :exception, :stacktrace
22
22
 
23
- def initialize(configuration:, message: nil)
23
+ def initialize(configuration:, integration_meta: nil, message: nil)
24
24
  # this needs to go first because some setters rely on configuration
25
25
  @configuration = configuration
26
26
 
@@ -28,7 +28,7 @@ module Sentry
28
28
  @event_id = SecureRandom.uuid.delete("-")
29
29
  @timestamp = Sentry.utc_now.iso8601
30
30
  @platform = :ruby
31
- @sdk = Sentry.sdk_meta
31
+ @sdk = integration_meta || Sentry.sdk_meta
32
32
 
33
33
  @user = {}
34
34
  @extra = {}
@@ -76,13 +76,14 @@ module Sentry
76
76
 
77
77
  def rack_env=(env)
78
78
  unless request || env.empty?
79
- @request = Sentry::RequestInterface.new.tap do |int|
80
- int.from_rack(env)
81
- end
79
+ env = env.dup
80
+
81
+ add_request_interface(env)
82
82
 
83
- if configuration.send_default_pii && ip = calculate_real_ip_from_rack(env.dup)
84
- user[:ip_address] = ip
83
+ if configuration.send_default_pii
84
+ user[:ip_address] = calculate_real_ip_from_rack(env)
85
85
  end
86
+
86
87
  if request_id = Utils::RequestId.read_from(env)
87
88
  tags[:request_id] = request_id
88
89
  end
@@ -107,6 +108,10 @@ module Sentry
107
108
  JSON.parse(JSON.generate(to_hash))
108
109
  end
109
110
 
111
+ def add_request_interface(env)
112
+ @request = Sentry::RequestInterface.from_rack(env)
113
+ end
114
+
110
115
  def add_exception_interface(exc)
111
116
  if exc.respond_to?(:sentry_context)
112
117
  @extra.merge!(exc.sentry_context)
@@ -124,33 +129,22 @@ module Sentry
124
129
  int.stacktrace =
125
130
  if e.backtrace && !backtraces.include?(e.backtrace.object_id)
126
131
  backtraces << e.backtrace.object_id
127
- StacktraceInterface.new.tap do |stacktrace|
128
- stacktrace.frames = stacktrace_interface_from(e.backtrace)
129
- end
132
+ initialize_stacktrace_interface(e.backtrace)
130
133
  end
131
134
  end
132
135
  end
133
136
  end
134
137
  end
135
138
 
136
- def stacktrace_interface_from(backtrace)
137
- project_root = configuration.project_root.to_s
138
-
139
- Backtrace.parse(backtrace, configuration: configuration).lines.reverse.each_with_object([]) do |line, memo|
140
- frame = StacktraceInterface::Frame.new(project_root)
141
- frame.abs_path = line.file if line.file
142
- frame.function = line.method if line.method
143
- frame.lineno = line.number
144
- frame.in_app = line.in_app
145
- frame.module = line.module_name if line.module_name
146
-
147
- if configuration.context_lines && frame.abs_path
148
- frame.pre_context, frame.context_line, frame.post_context = \
149
- configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration.context_lines)
150
- end
151
-
152
- memo << frame if frame.filename
153
- end
139
+ def initialize_stacktrace_interface(backtrace)
140
+ StacktraceInterface.new(
141
+ backtrace: backtrace,
142
+ project_root: configuration.project_root.to_s,
143
+ app_dirs_pattern: configuration.app_dirs_pattern,
144
+ linecache: configuration.linecache,
145
+ context_lines: configuration.context_lines,
146
+ backtrace_cleanup_callback: configuration.backtrace_cleanup_callback
147
+ )
154
148
  end
155
149
 
156
150
  private
@@ -3,6 +3,8 @@ require "sentry/client"
3
3
 
4
4
  module Sentry
5
5
  class Hub
6
+ include ArgumentCheckingHelper
7
+
6
8
  attr_reader :last_event_id
7
9
 
8
10
  def initialize(client, scope)
@@ -76,12 +78,14 @@ module Sentry
76
78
  def capture_exception(exception, **options, &block)
77
79
  return unless current_client
78
80
 
79
- event = current_client.event_from_exception(exception)
81
+ check_argument_type!(exception, ::Exception)
82
+
83
+ options[:hint] ||= {}
84
+ options[:hint][:exception] = exception
85
+ event = current_client.event_from_exception(exception, options[:hint])
80
86
 
81
87
  return unless event
82
88
 
83
- options[:hint] ||= {}
84
- options[:hint] = options[:hint].merge(exception: exception)
85
89
  capture_event(event, **options, &block)
86
90
  end
87
91
 
@@ -89,15 +93,17 @@ module Sentry
89
93
  return unless current_client
90
94
 
91
95
  options[:hint] ||= {}
92
- options[:hint] = options[:hint].merge(message: message)
93
- event = current_client.event_from_message(message)
96
+ options[:hint][:message] = message
97
+ event = current_client.event_from_message(message, options[:hint])
94
98
  capture_event(event, **options, &block)
95
99
  end
96
100
 
97
101
  def capture_event(event, **options, &block)
98
102
  return unless current_client
99
103
 
100
- hint = options.delete(:hint)
104
+ check_argument_type!(event, Sentry::Event)
105
+
106
+ hint = options.delete(:hint) || {}
101
107
  scope = current_scope.dup
102
108
 
103
109
  if block
@@ -110,7 +116,7 @@ module Sentry
110
116
 
111
117
  event = current_client.capture_event(event, scope, hint)
112
118
 
113
- @last_event_id = event.event_id
119
+ @last_event_id = event&.event_id
114
120
  event
115
121
  end
116
122
 
@@ -0,0 +1,24 @@
1
+ module Sentry
2
+ module Integrable
3
+ def register_integration(name:, version:)
4
+ Sentry.register_integration(name, version)
5
+ @integration_name = name
6
+ end
7
+
8
+ def integration_name
9
+ @integration_name
10
+ end
11
+
12
+ def capture_exception(exception, **options, &block)
13
+ options[:hint] ||= {}
14
+ options[:hint][:integration] = integration_name
15
+ Sentry.capture_exception(exception, **options, &block)
16
+ end
17
+
18
+ def capture_message(message, **options, &block)
19
+ options[:hint] ||= {}
20
+ options[:hint][:integration] = integration_name
21
+ Sentry.capture_message(message, **options, &block)
22
+ end
23
+ end
24
+ end
@@ -1,8 +1,9 @@
1
- require 'rack'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
4
  class RequestInterface < Interface
5
5
  REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
6
+ CONTENT_HEADERS = %w(CONTENT_TYPE CONTENT_LENGTH).freeze
6
7
  IP_HEADERS = [
7
8
  "REMOTE_ADDR",
8
9
  "HTTP_CLIENT_IP",
@@ -10,42 +11,52 @@ module Sentry
10
11
  "HTTP_X_FORWARDED_FOR"
11
12
  ].freeze
12
13
 
14
+ # See Sentry server default limits at
15
+ # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
16
+ MAX_BODY_LIMIT = 4096 * 4
17
+
13
18
  attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
14
19
 
15
- def initialize
16
- self.headers = {}
17
- self.env = {}
18
- self.cookies = nil
20
+ def self.from_rack(env)
21
+ env = clean_env(env)
22
+ req = ::Rack::Request.new(env)
23
+ self.new(req)
24
+ end
25
+
26
+ def self.clean_env(env)
27
+ unless Sentry.configuration.send_default_pii
28
+ # need to completely wipe out ip addresses
29
+ RequestInterface::IP_HEADERS.each do |header|
30
+ env.delete(header)
31
+ end
32
+ end
33
+
34
+ env
19
35
  end
20
36
 
21
- def from_rack(env_hash)
22
- req = ::Rack::Request.new(env_hash)
37
+ def initialize(req)
38
+ env = req.env
23
39
 
24
40
  if Sentry.configuration.send_default_pii
25
41
  self.data = read_data_from(req)
26
42
  self.cookies = req.cookies
27
- else
28
- # need to completely wipe out ip addresses
29
- IP_HEADERS.each { |h| env_hash.delete(h) }
30
43
  end
31
44
 
32
45
  self.url = req.scheme && req.url.split('?').first
33
46
  self.method = req.request_method
34
47
  self.query_string = req.query_string
35
48
 
36
- self.headers = format_headers_for_sentry(env_hash)
37
- self.env = format_env_for_sentry(env_hash)
49
+ self.headers = filter_and_format_headers(env)
50
+ self.env = filter_and_format_env(env)
38
51
  end
39
52
 
40
53
  private
41
54
 
42
- # See Sentry server default limits at
43
- # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
44
55
  def read_data_from(request)
45
56
  if request.form_data?
46
57
  request.POST
47
58
  elsif request.body # JSON requests, etc
48
- data = request.body.read(4096 * 4) # Sentry server limit
59
+ data = request.body.read(MAX_BODY_LIMIT)
49
60
  request.body.rewind
50
61
  data
51
62
  end
@@ -53,27 +64,18 @@ module Sentry
53
64
  e.message
54
65
  end
55
66
 
56
- def format_headers_for_sentry(env_hash)
57
- env_hash.each_with_object({}) do |(key, value), memo|
67
+ def filter_and_format_headers(env)
68
+ env.each_with_object({}) do |(key, value), memo|
58
69
  begin
59
70
  key = key.to_s # rack env can contain symbols
60
- value = value.to_s
61
- next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env_hash) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
62
- next unless key.upcase == key # Non-upper case stuff isn't either
63
-
64
- # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
65
- # to think this is a Version header. Instead, this is mapped to
66
- # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
67
- # if the request has legitimately sent a Version header themselves.
68
- # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
69
- next if key == 'HTTP_VERSION' && value == env_hash['SERVER_PROTOCOL']
70
- next if key == 'HTTP_COOKIE' # Cookies don't go here, they go somewhere else
71
- next unless key.start_with?('HTTP_') || %w(CONTENT_TYPE CONTENT_LENGTH).include?(key)
71
+ next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
72
+ next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
73
+ next if is_skippable_header?(key)
72
74
 
73
75
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
74
76
  key = key.sub(/^HTTP_/, "")
75
77
  key = key.split('_').map(&:capitalize).join('-')
76
- memo[key] = value
78
+ memo[key] = value.to_s
77
79
  rescue StandardError => e
78
80
  # Rails adds objects to the Rack env that can sometimes raise exceptions
79
81
  # when `to_s` is called.
@@ -84,10 +86,26 @@ module Sentry
84
86
  end
85
87
  end
86
88
 
87
- def format_env_for_sentry(env_hash)
88
- return env_hash if Sentry.configuration.rack_env_whitelist.empty?
89
+ def is_skippable_header?(key)
90
+ key.upcase != key || # lower-case envs aren't real http headers
91
+ key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
92
+ !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
93
+ end
94
+
95
+ # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
96
+ # to think this is a Version header. Instead, this is mapped to
97
+ # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
98
+ # if the request has legitimately sent a Version header themselves.
99
+ # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
100
+ # NOTE: This will be removed in version 3.0+
101
+ def is_server_protocol?(key, value, protocol_version)
102
+ key == 'HTTP_VERSION' && value == protocol_version
103
+ end
104
+
105
+ def filter_and_format_env(env)
106
+ return env if Sentry.configuration.rack_env_whitelist.empty?
89
107
 
90
- env_hash.select do |k, _v|
108
+ env.select do |k, _v|
91
109
  Sentry.configuration.rack_env_whitelist.include? k.to_s
92
110
  end
93
111
  end