sentry-ruby 0.1.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,11 +10,8 @@ module Sentry
10
10
  @buffer = Array.new(size)
11
11
  end
12
12
 
13
- def record(crumb = nil)
14
- if block_given?
15
- crumb = Breadcrumb.new if crumb.nil?
16
- yield(crumb)
17
- end
13
+ def record(crumb)
14
+ yield(crumb) if block_given?
18
15
  @buffer.slice!(0)
19
16
  @buffer << crumb
20
17
  end
@@ -20,8 +20,10 @@ module Sentry
20
20
  end
21
21
  end
22
22
 
23
- def capture_event(event, scope)
24
- scope.apply_to_event(event)
23
+ def capture_event(event, scope, hint = {})
24
+ return false unless configuration.sending_allowed?
25
+
26
+ scope.apply_to_event(event, hint)
25
27
 
26
28
  if configuration.async?
27
29
  begin
@@ -30,10 +32,16 @@ module Sentry
30
32
  configuration.async.call(event.to_json_compatible)
31
33
  rescue => e
32
34
  configuration.logger.error(LOGGER_PROGNAME) { "async event sending failed: #{e.message}" }
33
- send_event(event)
35
+ send_event(event, hint)
34
36
  end
35
37
  else
36
- send_event(event)
38
+ if hint.fetch(:background, true)
39
+ Sentry.background_worker.perform do
40
+ send_event(event, hint)
41
+ end
42
+ else
43
+ send_event(event, hint)
44
+ end
37
45
  end
38
46
 
39
47
  event
@@ -51,10 +59,20 @@ module Sentry
51
59
  Event.new(configuration: configuration, message: message)
52
60
  end
53
61
 
54
- def send_event(event)
55
- return false unless configuration.sending_allowed?(event)
62
+ def event_from_transaction(transaction)
63
+ TransactionEvent.new(configuration: configuration).tap do |event|
64
+ event.transaction = transaction.name
65
+ event.contexts.merge!(trace: transaction.get_trace_context)
66
+ event.timestamp = transaction.timestamp
67
+ event.start_timestamp = transaction.start_timestamp
68
+
69
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
70
+ event.spans = finished_spans.map(&:to_hash)
71
+ end
72
+ end
56
73
 
57
- event = configuration.before_send.call(event) if configuration.before_send
74
+ def send_event(event, hint = nil)
75
+ event = configuration.before_send.call(event, hint) if configuration.before_send
58
76
  if event.nil?
59
77
  configuration.logger.info(LOGGER_PROGNAME) { "Discarded event because before_send returned nil" }
60
78
  return
@@ -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"
@@ -15,6 +17,37 @@ module Sentry
15
17
  attr_reader :async
16
18
  alias async? async
17
19
 
20
+ # to send events in a non-blocking way, sentry-ruby has its own background worker
21
+ # by default, the worker holds a thread pool that has [the number of processors] threads
22
+ # but you can configure it with this configuration option
23
+ # E.g.: config.background_worker_threads = 5
24
+ #
25
+ # if you want to send events synchronously, set the value to 0
26
+ # E.g.: config.background_worker_threads = 0
27
+ attr_accessor :background_worker_threads
28
+
29
+ # a proc/lambda that takes an array of stack traces
30
+ # it'll be used to silence (reduce) backtrace of the exception
31
+ #
32
+ # for example:
33
+ #
34
+ # ```ruby
35
+ # Sentry.configuration.backtrace_cleanup_callback = lambda do |backtrace|
36
+ # Rails.backtrace_cleaner.clean(backtrace)
37
+ # end
38
+ # ```
39
+ #
40
+ attr_accessor :backtrace_cleanup_callback
41
+
42
+ # Optional Proc, called before sending an event to the server/
43
+ # E.g.: lambda { |event| event }
44
+ # E.g.: lambda { |event| nil }
45
+ # E.g.: lambda { |event|
46
+ # event[:message] = 'a'
47
+ # event
48
+ # }
49
+ attr_reader :before_send
50
+
18
51
  # An array of breadcrumbs loggers to be used. Available options are:
