sentry-ruby-core 4.2.2 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,9 +4,11 @@ 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
11
+ include LoggingHelper
10
12
  # Directories to be recognized as part of your app. e.g. if you
11
13
  # have an `engines` dir at the root of your project, you may want
12
14
  # to set this to something like /(app|config|engines|lib)/
@@ -61,12 +63,19 @@ module Sentry
61
63
  # - :active_support_logger
62
64
  attr_reader :breadcrumbs_logger
63
65
 
66
+ # Max number of breadcrumbs a breadcrumb buffer can hold
67
+ attr_accessor :max_breadcrumbs
68
+
64
69
  # Number of lines of code context to capture, or nil for none
65
70
  attr_accessor :context_lines
66
71
 
67
72
  # RACK_ENV by default.
68
73
  attr_reader :environment
69
74
 
75
+ # Whether the SDK should run in the debugging mode. Default is false.
76
+ # If set to true, SDK errors will be logged with backtrace
77
+ attr_accessor :debug
78
+
70
79
  # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
71
80
  attr_reader :dsn
72
81
 
@@ -163,14 +172,14 @@ module Sentry
163
172
  LOG_PREFIX = "** [Sentry] ".freeze
164
173
  MODULE_SEPARATOR = "::".freeze
165
174
 
166
- AVAILABLE_BREADCRUMBS_LOGGERS = [:sentry_logger, :active_support_logger].freeze
167
-
168
175
  # Post initialization callbacks are called at the end of initialization process
169
176
  # allowing extending the configuration of sentry-ruby by multiple extensions
170
177
  @@post_initialization_callbacks = []
171
178
 
172
179
  def initialize
180
+ self.debug = false
173
181
  self.background_worker_threads = Concurrent.processor_count
182
+ self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
174
183
  self.breadcrumbs_logger = []
175
184
  self.context_lines = 3
176
185
  self.environment = environment_from_env
@@ -195,6 +204,7 @@ module Sentry
195
204
 
196
205
  @transport = Transport::Configuration.new
197
206
  @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
207
+
198
208
  run_post_initialization_callbacks
199
209
  end
200
210
 
@@ -220,10 +230,6 @@ module Sentry
220
230
  if logger.is_a?(Array)
221
231
  logger
222
232
  else
223
- unless AVAILABLE_BREADCRUMBS_LOGGERS.include?(logger)
224
- raise Sentry::Error, "Unsupported breadcrumbs logger. Supported loggers: #{AVAILABLE_BREADCRUMBS_LOGGERS}"
225
- end
226
-
227
233
  Array(logger)
228
234
  end
229
235
 
@@ -272,10 +278,10 @@ module Sentry
272
278
  def exception_class_allowed?(exc)
273
279
  if exc.is_a?(Sentry::Error)
274
280
  # Try to prevent error reporting loops
275
- logger.debug(LOGGER_PROGNAME) { "Refusing to capture Sentry error: #{exc.inspect}" }
281
+ log_debug("Refusing to capture Sentry error: #{exc.inspect}")
276
282
  false
277
283
  elsif excluded_exception?(exc)
278
- logger.debug(LOGGER_PROGNAME) { "User excluded error: #{exc.inspect}" }
284
+ log_debug("User excluded error: #{exc.inspect}")
279
285
  false
280
286
  else
281
287
  true
@@ -287,7 +293,17 @@ module Sentry
287
293
  end
288
294
 
289
295
  def tracing_enabled?
290
- !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
296
+ !!((@traces_sample_rate && @traces_sample_rate >= 0.0 && @traces_sample_rate <= 1.0) || @traces_sampler) && sending_allowed?
297
+ end
298
+
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
+ )
291
307
  end
292
308
 
293
309
  private
@@ -298,7 +314,7 @@ module Sentry
298
314
  detect_release_from_capistrano ||
299
315
  detect_release_from_heroku
300
316
  rescue => e
301
- logger.error(LOGGER_PROGNAME) { "Error detecting release: #{e.message}" }
317
+ log_error("Error detecting release", e, debug: debug)
302
318
  end
303
319
 
304
320
  def excluded_exception?(incoming_exception)
@@ -333,7 +349,7 @@ module Sentry
333
349
  def detect_release_from_heroku
334
350
  return unless running_on_heroku?
335
351
  return if ENV['CI']
336
- logger.warn(LOGGER_PROGNAME) { HEROKU_DYNO_METADATA_MESSAGE } && return unless ENV['HEROKU_SLUG_COMMIT']
352
+ log_warn(HEROKU_DYNO_METADATA_MESSAGE) && return unless ENV['HEROKU_SLUG_COMMIT']
337
353
 
