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

Sign up to get free protection for your applications and to get access to all the features.
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)