19
52
  # - :sentry_logger
20
53
  # - :active_support_logger
@@ -24,10 +57,13 @@ module Sentry
24
57
  attr_accessor :context_lines
25
58
 
26
59
  # RACK_ENV by default.
27
- attr_reader :current_environment
60
+ attr_reader :environment
28
61
 
29
- # Whitelist of environments that will send notifications to Sentry. Array of Strings.
30
- attr_accessor :environments
62
+ # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
63
+ attr_reader :dsn
64
+
65
+ # Whitelist of enabled_environments that will send notifications to Sentry. Array of Strings.
66
+ attr_accessor :enabled_environments
31
67
 
32
68
  # Logger 'progname's to exclude from breadcrumbs
33
69
  attr_accessor :exclude_loggers
@@ -64,51 +100,36 @@ module Sentry
64
100
  # any events, and a value of 1.0 will send 100% of events.
65
101
  attr_accessor :sample_rate
66
102
 
67
- # a proc/lambda that takes an array of stack traces
68
- # it'll be used to silence (reduce) backtrace of the exception
69
- #
70
- # for example:
71
- #
72
- # ```ruby
73
- # Sentry.configuration.backtrace_cleanup_callback = lambda do |backtrace|
74
- # Rails.backtrace_cleaner.clean(backtrace)
75
- # end
76
- # ```
77
- #
78
- attr_accessor :backtrace_cleanup_callback
79
-
80
103
  # Include module versions in reports - boolean.
81
104
  attr_accessor :send_modules
82
105
 
106
+ # When send_default_pii's value is false (default), sensitive information like
107
+ # - user ip
108
+ # - user cookie
109
+ # - request body
110
+ # will not be sent to Sentry.
83
111
  attr_accessor :send_default_pii
84
112
 
85
113
  attr_accessor :server_name
86
114
 
87
- # Provide a configurable callback to determine event capture.
88
- # Note that the object passed into the block will be a String (messages) or
89
- # an exception.
90
- # e.g. lambda { |exc_or_msg| exc_or_msg.some_attr == false }
91
- attr_reader :should_capture
92
-
93
- # Silences ready message when true.
94
- attr_accessor :silence_ready
95
-
115
+ # Return a Transport::Configuration object for transport-related configurations.
96
116
  attr_reader :transport
97
117
 
98
- # Optional Proc, called before sending an event to the server/
99
- # E.g.: lambda { |event| event }
100
- # E.g.: lambda { |event| nil }
101
- # E.g.: lambda { |event|
102
- # event[:message] = 'a'
103
- # event
104
- # }
105
- attr_reader :before_send
118
+ # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
119
+ attr_accessor :traces_sample_rate
106
120
 
107
- # Errors object - an Array that contains error messages. See #
108
- attr_reader :errors
121
+ # Take a Proc that controls the sample rate for every tracing event, e.g.
122
+ # ```
123
+ # lambda do |tracing_context|
124
+ # # tracing_context[:transaction_context] contains the information about the transaction
125
+ # # tracing_context[:parent_sampled] contains the transaction's parent's sample decision
126
+ # true # return value can be a boolean or a float between 0.0 and 1.0
127
+ # end
128
+ # ```
129
+ attr_accessor :traces_sampler
109
130
 
110
- # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
111
- attr_reader :dsn
131
+ # these are not config options
132
+ attr_reader :errors, :gem_specs
112
133
 
113
134
  # Most of these errors generate 4XX responses. In general, Sentry clients
114
135
  # only automatically report 5xx responses.
@@ -136,10 +157,11 @@ module Sentry
136
157
 
137
158
  def initialize
138
159
  self.async = false
160
+ self.background_worker_threads = Concurrent.processor_count
139
161
  self.breadcrumbs_logger = []
140
162
  self.context_lines = 3
141
- self.current_environment = current_environment_from_env
142
- self.environments = []
163
+ self.environment = environment_from_env
164
+ self.enabled_environments = []
143
165
  self.exclude_loggers = []