338
354
  ENV['HEROKU_SLUG_COMMIT']
339
355
  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, :threads
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
 
@@ -93,13 +100,11 @@ module Sentry
93
100
  end
94
101
 
95
102
  def type
96
- "event"
97
103
  end
98
104
 
99
105
  def to_hash
100
106
  data = serialize_attributes
101
107
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
102
- data[:stacktrace] = stacktrace.to_hash if stacktrace
103
108
  data[:request] = request.to_hash if request
104
109
  data[:exception] = exception.to_hash if exception
105
110
  data[:threads] = threads.to_hash if threads
@@ -112,48 +117,23 @@ module Sentry
112
117
  end
113
118
 
114
119
  def add_request_interface(env)
115
- @request = Sentry::RequestInterface.from_rack(env)
120
+ @request = Sentry::RequestInterface.build(env: env)
116
121
  end
117
122
 
118
123
  def add_threads_interface(backtrace: nil, **options)
119
- @threads = ThreadsInterface.new(**options)
120
- @threads.stacktrace = initialize_stacktrace_interface(backtrace) if backtrace
124
+ @threads = ThreadsInterface.build(
125
+ backtrace: backtrace,
126
+ stacktrace_builder: configuration.stacktrace_builder,
127
+ **options
128
+ )
121
129
  end
122
130
 
123
- def add_exception_interface(exc)
124
- if exc.respond_to?(:sentry_context)
125
- @extra.merge!(exc.sentry_context)
131
+ def add_exception_interface(exception)
132
+ if exception.respond_to?(:sentry_context)
133
+ @extra.merge!(exception.sentry_context)
126
134
  end
127
135
 
128
- @exception = Sentry::ExceptionInterface.new.tap do |exc_int|
129
- exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exc).reverse
130
- backtraces = Set.new
131
- exc_int.values = exceptions.map do |e|
132
- SingleExceptionInterface.new.tap do |int|
133
- int.type = e.class.to_s
134
- int.value = e.message.byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
135
- int.module = e.class.to_s.split('::')[0...-1].join('::')
136
- int.thread_id = Thread.current.object_id
137
-
138
- int.stacktrace =
139
- if e.backtrace && !backtraces.include?(e.backtrace.object_id)
140
- backtraces << e.backtrace.object_id
141
- initialize_stacktrace_interface(e.backtrace)
142
- end
143
- end
144
- end
145
- end
146
- end
147
-
148
- def initialize_stacktrace_interface(backtrace)
149
- StacktraceInterface.new(
150
- backtrace: backtrace,
151
- project_root: configuration.project_root.to_s,
152
- app_dirs_pattern: configuration.app_dirs_pattern,
153
- linecache: configuration.linecache,
154
- context_lines: configuration.context_lines,
155
- backtrace_cleanup_callback: configuration.backtrace_cleanup_callback
156
- )
136
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: configuration.stacktrace_builder)
157
137
  end
158
138
 
159
139
  private
@@ -0,0 +1,7 @@
1
+ module Sentry
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ExternalError < Error
6
+ end
7
+ end
data/lib/sentry/hub.rb CHANGED
@@ -21,6 +21,10 @@ module Sentry
21
21
  current_layer&.client
22
22
  end
23
23
 
24
+ def configuration
25
+ current_client.configuration
26
+ end
27
+
24
28
  def current_scope
25
29
  current_layer&.scope
26
30
  end
@@ -69,9 +73,19 @@ module Sentry
69
73
  @stack.pop
70
74
  end
71
75
 
72
- def start_transaction(transaction: nil, **options)
73
- transaction ||= Transaction.new(**options)
74
- transaction.set_initial_sample_desicion
76
+ def start_transaction(transaction: nil, custom_sampling_context: {}, **options)
77
+ return unless configuration.tracing_enabled?
78
+
79
+ transaction ||= Transaction.new(**options.merge(hub: self))
80
+
81
+ sampling_context = {
82
+ transaction_context: transaction.to_hash,
83
+ parent_sampled: transaction.parent_sampled
84
+ }
85
+
86
+ sampling_context.merge!(custom_sampling_context)
87
+
88
+ transaction.set_initial_sample_decision(sampling_context: sampling_context)
75
89
  transaction
76
90
  end
77
91
 
@@ -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,15 +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 :thread_id
7
- 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
8
12
 
9
13
  def to_hash
10
14
  data = super
11
15
  data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
12
16
  data
13
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
14
25
  end
15
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?