sentry-ruby-core 4.1.5 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,7 @@ require "sentry/utils/exception_cause_chain"
4
4
  require "sentry/dsn"
5
5
  require "sentry/transport/configuration"
6
6
  require "sentry/linecache"
7
+ require "sentry/interfaces/stacktrace_builder"
7
8
 
8
9
  module Sentry
9
10
  class Configuration
@@ -38,10 +39,19 @@ module Sentry
38
39
  #
39
40
  attr_accessor :backtrace_cleanup_callback
40
41
 
42
+ # Optional Proc, called before adding the breadcrumb to the current scope
43
+ # E.g.: lambda { |breadcrumb, hint| breadcrumb }
44
+ # E.g.: lambda { |breadcrumb, hint| nil }
45
+ # E.g.: lambda { |breadcrumb, hint|
46
+ # breadcrumb.message = 'a'
47
+ # breadcrumb
48
+ # }
49
+ attr_reader :before_breadcrumb
50
+
41
51
  # Optional Proc, called before sending an event to the server/
42
- # E.g.: lambda { |event| event }
43
- # E.g.: lambda { |event| nil }
44
- # E.g.: lambda { |event|
52
+ # E.g.: lambda { |event, hint| event }
53
+ # E.g.: lambda { |event, hint| nil }
54
+ # E.g.: lambda { |event, hint|
45
55
  # event[:message] = 'a'
46
56
  # event
47
57
  # }
@@ -52,6 +62,9 @@ module Sentry
52
62
  # - :active_support_logger
53
63
  attr_reader :breadcrumbs_logger
54
64
 
65
+ # Max number of breadcrumbs a breadcrumb buffer can hold
66
+ attr_accessor :max_breadcrumbs
67
+
55
68
  # Number of lines of code context to capture, or nil for none
56
69
  attr_accessor :context_lines
57
70
 
@@ -109,6 +122,9 @@ module Sentry
109
122
  # will not be sent to Sentry.
110
123
  attr_accessor :send_default_pii
111
124
 
125
+ # IP ranges for trusted proxies that will be skipped when calculating IP address.
126
+ attr_accessor :trusted_proxies
127
+
112
128
  attr_accessor :server_name
113
129
 
114
130
  # Return a Transport::Configuration object for transport-related configurations.
@@ -153,23 +169,29 @@ module Sentry
153
169
 
154
170
  AVAILABLE_BREADCRUMBS_LOGGERS = [:sentry_logger, :active_support_logger].freeze
155
171
 
172
+ # Post initialization callbacks are called at the end of initialization process
173
+ # allowing extending the configuration of sentry-ruby by multiple extensions
174
+ @@post_initialization_callbacks = []
175
+
156
176
  def initialize
157
177
  self.background_worker_threads = Concurrent.processor_count
178
+ self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
158
179
  self.breadcrumbs_logger = []
159
180
  self.context_lines = 3
160
181
  self.environment = environment_from_env
161
182
  self.enabled_environments = []
162
183
  self.exclude_loggers = []
163
184
  self.excluded_exceptions = IGNORE_DEFAULT.dup
164
- self.inspect_exception_causes_for_exclusion = false
185
+ self.inspect_exception_causes_for_exclusion = true
165
186
  self.linecache = ::Sentry::LineCache.new
166
187
  self.logger = ::Sentry::Logger.new(STDOUT)
167
- self.project_root = detect_project_root
188
+ self.project_root = Dir.pwd
168
189
 
169
190
  self.release = detect_release
170
191
  self.sample_rate = 1.0
171
192
  self.send_modules = true
172
193
  self.send_default_pii = false
194
+ self.trusted_proxies = []
173
195
  self.dsn = ENV['SENTRY_DSN']
174
196
  self.server_name = server_name_from_env
175
197
 
@@ -178,7 +200,8 @@ module Sentry
178
200
 
179
201
  @transport = Transport::Configuration.new
180
202
  @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
181
- post_initialization_callback
203
+
204
+ run_post_initialization_callbacks
182
205
  end
183
206
 
184
207
  def dsn=(value)
@@ -223,6 +246,14 @@ module Sentry
223
246
  @before_send = value
224
247
  end
225
248
 
249
+ def before_breadcrumb=(value)
250
+ unless value.nil? || value.respond_to?(:call)
251
+ raise ArgumentError, "before_breadcrumb must be callable (or nil to disable)"
252
+ end
253
+
254
+ @before_breadcrumb = value
255
+ end
256
+
226
257
  def environment=(environment)
227
258
  @environment = environment.to_s
228
259
  end
