sentry-ruby 0.1.1 → 4.0.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
@@ -1,5 +1,4 @@
1
1
  require "sentry/transport"
2
- require 'sentry/utils/deep_merge'
3
2
 
4
3
  module Sentry
5
4
  class Client
@@ -21,55 +20,51 @@ module Sentry
21
20
  end
22
21
  end
23
22
 
24
- def capture_exception(exception, scope:, **options, &block)
25
- event = event_from_exception(exception, **options)
26
- return unless event
27
-
28
- block.call(event) if block
29
- capture_event(event, scope)
30
- end
31
-
32
- def capture_message(message, scope:, **options, &block)
33
- event = event_from_message(message, **options)
34
- block.call(event) if block
35
- capture_event(event, scope)
36
- end
23
+ def capture_event(event, scope, hint = nil)
24
+ scope.apply_to_event(event, hint)
25
+
26
+ if configuration.async?
27
+ begin
28
+ # We have to convert to a JSON-like hash, because background job
29
+ # processors (esp ActiveJob) may not like weird types in the event hash
30
+ configuration.async.call(event.to_json_compatible)
31
+ rescue => e
32
+ configuration.logger.error(LOGGER_PROGNAME) { "async event sending failed: #{e.message}" }
33
+ send_event(event, hint)
34
+ end
35
+ else
36
+ send_event(event, hint)
37
+ end
37
38
 
38
- def capture_event(event, scope)
39
- scope.apply_to_event(event)
40
- send_event(event)
41
39
  event
42
40
  end
43
41
 
44
- def event_from_exception(exception, **options)
45
- exception_context =
46
- if exception.instance_variable_defined?(:@__sentry_context)
47
- exception.instance_variable_get(:@__sentry_context)
48
- elsif exception.respond_to?(:sentry_context)
49
- exception.sentry_context
50
- else
51
- {}
52
- end
53
-
54
- options = Utils::DeepMergeHash.deep_merge(exception_context, options)
55
-
42
+ def event_from_exception(exception)
56
43
  return unless @configuration.exception_class_allowed?(exception)
57
44
 
58
- options = Event::Options.new(**options)
59
-
60
- Event.new(configuration: configuration, options: options).tap do |event|
45
+ Event.new(configuration: configuration).tap do |event|
61
46
  event.add_exception_interface(exception)
62
47
  end
63
48
  end
64
49
 
65
- def event_from_message(message, **options)
66
- options.merge!(message: message)
67
- options = Event::Options.new(options)
68
- Event.new(configuration: configuration, options: options)
50
+ def event_from_message(message)
51
+ Event.new(configuration: configuration, message: message)
52
+ end
53
+
54
+ def event_from_transaction(transaction)
55
+ TransactionEvent.new(configuration: configuration).tap do |event|
56
+ event.transaction = transaction.name
57
+ event.contexts.merge!(trace: transaction.get_trace_context)
58
+ event.timestamp = transaction.timestamp
59
+ event.start_timestamp = transaction.start_timestamp
60
+
61
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
62
+ event.spans = finished_spans.map(&:to_hash)
63
+ end
69
64
  end
70
65
 
71
66
  def send_event(event, hint = nil)
72
- return false unless configuration.sending_allowed?(event)
67
+ return false unless configuration.sending_allowed?
73
68
 
74
69
  event = configuration.before_send.call(event, hint) if configuration.before_send
75
70
  if event.nil?
@@ -15,6 +15,28 @@ module Sentry
15
15
  attr_reader :async
16
16
  alias async? async
17
17
 
18
+ # a proc/lambda that takes an array of stack traces
19
+ # it'll be used to silence (reduce) backtrace of the exception
20
+ #
21
+ # for example:
22
+ #
23
+ # ```ruby
24
+ # Sentry.configuration.backtrace_cleanup_callback = lambda do |backtrace|
25
+ # Rails.backtrace_cleaner.clean(backtrace)
26
+ # end
27
+ # ```
28
+ #
29
+ attr_accessor :backtrace_cleanup_callback
30
+
31
+ # Optional Proc, called before sending an event to the server/
32
+ # E.g.: lambda { |event| event }
33
+ # E.g.: lambda { |event| nil }
34
+ # E.g.: lambda { |event|
35
+ # event[:message] = 'a'
36
+ # event
37
+ # }
38
+ attr_reader :before_send
39
+
18
40
  # An array of breadcrumbs loggers to be used. Available options are:
19
41
  # - :sentry_logger
20
42
  # - :active_support_logger
@@ -24,10 +46,13 @@ module Sentry
24
46
  attr_accessor :context_lines
25
47
 
26
48
  # RACK_ENV by default.