144
166
  self.excluded_exceptions = IGNORE_DEFAULT.dup
145
167
  self.inspect_exception_causes_for_exclusion = false
@@ -153,11 +175,12 @@ module Sentry
153
175
  self.send_default_pii = false
154
176
  self.dsn = ENV['SENTRY_DSN']
155
177
  self.server_name = server_name_from_env
156
- self.should_capture = false
157
178
 
158
- @transport = Transport::Configuration.new
159
179
  self.before_send = false
160
180
  self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
181
+
182
+ @transport = Transport::Configuration.new
183
+ @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
161
184
  post_initialization_callback
162
185
  end
163
186
 
@@ -195,14 +218,6 @@ module Sentry
195
218
  @breadcrumbs_logger = logger
196
219
  end
197
220
 
198
- def should_capture=(value)
199
- unless value == false || value.respond_to?(:call)
200
- raise ArgumentError, "should_capture must be callable (or false to disable)"
201
- end
202
-
203
- @should_capture = value
204
- end
205
-
206
221
  def before_send=(value)
207
222
  unless value == false || value.respond_to?(:call)
208
223
  raise ArgumentError, "before_send must be callable (or false to disable)"
@@ -211,31 +226,21 @@ module Sentry
211
226
  @before_send = value
212
227
  end
213
228
 
214
- # Allows config options to be read like a hash
215
- #
216
- # @param [Symbol] option Key for a given attribute
217
- def [](option)
218
- public_send(option)
229
+ def environment=(environment)
230
+ @environment = environment.to_s
219
231
  end
220
232
 
221
- def current_environment=(environment)
222
- @current_environment = environment.to_s
223
- end
224
-
225
- def capture_allowed?(message_or_exc = nil)
233
+ def sending_allowed?
226
234
  @errors = []
227
235
 
228
236
  valid? &&
229
- capture_in_current_environment? &&
230
- capture_allowed_by_callback?(message_or_exc) &&
237
+ capture_in_environment? &&
231
238
  sample_allowed?
232
239
  end
233
- # If we cannot capture, we cannot send.
234
- alias sending_allowed? capture_allowed?
235
240
 
236
241
  def error_messages
237
- @errors = [errors[0]] + errors[1..-1].map(&:downcase) # fix case of all but first
238
- errors.join(", ")
242
+ @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first
243
+ @errors.join(", ")
239
244
  end
240
245
 
241
246
  def project_root=(root_dir)
@@ -256,7 +261,11 @@ module Sentry
256
261
  end
257
262
 
258
263
  def enabled_in_current_env?
259
- environments.empty? || environments.include?(current_environment)
264
+ enabled_environments.empty? || enabled_environments.include?(environment)
265
+ end
266
+
267
+ def tracing_enabled?
268
+ !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
260
269
  end
261
270
 
262
271
  private
@@ -279,13 +288,17 @@ module Sentry
279
288
  end
280
289
 
281
290
  def excluded_exception?(incoming_exception)
282
- excluded_exceptions.any? do |excluded_exception|
283
- matches_exception?(get_exception_class(excluded_exception), incoming_exception)
291
+ excluded_exception_classes.any? do |excluded_exception|
292
+ matches_exception?(excluded_exception, incoming_exception)
284
293
  end
285
294
  end
286
295
 
296
+ def excluded_exception_classes
297
+ @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) }
298
+ end
299
+
287
300
  def get_exception_class(x)
288
- x.is_a?(Module) ? x : qualified_const_get(x)
301
+ x.is_a?(Module) ? x : safe_const_get(x)
289
302
  end
290
303
 
291
304
  def matches_exception?(excluded_exception_class, incoming_exception)
@@ -296,14 +309,9 @@ module Sentry
296
309
  end
297
310
  end
298
311
 