@@ -265,16 +296,18 @@ module Sentry
265
296
  !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
266
297
  end
267
298
 
268
- private
269
-
270
- def detect_project_root
271
- if defined? Rails.root # we are in a Rails application
272
- Rails.root.to_s
273
- else
274
- Dir.pwd
275
- end
299
+ def stacktrace_builder
300
+ @stacktrace_builder ||= StacktraceBuilder.new(
301
+ project_root: @project_root.to_s,
302
+ app_dirs_pattern: @app_dirs_pattern,
303
+ linecache: @linecache,
304
+ context_lines: @context_lines,
305
+ backtrace_cleanup_callback: @backtrace_cleanup_callback
306
+ )
276
307
  end
277
308
 
309
+ private
310
+
278
311
  def detect_release
279
312
  detect_release_from_env ||
280
313
  detect_release_from_git ||
@@ -390,7 +423,21 @@ module Sentry
390
423
  end
391
424
  end
392
425
 
393
- # allow extensions to extend the Configuration class
394
- def post_initialization_callback; end
426
+ def run_post_initialization_callbacks
427
+ self.class.post_initialization_callbacks.each do |hook|
428
+ instance_eval(&hook)
429
+ end
430
+ end
431
+
432
+ # allow extensions to add their hooks to the Configuration class
433
+ def self.add_post_initialization_callback(&block)
434
+ self.post_initialization_callbacks << block
435
+ end
436
+
437
+ protected
438
+
439
+ def self.post_initialization_callbacks
440
+ @@post_initialization_callbacks
441
+ end
395
442
  end
396
443
  end
data/lib/sentry/event.rb CHANGED
@@ -20,7 +20,7 @@ module Sentry
20
20
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
21
21
 
22
22
  attr_accessor(*ATTRIBUTES)
23
- attr_reader :configuration, :request, :exception, :stacktrace
23
+ attr_reader :configuration, :request, :exception, :threads
24
24
 
25
25
  def initialize(configuration:, integration_meta: nil, message: nil)
26
26
  # this needs to go first because some setters rely on configuration
@@ -52,19 +52,26 @@ module Sentry
52
52
  class << self
53
53
  def get_log_message(event_hash)
54
54
  message = event_hash[:message] || event_hash['message']
55
- message = get_message_from_exception(event_hash) if message.nil? || message.empty?
56
- message = '<no message value>' if message.nil? || message.empty?
57
- message
55
+
56
+ return message unless message.nil? || message.empty?
57
+
58
+ message = get_message_from_exception(event_hash)
59
+
60
+ return message unless message.nil? || message.empty?
61
+
62
+ message = event_hash[:transaction] || event_hash["transaction"]
63
+
64
+ return message unless message.nil? || message.empty?
65
+
66
+ '<no message value>'
58
67
  end
59
68
 
60
69
  def get_message_from_exception(event_hash)
61
- (
62
- event_hash &&
63
- event_hash[:exception] &&
64
- event_hash[:exception][:values] &&
65
- event_hash[:exception][:values][0] &&
66
- "#{event_hash[:exception][:values][0][:type]}: #{event_hash[:exception][:values][0][:value]}"
67
- )
70
+ if exception = event_hash.dig(:exception, :values, 0)
71
+ "#{exception[:type]}: #{exception[:value]}"
72
+ elsif exception = event_hash.dig("exception", "values", 0)
73
+ "#{exception["type"]}: #{exception["value"]}"
74
+ end
68
75
  end
69
76
  end
70
77
 
@@ -99,9 +106,9 @@ module Sentry
99
106
  def to_hash
100
107
  data = serialize_attributes
101
108
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
102
- data[:stacktrace] = stacktrace.to_hash if stacktrace
103
109
  data[:request] = request.to_hash if request
104
110
  data[:exception] = exception.to_hash if exception
111
+ data[:threads] = threads.to_hash if threads
105
112
 
106
113
  data
107
114
  end
@@ -111,42 +118,23 @@ module Sentry
111
118
  end
112
119
 
113
120
  def add_request_interface(env)
114
- @request = Sentry::RequestInterface.from_rack(env)
121
+ @request = Sentry::RequestInterface.build(env: env)
115
122
  end
116
123
 
117
- def add_exception_interface(exc)
118
- if exc.respond_to?(:sentry_context)
119
- @extra.merge!(exc.sentry_context)
120
- end
124
+ def add_threads_interface(backtrace: nil, **options)
125
+ @threads = ThreadsInterface.build(
126
+ backtrace: backtrace,
127
+ stacktrace_builder: configuration.stacktrace_builder,
128
+ **options
129
+ )
130
+ end
121
131
 