27
- attr_reader :current_environment
49
+ attr_reader :environment
28
50
 
29
- # Whitelist of environments that will send notifications to Sentry. Array of Strings.
30
- attr_accessor :environments
51
+ # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
52
+ attr_reader :dsn
53
+
54
+ # Whitelist of enabled_environments that will send notifications to Sentry. Array of Strings.
55
+ attr_accessor :enabled_environments
31
56
 
32
57
  # Logger 'progname's to exclude from breadcrumbs
33
58
  attr_accessor :exclude_loggers
@@ -64,54 +89,36 @@ module Sentry
64
89
  # any events, and a value of 1.0 will send 100% of events.
65
90
  attr_accessor :sample_rate
66
91
 
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
92
  # Include module versions in reports - boolean.
81
93
  attr_accessor :send_modules
82
94
 
95
+ # When send_default_pii's value is false (default), sensitive information like
96
+ # - user ip
97
+ # - user cookie
98
+ # - request body
99
+ # will not be sent to Sentry.
83
100
  attr_accessor :send_default_pii
84
101
 
85
102
  attr_accessor :server_name
86
103
 
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
-
96
- # Default tags for events. Hash.
97
- attr_accessor :tags
98
-
104
+ # Return a Transport::Configuration object for transport-related configurations.
99
105
  attr_reader :transport
100
106
 
101
- # Optional Proc, called before sending an event to the server/
102
- # E.g.: lambda { |event, hint| event }
103
- # E.g.: lambda { |event, hint| nil }
104
- # E.g.: lambda { |event, hint|
105
- # event[:message] = 'a'
106
- # event
107
- # }
108
- attr_reader :before_send
107
+ # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
108
+ attr_accessor :traces_sample_rate
109
109
 
110
- # Errors object - an Array that contains error messages. See #
111
- attr_reader :errors
110
+ # Take a Proc that controls the sample rate for every tracing event, e.g.
111
+ # ```
112
+ # lambda do |tracing_context|
113
+ # # tracing_context[:transaction_context] contains the information about the transaction
114
+ # # tracing_context[:parent_sampled] contains the transaction's parent's sample decision
115
+ # true # return value can be a boolean or a float between 0.0 and 1.0
116
+ # end
117
+ # ```
118
+ attr_accessor :traces_sampler
112
119
 
113
- # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
114
- attr_reader :dsn
120
+ # these are not config options
121
+ attr_reader :errors, :gem_specs
115
122
 
116
123
  # Most of these errors generate 4XX responses. In general, Sentry clients
117
124
  # only automatically report 5xx responses.
@@ -141,8 +148,8 @@ module Sentry
141
148
  self.async = false
142
149
  self.breadcrumbs_logger = []
143
150
  self.context_lines = 3
144
- self.current_environment = current_environment_from_env
145
- self.environments = []
151
+ self.environment = environment_from_env
152
+ self.enabled_environments = []
146
153
  self.exclude_loggers = []
147
154
  self.excluded_exceptions = IGNORE_DEFAULT.dup
148
155
  self.inspect_exception_causes_for_exclusion = false
@@ -156,11 +163,12 @@ module Sentry
156
163
  self.send_default_pii = false
157
164
  self.dsn = ENV['SENTRY_DSN']
158
165
  self.server_name = server_name_from_env
159
- self.should_capture = false
160
- self.tags = {}
161
- @transport = Transport::Configuration.new
166
+
162
167
  self.before_send = false
163
168
  self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
169
+
170
+ @transport = Transport::Configuration.new
171
+ @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
164
172
  post_initialization_callback
165
173
  end
166
174
 
@@ -198,14 +206,6 @@ module Sentry
198
206
  @breadcrumbs_logger = logger
199
207
  end
200
208
 
201
- def should_capture=(value)
202
- unless value == false || value.respond_to?(:call)
203
- raise ArgumentError, "should_capture must be callable (or false to disable)"
204
- end
205
-
206
- @should_capture = value
207
- end
208
-
209
209
  def before_send=(value)
210
210
  unless value == false || value.respond_to?(:call)
211
211
  raise ArgumentError, "before_send must be callable (or false to disable)"
@@ -214,31 +214,21 @@ module Sentry
214
214
  @before_send = value
215
215
  end
216
216
 
217
- # Allows config options to be read like a hash
218
- #
219
- # @param [Symbol] option Key for a given attribute
220
- def [](option)
221
- public_send(option)
217
+ def environment=(environment)
218
+ @environment = environment.to_s
222
219
  end
223
220
 
224
- def current_environment=(environment)
225
- @current_environment = environment.to_s
226
- end
227
-
228
- def capture_allowed?(message_or_exc = nil)
221
+ def sending_allowed?
229
222
  @errors = []
