sentry-ruby 4.0.0 → 4.1.3

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.
@@ -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
@@ -18,9 +18,9 @@ module Sentry
18
18
  )
19
19
 
20
20
  attr_accessor(*ATTRIBUTES)
21
- attr_reader :configuration
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 = {}
@@ -75,14 +75,15 @@ module Sentry
75
75
  end
76
76
 
77
77
  def rack_env=(env)
78
- unless @request || env.empty?
79
- @request = Sentry::RequestInterface.new.tap do |int|
80
- int.from_rack(env)
81
- end
78
+ unless request || env.empty?
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
@@ -96,9 +97,9 @@ module Sentry
96
97
  def to_hash
97
98
  data = serialize_attributes
98
99
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
99
- data[:stacktrace] = @stacktrace.to_hash if @stacktrace
100
- data[:request] = @request.to_hash if @request
101
- data[:exception] = @exception.to_hash if @exception
100
+ data[:stacktrace] = stacktrace.to_hash if stacktrace
101
+ data[:request] = request.to_hash if request
102
+ data[:exception] = exception.to_hash if exception
102
103
 
103
104
  data
104
105
  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
@@ -76,12 +76,12 @@ module Sentry
76
76
  def capture_exception(exception, **options, &block)
77
77
  return unless current_client
78
78
 
79
- event = current_client.event_from_exception(exception)
79
+ options[:hint] ||= {}
80
+ options[:hint][:exception] = exception
81
+ event = current_client.event_from_exception(exception, options[:hint])
80
82
 
81
83
  return unless event
82
84
 
83
- options[:hint] ||= {}
84
- options[:hint] = options[:hint].merge(exception: exception)
85
85
  capture_event(event, **options, &block)
86
86
  end
87
87
 
@@ -89,15 +89,15 @@ module Sentry
89
89
  return unless current_client
90
90
 
91
91
  options[:hint] ||= {}
92
- options[:hint] = options[:hint].merge(message: message)
93
- event = current_client.event_from_message(message)
92
+ options[:hint][:message] = message
93
+ event = current_client.event_from_message(message, options[:hint])
94
94
  capture_event(event, **options, &block)
95
95
  end
96
96
 
97
97
  def capture_event(event, **options, &block)
98
98
  return unless current_client
99
99
 
100
- hint = options.delete(:hint)
100
+ hint = options.delete(:hint) || {}
101
101
  scope = current_scope.dup
102
102
 
103
103
  if block
@@ -110,7 +110,7 @@ module Sentry
110
110
 
111
111
  event = current_client.capture_event(event, scope, hint)
112
112
 
113
- @last_event_id = event.event_id
113
+ @last_event_id = event&.event_id
114
114
  event
115
115
  end
116
116
 
@@ -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,22 +64,14 @@ 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
71
  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)
72
+ next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
73
+ next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
74
+ next if is_skippable_header?(key)
72
75
 
73
76
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
74
77
  key = key.sub(/^HTTP_/, "")
@@ -84,10 +87,26 @@ module Sentry
84
87
  end
85
88
  end
86
89
 
87
- def format_env_for_sentry(env_hash)
88
- return env_hash if Sentry.configuration.rack_env_whitelist.empty?
90
+ def is_skippable_header?(key)
91
+ key.upcase != key || # lower-case envs aren't real http headers
92
+ key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
93
+ !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
94
+ end
95
+
96
+ # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
97
+ # to think this is a Version header. Instead, this is mapped to
98
+ # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
99
+ # if the request has legitimately sent a Version header themselves.
100
+ # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
101
+ # NOTE: This will be removed in version 3.0+
102
+ def is_server_protocol?(key, value, protocol_version)
103
+ key == 'HTTP_VERSION' && value == protocol_version
104
+ end
105
+
106
+ def filter_and_format_env(env)
107
+ return env if Sentry.configuration.rack_env_whitelist.empty?
89
108
 
90
- env_hash.select do |k, _v|
109
+ env.select do |k, _v|
91
110
  Sentry.configuration.rack_env_whitelist.include? k.to_s
92
111
  end
93
112
  end
@@ -1,11 +1,31 @@
1
1
  module Sentry