299
- # In Ruby <2.0 const_get can't lookup "SomeModule::SomeClass" in one go
300
- def qualified_const_get(x)
301
- x = x.to_s
302
- if !x.match(/::/)
303
- Object.const_get(x)
304
- else
305
- x.split(MODULE_SEPARATOR).reject(&:empty?).inject(Object) { |a, e| a.const_get(e) }
306
- end
312
+ def safe_const_get(x)
313
+ x = x.to_s unless x.is_a?(String)
314
+ Object.const_get(x)
307
315
  rescue NameError # There's no way to safely ask if a constant exist for an unknown string
308
316
  nil
309
317
  end
@@ -339,17 +347,10 @@ module Sentry
339
347
  ENV['SENTRY_RELEASE']
340
348
  end
341
349
 
342
- def capture_in_current_environment?
350
+ def capture_in_environment?
343
351
  return true if enabled_in_current_env?
344
352
 
345
- @errors << "Not configured to send/capture in environment '#{current_environment}'"
346
- false
347
- end
348
-
349
- def capture_allowed_by_callback?(message_or_exc)
350
- return true if !should_capture || message_or_exc.nil? || should_capture.call(message_or_exc)
351
-
352
- @errors << "should_capture returned false"
353
+ @errors << "Not configured to send/capture in environment '#{environment}'"
353
354
  false
354
355
  end
355
356
 
@@ -380,7 +381,7 @@ module Sentry
380
381
  Socket.gethostbyname(hostname).first rescue server_name
381
382
  end
382
383
 
383
- def current_environment_from_env
384
+ def environment_from_env
384
385
  ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default'
385
386
  end
386
387
 
@@ -2,7 +2,10 @@ require "uri"
2
2
 
3
3
  module Sentry
4
4
  class DSN
5
- attr_reader :scheme, :project_id, :public_key, :secret_key, :host, :port, :path
5
+ PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze
6
+ REQUIRED_ATTRIBUTES = %w(host path public_key project_id).freeze
7
+
8
+ attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
6
9
 
7
10
  def initialize(dsn_string)
8
11
  @raw_value = dsn_string
@@ -24,7 +27,7 @@ module Sentry
24
27
  end
25
28
 
26
29
  def valid?
27
- %w(host path public_key project_id).all? { |k| public_send(k) }
30
+ REQUIRED_ATTRIBUTES.all? { |k| public_send(k) }
28
31
  end
29
32
 
30
33
  def to_s
@@ -33,7 +36,7 @@ module Sentry
33
36
 
34
37
  def server
35
38
  server = "#{scheme}://#{host}"
36
- server += ":#{port}" unless port == { 'http' => 80, 'https' => 443 }[scheme]
39
+ server += ":#{port}" unless port == PORT_MAP[scheme]
37
40
  server += path
38
41
  server
39
42
  end
@@ -5,6 +5,7 @@ require 'securerandom'
5
5
  require 'sentry/interface'
6
6
  require 'sentry/backtrace'
7
7
  require 'sentry/utils/real_ip'
8
+ require 'sentry/utils/request_id'
8
9
 
9
10
  module Sentry
10
11
  class Event
@@ -13,21 +14,19 @@ module Sentry
13
14
  release environment server_name modules
14
15
  message user tags contexts extra
15
16
  fingerprint breadcrumbs backtrace transaction
16
- platform sdk
17
+ platform sdk type
17
18
  )
18
19
 
19
20
  attr_accessor(*ATTRIBUTES)
20
- attr_reader :id, :configuration
21
-
22
- alias event_id id
21
+ attr_reader :configuration, :request, :exception, :stacktrace
23
22
 
24
23
  def initialize(configuration:, message: nil)
25
24
  # this needs to go first because some setters rely on configuration
26
25
  @configuration = configuration
27
26
 
28
27
  # Set some simple default values
29
- @id = SecureRandom.uuid.delete("-")
30
- @timestamp = Time.now.utc
28
+ @event_id = SecureRandom.uuid.delete("-")
29
+ @timestamp = Sentry.utc_now.iso8601
31
30
  @platform = :ruby
32
31
  @sdk = Sentry.sdk_meta
33
32
 
@@ -39,9 +38,9 @@ module Sentry
39
38
  @fingerprint = []
40
39
 
41
40
  @server_name = configuration.server_name