122
- @exception = Sentry::ExceptionInterface.new.tap do |exc_int|
123
- exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exc).reverse
124
- backtraces = Set.new
125
- exc_int.values = exceptions.map do |e|
126
- SingleExceptionInterface.new.tap do |int|
127
- int.type = e.class.to_s
128
- int.value = e.message.byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
129
- int.module = e.class.to_s.split('::')[0...-1].join('::')
130
-
131
- int.stacktrace =
132
- if e.backtrace && !backtraces.include?(e.backtrace.object_id)
133
- backtraces << e.backtrace.object_id
134
- initialize_stacktrace_interface(e.backtrace)
135
- end
136
- end
137
- end
132
+ def add_exception_interface(exception)
133
+ if exception.respond_to?(:sentry_context)
134
+ @extra.merge!(exception.sentry_context)
138
135
  end
139
- end
140
136
 
141
- def initialize_stacktrace_interface(backtrace)
142
- StacktraceInterface.new(
143
- backtrace: backtrace,
144
- project_root: configuration.project_root.to_s,
145
- app_dirs_pattern: configuration.app_dirs_pattern,
146
- linecache: configuration.linecache,
147
- context_lines: configuration.context_lines,
148
- backtrace_cleanup_callback: configuration.backtrace_cleanup_callback
149
- )
137
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: configuration.stacktrace_builder)
150
138
  end
151
139
 
152
140
  private
@@ -166,7 +154,8 @@ module Sentry
166
154
  :remote_addr => env["REMOTE_ADDR"],
167
155
  :client_ip => env["HTTP_CLIENT_IP"],
168
156
  :real_ip => env["HTTP_X_REAL_IP"],
169
- :forwarded_for => env["HTTP_X_FORWARDED_FOR"]
157
+ :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
158
+ :trusted_proxies => configuration.trusted_proxies
170
159
  ).calculate_ip
171
160
  end
172
161
  end
@@ -0,0 +1,7 @@
1
+ module Sentry
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ExternalError < StandardError
6
+ end
7
+ end
data/lib/sentry/hub.rb CHANGED
@@ -69,9 +69,11 @@ module Sentry
69
69
  @stack.pop
70
70
  end
71
71
 
72
- def start_transaction(transaction: nil, **options)
72
+ def start_transaction(transaction: nil, configuration: Sentry.configuration, **options)
73
+ return unless configuration.tracing_enabled?
74
+
73
75
  transaction ||= Transaction.new(**options)
74
- transaction.set_initial_sample_desicion
76
+ transaction.set_initial_sample_decision(configuration: current_client.configuration)
75
77
  transaction
76
78
  end
77
79
 
@@ -120,7 +122,13 @@ module Sentry
120
122
  event
121
123
  end
122
124
 
123
- def add_breadcrumb(breadcrumb)
125
+ def add_breadcrumb(breadcrumb, hint: {})
126
+ if before_breadcrumb = current_client.configuration.before_breadcrumb
127
+ breadcrumb = before_breadcrumb.call(breadcrumb, hint)
128
+ end
129
+
130
+ return unless breadcrumb
131
+
124
132
  current_scope.add_breadcrumb(breadcrumb)
125
133
  end
126
134
 
@@ -20,3 +20,4 @@ require "sentry/interfaces/exception"
20
20
  require "sentry/interfaces/request"
21
21
  require "sentry/interfaces/single_exception"
22
22
  require "sentry/interfaces/stacktrace"
23
+ require "sentry/interfaces/threads"
@@ -1,11 +1,29 @@
1
1
  module Sentry
2
2
  class ExceptionInterface < Interface
3
- attr_accessor :values
3
+ def initialize(values:)
4
+ @values = values
5
+ end
4
6
 
5
7
  def to_hash
6
8
  data = super
7
9
  data[:values] = data[:values].map(&:to_hash) if data[:values]
8
10
  data
9
11
  end
12
+
13
+ def self.build(exception:, stacktrace_builder:)
14
+ exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
15
+ processed_backtrace_ids = Set.new
16
+
17
+ exceptions = exceptions.map do |e|
18
+ if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
19
+ processed_backtrace_ids << e.backtrace.object_id
20
+ SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder)
21
+ else
22
+ SingleExceptionInterface.new(exception: exception)
23
+ end
24
+ end
25
+
26
+ new(values: exceptions)
27
+ end
10
28
  end
11
29
  end
@@ -17,10 +17,10 @@ module Sentry
17
17
 
