sentry-ruby-core 4.2.1 → 4.4.0.pre.beta.0

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.
data/lib/sentry/client.rb CHANGED
@@ -2,10 +2,13 @@ require "sentry/transport"
2
2
 
3
3
  module Sentry
4
4
  class Client
5
- attr_reader :transport, :configuration
5
+ include LoggingHelper
6
+
7
+ attr_reader :transport, :configuration, :logger
6
8
 
7
9
  def initialize(configuration)
8
10
  @configuration = configuration
11
+ @logger = configuration.logger
9
12
 
10
13
  if transport_class = configuration.transport.transport_class
11
14
  @transport = transport_class.new(configuration)
@@ -26,32 +29,17 @@ module Sentry
26
29
  scope.apply_to_event(event, hint)
27
30
 
28
31
  if async_block = configuration.async
29
- begin
30
- # We have to convert to a JSON-like hash, because background job
31
- # processors (esp ActiveJob) may not like weird types in the event hash
32
- event_hash = event.to_json_compatible
33
-
34
- if async_block.arity == 2
35
- hint = JSON.parse(JSON.generate(hint))
36
- async_block.call(event_hash, hint)
37
- else
38
- async_block.call(event_hash)
39
- end
40
- rescue => e
41
- configuration.logger.error(LOGGER_PROGNAME) { "async event sending failed: #{e.message}" }
42
- send_event(event, hint)
43
- end
32
+ dispatch_async_event(async_block, event, hint)
33
+ elsif hint.fetch(:background, true)
34
+ dispatch_background_event(event, hint)
44
35
  else
45
- if hint.fetch(:background, true)
46
- Sentry.background_worker.perform do
47
- send_event(event, hint)
48
- end
49
- else
50
- send_event(event, hint)
51
- end
36
+ send_event(event, hint)
52
37
  end
53
38
 
54
39
  event
40
+ rescue => e
41
+ log_error("Event capturing failed", e, debug: configuration.debug)
42
+ nil
55
43
  end
56
44
 
57
45
  def event_from_exception(exception, hint = {})
@@ -85,16 +73,51 @@ module Sentry
85
73
 
86
74
  def send_event(event, hint = nil)
87
75
  event_type = event.is_a?(Event) ? event.type : event["type"]
88
- event = configuration.before_send.call(event, hint) if configuration.before_send && event_type == "event"
89
76
 
90
- if event.nil?
91
- configuration.logger.info(LOGGER_PROGNAME) { "Discarded event because before_send returned nil" }
92
- return
77
+ if event_type != TransactionEvent::TYPE && configuration.before_send
78
+ event = configuration.before_send.call(event, hint)
79
+
80
+ if event.nil?
81
+ log_info("Discarded event because before_send returned nil")
82
+ return
83
+ end
93
84
  end
94
85
 
95
86
  transport.send_event(event)
96
87
 
97
88
  event
89
+ rescue => e
90
+ loggable_event_type = (event_type || "event").capitalize
91
+ log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug)
92
+
93
+ event_info = Event.get_log_message(event.to_hash)
94
+ log_info("Unreported #{loggable_event_type}: #{event_info}")
95
+ raise
96
+ end
97
+
98
+ private
99
+
100
+ def dispatch_background_event(event, hint)
101
+ Sentry.background_worker.perform do
102
+ send_event(event, hint)
103
+ end
104
+ end
105
+
106
+ def dispatch_async_event(async_block, event, hint)
107
+ # We have to convert to a JSON-like hash, because background job
108
+ # processors (esp ActiveJob) may not like weird types in the event hash
109
+ event_hash = event.to_json_compatible
110
+
111
+ if async_block.arity == 2
112
+ hint = JSON.parse(JSON.generate(hint))
113
+ async_block.call(event_hash, hint)
114
+ else
115
+ async_block.call(event_hash)
116
+ end
117
+ rescue => e
118
+ loggable_event_type = event_hash["type"] || "event"
119
+ log_error("Async #{loggable_event_type} sending failed", e, debug: configuration.debug)
120
+ send_event(event, hint)
98
121
  end
99
122
  end
100
123
  end
@@ -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)
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,47 +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
-
137
- int.stacktrace =
138
- if e.backtrace && !backtraces.include?(e.backtrace.object_id)
139
- backtraces << e.backtrace.object_id
140
- initialize_stacktrace_interface(e.backtrace)
141
- end
142
- end
143
- end
144
- end
145
- end
146
-
147
- def initialize_stacktrace_interface(backtrace)
148
- StacktraceInterface.new(
149
- backtrace: backtrace,
150
- project_root: configuration.project_root.to_s,
151
- app_dirs_pattern: configuration.app_dirs_pattern,
152
- linecache: configuration.linecache,
153
- context_lines: configuration.context_lines,
154
- backtrace_cleanup_callback: configuration.backtrace_cleanup_callback
155
- )
136
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: configuration.stacktrace_builder)
156
137
  end
157
138
 
158
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)