42
- @environment = configuration.current_environment
41
+ @environment = configuration.environment
43
42
  @release = configuration.release
44
- @modules = list_gem_specs if configuration.send_modules
43
+ @modules = configuration.gem_specs if configuration.send_modules
45
44
 
46
45
  @message = message || ""
47
46
 
@@ -51,8 +50,8 @@ module Sentry
51
50
  class << self
52
51
  def get_log_message(event_hash)
53
52
  message = event_hash[:message] || event_hash['message']
54
- message = get_message_from_exception(event_hash) if message.empty?
55
- message = '<no message value>' if message.empty?
53
+ message = get_message_from_exception(event_hash) if message.nil? || message.empty?
54
+ message = '<no message value>' if message.nil? || message.empty?
56
55
  message
57
56
  end
58
57
 
@@ -68,7 +67,7 @@ module Sentry
68
67
  end
69
68
 
70
69
  def timestamp=(time)
71
- @timestamp = time.is_a?(Time) ? time.strftime('%Y-%m-%dT%H:%M:%S') : time
70
+ @timestamp = time.is_a?(Time) ? time.to_f : time
72
71
  end
73
72
 
74
73
  def level=(new_level) # needed to meet the Sentry spec
@@ -76,7 +75,7 @@ module Sentry
76
75
  end
77
76
 
78
77
  def rack_env=(env)
79
- unless @request || env.empty?
78
+ unless request || env.empty?
80
79
  @request = Sentry::RequestInterface.new.tap do |int|
81
80
  int.from_rack(env)
82
81
  end
@@ -84,18 +83,22 @@ module Sentry
84
83
  if configuration.send_default_pii && ip = calculate_real_ip_from_rack(env.dup)
85
84
  user[:ip_address] = ip
86
85
  end
86
+ if request_id = Utils::RequestId.read_from(env)
87
+ tags[:request_id] = request_id
88
+ end
87
89
  end
88
90
  end
89
91
 
90
- def to_hash
91
- data = ATTRIBUTES.each_with_object({}) do |att, memo|
92
- memo[att] = public_send(att) if public_send(att)
93
- end
92
+ def type
93
+ "event"
94
+ end
94
95
 
96
+ def to_hash
97
+ data = serialize_attributes
95
98
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
96
- data[:stacktrace] = @stacktrace.to_hash if @stacktrace
97
- data[:request] = @request.to_hash if @request
98
- data[:exception] = @exception.to_hash if @exception
99
+ data[:stacktrace] = stacktrace.to_hash if stacktrace
100
+ data[:request] = request.to_hash if request
101
+ data[:exception] = exception.to_hash if exception
99
102
 
100
103
  data
101
104
  end
@@ -141,9 +144,9 @@ module Sentry
141
144
  frame.in_app = line.in_app
142
145
  frame.module = line.module_name if line.module_name
143
146
 
144
- if configuration[:context_lines] && frame.abs_path
147
+ if configuration.context_lines && frame.abs_path
145
148
  frame.pre_context, frame.context_line, frame.post_context = \
146
- configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration[:context_lines])
149
+ configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration.context_lines)
147
150
  end
148
151
 
149
152
  memo << frame if frame.filename
@@ -152,6 +155,14 @@ module Sentry
152
155
 
153
156
  private
154
157
 
158
+ def serialize_attributes
159
+ self.class::ATTRIBUTES.each_with_object({}) do |att, memo|
160
+ if value = public_send(att)
161
+ memo[att] = value
162
+ end
163
+ end
164
+ end
165
+
155
166
  # When behind a proxy (or if the user is using a proxy), we can't use
156
167
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
157
168
  def calculate_real_ip_from_rack(env)
@@ -162,10 +173,5 @@ module Sentry
162
173
  :forwarded_for => env["HTTP_X_FORWARDED_FOR"]
163
174
  ).calculate_ip
164
175
  end
165
-
166
- def list_gem_specs
167
- # Older versions of Rubygems don't support iterating over all specs
168
- Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
169
- end
170
176
  end
171
177
  end