18
18
  attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
19
19
 
20
- def self.from_rack(env)
20
+ def self.build(env:)
21
21
  env = clean_env(env)
22
- req = ::Rack::Request.new(env)
23
- self.new(req)
22
+ request = ::Rack::Request.new(env)
23
+ self.new(request: request)
24
24
  end
25
25
 
26
26
  def self.clean_env(env)
@@ -34,17 +34,17 @@ module Sentry
34
34
  env
35
35
  end
36
36
 
37
- def initialize(req)
38
- env = req.env
37
+ def initialize(request:)
38
+ env = request.env
39
39
 
40
40
  if Sentry.configuration.send_default_pii
41
- self.data = read_data_from(req)
42
- self.cookies = req.cookies
41
+ self.data = read_data_from(request)
42
+ self.cookies = request.cookies
43
+ self.query_string = request.query_string
43
44
  end
44
45
 
45
- self.url = req.scheme && req.url.split('?').first
46
- self.method = req.request_method
47
- self.query_string = req.query_string
46
+ self.url = request.scheme && request.url.split('?').first
47
+ self.method = request.request_method
48
48
 
49
49
  self.headers = filter_and_format_headers(env)
50
50
  self.env = filter_and_format_env(env)
@@ -1,14 +1,26 @@
1
1
  module Sentry
2
2
  class SingleExceptionInterface < Interface
3
- attr_accessor :type
4
- attr_accessor :value
5
- attr_accessor :module
6
- attr_accessor :stacktrace
3
+ attr_reader :type, :value, :module, :thread_id, :stacktrace
4
+
5
+ def initialize(exception:, stacktrace: nil)
6
+ @type = exception.class.to_s
7
+ @value = (exception.message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
8
+ @module = exception.class.to_s.split('::')[0...-1].join('::')
9
+ @thread_id = Thread.current.object_id
10
+ @stacktrace = stacktrace
11
+ end
7
12
 
8
13
  def to_hash
9
14
  data = super
10
15
  data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
11
16
  data
12
17
  end
18
+
19
+ # patch this method if you want to change an exception's stacktrace frames
20
+ # also see `StacktraceBuilder.build`.
21
+ def self.build_with_stacktrace(exception:, stacktrace_builder:)
22
+ stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
23
+ new(exception: exception, stacktrace: stacktrace)
24
+ end
13
25
  end
14
26
  end
@@ -2,18 +2,8 @@ module Sentry
2
2
  class StacktraceInterface
3
3
  attr_reader :frames
4
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
5
+ def initialize(frames:)
6
+ @frames = frames
17
7
  end
18
8
 
19
9
  def to_hash
@@ -22,30 +12,24 @@ module Sentry
22
12
 
23
13
  private
24
14
 
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
29
- end
30
-
31
15
  # Not actually an interface, but I want to use the same style
32
16
  class Frame < Interface
33
- attr_accessor :abs_path, :context_line, :function, :in_app,
34
- :lineno, :module, :pre_context, :post_context, :vars
17
+ attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
18
+ :lineno, :module, :pre_context, :post_context, :vars
35
19
 
36
20
  def initialize(project_root, line)
37
21
  @project_root = project_root
38
22
 
39
- @abs_path = line.file if line.file
23
+ @abs_path = line.file
40
24
  @function = line.method if line.method
41
25
  @lineno = line.number
42
26
  @in_app = line.in_app
43
27
  @module = line.module_name if line.module_name
28
+ @filename = compute_filename
44
29
  end
45
30
 
46
- def filename
31
+ def compute_filename
47
32
  return if abs_path.nil?
48
- return @filename if instance_variable_defined?(:@filename)
49
33
 
50
34
  prefix =
51
35
  if under_project_root? && in_app
@@ -56,19 +40,18 @@ module Sentry
56
40
  longest_load_path
57
41
  end
58
42
 
59
- @filename = prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
43
+ prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
60
44
  end
61
45
 
62
46
  def set_context(linecache, context_lines)
63
47
  return unless abs_path
64
48
 
65
- self.pre_context, self.context_line, self.post_context = \
49
+ @pre_context, @context_line, @post_context = \
66
50
  linecache.get_file_context(abs_path, lineno, context_lines)
67
51
  end
68
52
 
69
53
  def to_hash(*args)
70
54
  data = super(*args)
71
- data[:filename] = filename
72
55
  data.delete(:vars) unless vars && !vars.empty?
73
56
  data.delete(:pre_context) unless pre_context && !pre_context.empty?
74
57
  data.delete(:post_context) unless post_context && !post_context.empty?