230
223
 
231
224
  valid? &&
232
- capture_in_current_environment? &&
233
- capture_allowed_by_callback?(message_or_exc) &&
225
+ capture_in_environment? &&
234
226
  sample_allowed?
235
227
  end
236
- # If we cannot capture, we cannot send.
237
- alias sending_allowed? capture_allowed?
238
228
 
239
229
  def error_messages
240
- @errors = [errors[0]] + errors[1..-1].map(&:downcase) # fix case of all but first
241
- errors.join(", ")
230
+ @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first
231
+ @errors.join(", ")
242
232
  end
243
233
 
244
234
  def project_root=(root_dir)
@@ -259,7 +249,11 @@ module Sentry
259
249
  end
260
250
 
261
251
  def enabled_in_current_env?
262
- environments.empty? || environments.include?(current_environment)
252
+ enabled_environments.empty? || enabled_environments.include?(environment)
253
+ end
254
+
255
+ def tracing_enabled?
256
+ !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
263
257
  end
264
258
 
265
259
  private
@@ -282,13 +276,17 @@ module Sentry
282
276
  end
283
277
 
284
278
  def excluded_exception?(incoming_exception)
285
- excluded_exceptions.any? do |excluded_exception|
286
- matches_exception?(get_exception_class(excluded_exception), incoming_exception)
279
+ excluded_exception_classes.any? do |excluded_exception|
280
+ matches_exception?(excluded_exception, incoming_exception)
287
281
  end
288
282
  end
289
283
 
284
+ def excluded_exception_classes
285
+ @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) }
286
+ end
287
+
290
288
  def get_exception_class(x)
291
- x.is_a?(Module) ? x : qualified_const_get(x)
289
+ x.is_a?(Module) ? x : safe_const_get(x)
292
290
  end
293
291
 
294
292
  def matches_exception?(excluded_exception_class, incoming_exception)
@@ -299,14 +297,9 @@ module Sentry
299
297
  end
300
298
  end
301
299
 
302
- # In Ruby <2.0 const_get can't lookup "SomeModule::SomeClass" in one go
303
- def qualified_const_get(x)
304
- x = x.to_s
305
- if !x.match(/::/)
306
- Object.const_get(x)
307
- else
308
- x.split(MODULE_SEPARATOR).reject(&:empty?).inject(Object) { |a, e| a.const_get(e) }
309
- end
300
+ def safe_const_get(x)
301
+ x = x.to_s unless x.is_a?(String)
302
+ Object.const_get(x)
310
303
  rescue NameError # There's no way to safely ask if a constant exist for an unknown string
311
304
  nil
312
305
  end
@@ -342,17 +335,10 @@ module Sentry
342
335
  ENV['SENTRY_RELEASE']
343
336
  end
344
337
 
345
- def capture_in_current_environment?
338
+ def capture_in_environment?
346
339
  return true if enabled_in_current_env?
347
340
 
348
- @errors << "Not configured to send/capture in environment '#{current_environment}'"
349
- false
350
- end
351
-
352
- def capture_allowed_by_callback?(message_or_exc)
353
- return true if !should_capture || message_or_exc.nil? || should_capture.call(message_or_exc)
354
-
355
- @errors << "should_capture returned false"
341
+ @errors << "Not configured to send/capture in environment '#{environment}'"
356
342
  false
357
343
  end
358
344
 
@@ -383,7 +369,7 @@ module Sentry
383
369
  Socket.gethostbyname(hostname).first rescue server_name
384
370
  end
385
371
 
386
- def current_environment_from_env
372
+ def environment_from_env
387
373
  ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default'
388
374
  end
389
375
 
@@ -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
@@ -2,11 +2,10 @@
2
2
 
3
3
  require 'socket'
4
4
  require 'securerandom'
5
- require 'sentry/event/options'
6
5
  require 'sentry/interface'
7
6
  require 'sentry/backtrace'
8
- require 'sentry/utils/deep_merge'
9
7
  require 'sentry/utils/real_ip'
8
+ require 'sentry/utils/request_id'
10
9
 
11
10
  module Sentry
12
11
  class Event
@@ -15,52 +14,44 @@ module Sentry
15
14
  release environment server_name modules
16
15
  message user tags contexts extra
17
16
  fingerprint breadcrumbs backtrace transaction
18
- platform sdk
17
+ platform sdk type
19
18
  )
20
19
 
21
20
  attr_accessor(*ATTRIBUTES)
22
- attr_reader :id, :configuration
21
+ attr_reader :configuration
23
22
 
24
- alias event_id id
25
-
26
- def initialize(options:, configuration:)
23
+ def initialize(configuration:, message: nil)
27
24
  # this needs to go first because some setters rely on configuration