2
- class StacktraceInterface < Interface
3
- attr_accessor :frames
2
+ class StacktraceInterface
3
+ attr_reader :frames
4
+
5
+ def initialize(backtrace:, project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
6
+ @project_root = project_root
7
+ @frames = []
8
+
9
+ parsed_backtrace_lines = Backtrace.parse(
10
+ backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
11
+ ).lines
12
+
13
+ parsed_backtrace_lines.reverse.each_with_object(@frames) do |line, frames|
14
+ frame = convert_parsed_line_into_frame(line, project_root, linecache, context_lines)
15
+ frames << frame if frame.filename
16
+ end
17
+ end
4
18
 
5
19
  def to_hash
6
- data = super
7
- data[:frames] = data[:frames].map(&:to_hash)
8
- data
20
+ { frames: @frames.map(&:to_hash) }
21
+ end
22
+
23
+ private
24
+
25
+ def convert_parsed_line_into_frame(line, project_root, linecache, context_lines)
26
+ frame = StacktraceInterface::Frame.new(@project_root, line)
27
+ frame.set_context(linecache, context_lines) if context_lines
28
+ frame
9
29
  end
10
30
 
11
31
  # Not actually an interface, but I want to use the same style
@@ -13,8 +33,14 @@ module Sentry
13
33
  attr_accessor :abs_path, :context_line, :function, :in_app,
14
34
  :lineno, :module, :pre_context, :post_context, :vars
15
35
 
16
- def initialize(project_root)
36
+ def initialize(project_root, line)
17
37
  @project_root = project_root
38
+
39
+ @abs_path = line.file if line.file
40
+ @function = line.method if line.method
41
+ @lineno = line.number
42
+ @in_app = line.in_app
43
+ @module = line.module_name if line.module_name
18
44
  end
19
45
 
20
46
  def filename
@@ -33,6 +59,13 @@ module Sentry
33
59
  @filename = prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
34
60
  end
35
61
 
62
+ def set_context(linecache, context_lines)
63
+ return unless abs_path
64
+
65
+ self.pre_context, self.context_line, self.post_context = \
66
+ linecache.get_file_context(abs_path, lineno, context_lines)
67
+ end
68
+
36
69
  def to_hash(*args)
37
70
  data = super(*args)
38
71
  data[:filename] = filename
@@ -1,5 +1,4 @@
1
- require 'time'
2
1
  require 'rack'
3
2
 
4
- require 'sentry/rack/capture_exception'
5
- require 'sentry/rack/tracing'
3
+ require 'sentry/rack/capture_exceptions'
4
+ require 'sentry/rack/deprecations'
@@ -0,0 +1,62 @@
1
+ module Sentry
2
+ module Rack
3
+ class CaptureExceptions
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ return @app.call(env) unless Sentry.initialized?
10
+
11
+ # make sure the current thread has a clean hub
12
+ Sentry.clone_hub_to_current_thread
13
+
14
+ Sentry.with_scope do |scope|
15
+ scope.clear_breadcrumbs
16
+ scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
17
+ scope.set_rack_env(env)
18
+
19
+ span = Sentry.start_transaction(name: scope.transaction_name, op: transaction_op)
20
+ scope.set_span(span)
21
+
22
+ begin
23
+ response = @app.call(env)
24
+ rescue Sentry::Error
25
+ finish_span(span, 500)
26
+ raise # Don't capture Sentry errors
27
+ rescue Exception => e
28
+ capture_exception(e)
29
+ finish_span(span, 500)
30
+ raise
31
+ end
32
+
33
+ exception = collect_exception(env)
34
+ capture_exception(exception) if exception
35
+
36
+ finish_span(span, response[0])
37
+
38
+ response
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def collect_exception(env)
45
+ env['rack.exception'] || env['sinatra.error']
46
+ end
47
+
48
+ def transaction_op
49
+ "rack.request".freeze
50
+ end
51
+
52
+ def capture_exception(exception)
53
+ Sentry.capture_exception(exception)
54
+ end
55
+
56
+ def finish_span(span, status_code)
57
+ span.set_http_status(status_code)
58
+ span.finish
59
+ end
60
+ end
61
+ end
62
+ end