28
25
  @configuration = configuration
29
26
 
30
27
  # Set some simple default values
31
- @id = SecureRandom.uuid.delete("-")
32
- @timestamp = Time.now.utc
28
+ @event_id = SecureRandom.uuid.delete("-")
29
+ @timestamp = Sentry.utc_now.iso8601
33
30
  @platform = :ruby
34
31
  @sdk = Sentry.sdk_meta
35
32
 
36
- @user = options.user
37
- @extra = options.extra
38
- @contexts = options.contexts
39
- @tags = configuration.tags.merge(options.tags)
40
-
41
- @fingerprint = options.fingerprint
33
+ @user = {}
34
+ @extra = {}
35
+ @contexts = {}
36
+ @tags = {}
42
37
 
43
- @server_name = options.server_name || configuration.server_name
44
- @environment = options.environment || configuration.current_environment
45
- @release = options.release || configuration.release
46
- @modules = list_gem_specs if configuration.send_modules
38
+ @fingerprint = []
47
39
 
48
- @message = options.message if options.message
40
+ @server_name = configuration.server_name
41
+ @environment = configuration.environment
42
+ @release = configuration.release
43
+ @modules = configuration.gem_specs if configuration.send_modules
49
44
 
50
- self.level = options.level
45
+ @message = message || ""
51
46
 
52
- if !options.backtrace.empty?
53
- @stacktrace = Sentry::StacktraceInterface.new.tap do |int|
54
- int.frames = stacktrace_interface_from(options.backtrace)
55
- end
56
- end
47
+ self.level = :error
57
48
  end
58
49
 
59
50
  class << self
60
51
  def get_log_message(event_hash)
61
52
  message = event_hash[:message] || event_hash['message']
62
- message = get_message_from_exception(event_hash) if message.empty?
63
- 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?
64
55
  message
65
56
  end
66
57
 
@@ -76,7 +67,7 @@ module Sentry
76
67
  end
77
68
 
78
69
  def timestamp=(time)
79
- @timestamp = time.is_a?(Time) ? time.strftime('%Y-%m-%dT%H:%M:%S') : time
70
+ @timestamp = time.is_a?(Time) ? time.to_f : time
80
71
  end
81
72
 
82
73
  def level=(new_level) # needed to meet the Sentry spec
@@ -92,14 +83,18 @@ module Sentry
92
83
  if configuration.send_default_pii && ip = calculate_real_ip_from_rack(env.dup)
93
84
  user[:ip_address] = ip
94
85
  end
86
+ if request_id = Utils::RequestId.read_from(env)
87
+ tags[:request_id] = request_id
88
+ end
95
89
  end
96
90
  end
97
91
 
98
- def to_hash
99
- data = ATTRIBUTES.each_with_object({}) do |att, memo|
100
- memo[att] = public_send(att) if public_send(att)
101
- end
92
+ def type
93
+ "event"
94
+ end
102
95
 
96
+ def to_hash
97
+ data = serialize_attributes
103
98
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
104
99
  data[:stacktrace] = @stacktrace.to_hash if @stacktrace
105
100
  data[:request] = @request.to_hash if @request
@@ -113,6 +108,10 @@ module Sentry
113
108
  end
114
109
 
115
110
  def add_exception_interface(exc)
111
+ if exc.respond_to?(:sentry_context)
112
+ @extra.merge!(exc.sentry_context)
113
+ end
114
+
116
115
  @exception = Sentry::ExceptionInterface.new.tap do |exc_int|
117
116
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exc).reverse
118
117
  backtraces = Set.new
@@ -145,9 +144,9 @@ module Sentry
145
144
  frame.in_app = line.in_app
146
145
  frame.module = line.module_name if line.module_name
147
146
 
148
- if configuration[:context_lines] && frame.abs_path
147
+ if configuration.context_lines && frame.abs_path
149
148
  frame.pre_context, frame.context_line, frame.post_context = \
150
- 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)
151
150
  end
152
151
 
153
152
  memo << frame if frame.filename
@@ -156,6 +155,14 @@ module Sentry
156
155
 
157
156
  private
158
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
+
159
166
  # When behind a proxy (or if the user is using a proxy), we can't use
160
167
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
161
168
  def calculate_real_ip_from_rack(env)
@@ -166,10 +173,5 @@ module Sentry
166
173
  :forwarded_for => env["HTTP_X_FORWARDED_FOR"]
167
174
  ).calculate_ip
168
175
  end
169
-
170
- def list_gem_specs
171
- # Older versions of Rubygems don't support iterating over all specs
172
- Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
173
- end
174
176
  end
175
177
  end