sentry-ruby-core 4.7.0 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08bb1e6f6c55f8f9a206aabcb008ac4e8322652831108a0ec5145b69ed741c1d'
4
- data.tar.gz: 7d699edbb8b0a1e05c44b0d039f2e9ea79cba7355961488f1bc9311e8a92cb14
3
+ metadata.gz: f675111daf93ba05db7de62dcfa1dd9268f6ba009265de08e0e4f118ffea0c62
4
+ data.tar.gz: 2ece2ccaf14be6cc5fa762b77a57cd63dcc3fe52a03987c8c4136a43ec62bbd1
5
5
  SHA512:
6
- metadata.gz: ff6528918de5664c632d44c48cc04536cd41537e7fb2e5bc372c5794834cc4450b9d6aff82ba6ba4ad0a822ee5f96bc74a165ae6c1164299585019a187c63aad
7
- data.tar.gz: 9cf4dcecb8d81ec03048d22ffc75a0d4647b70a4bf037e9183ff794ef044f1ece661218a4ca22aef89cdbb4992361b1c9829a5aeeab0179430afc420c2d8a01f
6
+ metadata.gz: d5d9405c41879a40ba0c0205fa08e38fa5b3ed243276181239be63f0baa0b739b37ac91ecb86abd76768c4c5a706f1f7b7e47d7eadd357f2116ec2ec9bd913d5
7
+ data.tar.gz: abb46869f2eba00f053096e5568829815b02520db31fe9c98bbfabea40b5dcbe6d438ca97287ca7f9b4fee9a4710b49dec73b9196a3650d3a811184a960a09a7
data/Gemfile CHANGED
@@ -10,10 +10,12 @@ gem "rspec", "~> 3.0"
10
10
  gem "rspec-retry"
11
11
  gem "webmock"
12
12
  gem "timecop"
13
- gem "codecov", "0.2.12"
13
+ gem 'simplecov'
14
+ gem "simplecov-cobertura", "~> 1.4"
15
+ gem "rexml"
14
16
 
15
17
  gem "object_tracer"
16
- gem "debug", github: "ruby/debug" if RUBY_VERSION.to_f >= 2.6
18
+ gem "debug", github: "ruby/debug", platform: :ruby if RUBY_VERSION.to_f >= 2.6
17
19
  gem "pry"
18
20
 
19
21
  gem "benchmark-ips"
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020 getsentry
3
+ Copyright (c) 2020 Sentry
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -10,13 +10,13 @@ _Bad software is everywhere, and we're tired of it. Sentry is on a mission to he
10
10
  Sentry SDK for Ruby
11
11
  ===========
12
12
 
13
- | current version | build | coverage | downloads | semver stability |
14
- | --- | ----- | -------- | --------- | ---------------- |
15
- | [![Gem Version](https://img.shields.io/gem/v/sentry-ruby?label=sentry-ruby)](https://rubygems.org/gems/sentry-ruby) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-ruby%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_ruby_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-ruby.svg)](https://rubygems.org/gems/sentry-ruby/) | [![SemVer stability](https://api.dependabot.com/badges/compatibility_score?dependency-name=sentry-ruby&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=sentry-ruby&package-manager=bundler&version-scheme=semver) |
16
- | [![Gem Version](https://img.shields.io/gem/v/sentry-rails?label=sentry-rails)](https://rubygems.org/gems/sentry-rails) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-rails%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_rails_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-rails.svg)](https://rubygems.org/gems/sentry-rails/) | [![SemVer stability](https://api.dependabot.com/badges/compatibility_score?dependency-name=sentry-rails&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=sentry-rails&package-manager=bundler&version-scheme=semver) |
17
- | [![Gem Version](https://img.shields.io/gem/v/sentry-sidekiq?label=sentry-sidekiq)](https://rubygems.org/gems/sentry-sidekiq) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-sidekiq%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-sidekiq.svg)](https://rubygems.org/gems/sentry-sidekiq/) | [![SemVer stability](https://api.dependabot.com/badges/compatibility_score?dependency-name=sentry-sidekiq&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=sentry-sidekiq&package-manager=bundler&version-scheme=semver) |
18
- | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-delayed_job%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-delayed_job.svg)](https://rubygems.org/gems/sentry-delayed_job/) | [![SemVer stability](https://api.dependabot.com/badges/compatibility_score?dependency-name=sentry-delayed_job&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=sentry-delayed_job&package-manager=bundler&version-scheme=semver) |
19
- | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-resque%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-resque.svg)](https://rubygems.org/gems/sentry-resque/) | [![SemVer stability](https://api.dependabot.com/badges/compatibility_score?dependency-name=sentry-resque&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=sentry-resque&package-manager=bundler&version-scheme=semver) |
13
+ | current version | build | coverage | downloads |
14
+ | --- | ----- | -------- | --------- |
15
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-ruby?label=sentry-ruby)](https://rubygems.org/gems/sentry-ruby) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-ruby%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_ruby_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-ruby.svg)](https://rubygems.org/gems/sentry-ruby/) |
16
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-rails?label=sentry-rails)](https://rubygems.org/gems/sentry-rails) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-rails%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_rails_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-rails.svg)](https://rubygems.org/gems/sentry-rails/) |
17
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-sidekiq?label=sentry-sidekiq)](https://rubygems.org/gems/sentry-sidekiq) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-sidekiq%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-sidekiq.svg)](https://rubygems.org/gems/sentry-sidekiq/) |
18
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-delayed_job%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-delayed_job.svg)](https://rubygems.org/gems/sentry-delayed_job/) |
19
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-resque%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-resque.svg)](https://rubygems.org/gems/sentry-resque/) |
20
20
 
21
21
 
22
22
 
data/bin/console CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundler/setup"
4
- require "sentry/ruby"
4
+ require "sentry-ruby"
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,9 @@ require "sentry/ruby"
10
10
  # require "pry"
11
11
  # Pry.start
12
12
 
13
+ # Sentry.init do |config|
14
+ # config.dsn = 'https://2fb45f003d054a7ea47feb45898f7649@o447951.ingest.sentry.io/5434472'
15
+ # end
16
+
13
17
  require "irb"
14
18
  IRB.start(__FILE__)
data/lib/sentry/client.rb CHANGED
@@ -26,17 +26,25 @@ module Sentry
26
26
  def capture_event(event, scope, hint = {})
27
27
  return unless configuration.sending_allowed?
28
28
 
29
+ unless event.is_a?(TransactionEvent) || configuration.sample_allowed?
30
+ transport.record_lost_event(:sample_rate, 'event')
31
+ return
32
+ end
33
+
34
+ event_type = event.is_a?(Event) ? event.type : event["type"]
29
35
  event = scope.apply_to_event(event, hint)
30
36
 
31
37
  if event.nil?
32
38
  log_info("Discarded event because one of the event processors returned nil")
39
+ transport.record_lost_event(:event_processor, event_type)
33
40
  return
34
41
  end
35
42
 
36
43
  if async_block = configuration.async
37
44
  dispatch_async_event(async_block, event, hint)
38
45
  elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true)
39
- dispatch_background_event(event, hint)
46
+ queued = dispatch_background_event(event, hint)
47
+ transport.record_lost_event(:queue_overflow, event_type) unless queued
40
48
  else
41
49
  send_event(event, hint)
42
50
  end
@@ -84,6 +92,7 @@ module Sentry
84
92
 
85
93
  if event.nil?
86
94
  log_info("Discarded event because before_send returned nil")
95
+ transport.record_lost_event(:before_send, 'event')
87
96
  return
88
97
  end
89
98
  end
@@ -97,6 +106,7 @@ module Sentry
97
106
 
98
107
  event_info = Event.get_log_message(event.to_hash)
99
108
  log_info("Unreported #{loggable_event_type}: #{event_info}")
109
+ transport.record_lost_event(:network_error, event_type)
100
110
  raise
101
111
  end
102
112
 
@@ -1,13 +1,16 @@
1
1
  require "concurrent/utility/processor_counter"
2
2
 
3
3
  require "sentry/utils/exception_cause_chain"
4
+ require 'sentry/utils/custom_inspection'
4
5
  require "sentry/dsn"
6
+ require "sentry/release_detector"
5
7
  require "sentry/transport/configuration"
6
8
  require "sentry/linecache"
7
9
  require "sentry/interfaces/stacktrace_builder"
8
10
 
9
11
  module Sentry
10
12
  class Configuration
13
+ include CustomInspection
11
14
  include LoggingHelper
12
15
  # Directories to be recognized as part of your app. e.g. if you
13
16
  # have an `engines` dir at the root of your project, you may want
@@ -63,6 +66,9 @@ module Sentry
63
66
  # - :active_support_logger
64
67
  attr_reader :breadcrumbs_logger
65
68
 
69
+ # Whether to capture local variables from the raised exception's frame. Default is false.
70
+ attr_accessor :capture_exception_frame_locals
71
+
66
72
  # Max number of breadcrumbs a breadcrumb buffer can hold
67
73
  attr_accessor :max_breadcrumbs
68
74
 
@@ -104,7 +110,7 @@ module Sentry
104
110
 
105
111
  # Project directory root for in_app detection. Could be Rails root, etc.
106
112
  # Set automatically for Rails.
107
- attr_reader :project_root
113
+ attr_accessor :project_root
108
114
 
109
115
  # Insert sentry-trace to outgoing requests' headers
110
116
  attr_accessor :propagate_traces
@@ -154,6 +160,10 @@ module Sentry
154
160
  # ```
155
161
  attr_accessor :traces_sampler
156
162
 
163
+ # Send diagnostic client reports about dropped events, true by default
164
+ # tries to attach to an existing envelope max once every 30s
165
+ attr_accessor :send_client_reports
166
+
157
167
  # these are not config options
158
168
  attr_reader :errors, :gem_specs
159
169
 
@@ -177,17 +187,21 @@ module Sentry
177
187
 
178
188
  LOG_PREFIX = "** [Sentry] ".freeze
179
189
  MODULE_SEPARATOR = "::".freeze
190
+ SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder]
180
191
 
181
192
  # Post initialization callbacks are called at the end of initialization process
182
193
  # allowing extending the configuration of sentry-ruby by multiple extensions
183
194
  @@post_initialization_callbacks = []
184
195
 
185
196
  def initialize
197
+ self.app_dirs_pattern = nil
186
198
  self.debug = false
187
199
  self.background_worker_threads = Concurrent.processor_count
200
+ self.backtrace_cleanup_callback = nil
188
201
  self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
189
202
  self.breadcrumbs_logger = []
190
203
  self.context_lines = 3
204
+ self.capture_exception_frame_locals = false
191
205
  self.environment = environment_from_env
192
206
  self.enabled_environments = []
193
207
  self.exclude_loggers = []
@@ -202,12 +216,15 @@ module Sentry
202
216
  self.send_modules = true
203
217
  self.send_default_pii = false
204
218
  self.skip_rake_integration = false
219
+ self.send_client_reports = true
205
220
  self.trusted_proxies = []
206
221
  self.dsn = ENV['SENTRY_DSN']
207
222
  self.server_name = server_name_from_env
208
223
 
209
- self.before_send = false
224
+ self.before_send = nil
210
225
  self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
226
+ self.traces_sample_rate = nil
227
+ self.traces_sampler = nil
211
228
 
212
229
  @transport = Transport::Configuration.new
213
230
  @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
@@ -216,18 +233,13 @@ module Sentry
216
233
  end
217
234
 
218
235
  def dsn=(value)
219
- return if value.nil? || value.empty?
220
-
221
- @dsn = DSN.new(value)
236
+ @dsn = init_dsn(value)
222
237
  end
223
238
 
224
239
  alias server= dsn=
225
240
 
226
-
227
241
  def async=(value)
228
- if value && !value.respond_to?(:call)
229
- raise(ArgumentError, "async must be callable")
230
- end
242
+ check_callable!("async", value)
231
243
 
232
244
  @async = value
233
245
  end
@@ -246,17 +258,13 @@ module Sentry
246
258
  end
247
259
 
248
260
  def before_send=(value)
249
- unless value == false || value.respond_to?(:call)
250
- raise ArgumentError, "before_send must be callable (or false to disable)"
251
- end
261
+ check_callable!("before_send", value)
252
262
 
253
263
  @before_send = value
254
264
  end
255
265
 
256
266
  def before_breadcrumb=(value)
257
- unless value.nil? || value.respond_to?(:call)
258
- raise ArgumentError, "before_breadcrumb must be callable (or nil to disable)"
259
- end
267
+ check_callable!("before_breadcrumb", value)
260
268
 
261
269
  @before_breadcrumb = value
262
270
  end
@@ -268,9 +276,13 @@ module Sentry
268
276
  def sending_allowed?
269
277
  @errors = []
270
278
 
271
- valid? &&
272
- capture_in_environment? &&
273
- sample_allowed?
279
+ valid? && capture_in_environment?
280
+ end
281
+
282
+ def sample_allowed?
283
+ return true if sample_rate == 1.0
284
+
285
+ Random.rand < sample_rate
274
286
  end
275
287
 
276
288
  def error_messages
@@ -278,10 +290,6 @@ module Sentry
278
290
  @errors.join(", ")
279
291
  end
280
292
 
281
- def project_root=(root_dir)
282
- @project_root = root_dir
283
- end
284
-
285
293
  def exception_class_allowed?(exc)
286
294
  if exc.is_a?(Sentry::Error)
287
295
  # Try to prevent error reporting loops
@@ -316,10 +324,11 @@ module Sentry
316
324
  def detect_release
317
325
  return unless sending_allowed?
318
326
 
319
- self.release ||= detect_release_from_env ||
320
- detect_release_from_git ||
321
- detect_release_from_capistrano ||
322
- detect_release_from_heroku
327
+ self.release ||= ReleaseDetector.detect_release(project_root: project_root, running_on_heroku: running_on_heroku?)
328
+
329
+ if running_on_heroku? && release.nil?
330
+ log_warn(HEROKU_DYNO_METADATA_MESSAGE)
331
+ end
323
332
  rescue => e
324
333
  log_error("Error detecting release", e, debug: debug)
325
334
  end
@@ -335,6 +344,18 @@ module Sentry
335
344
 
336
345
  private
337
346
 
347
+ def check_callable!(name, value)
348
+ unless value == nil || value.respond_to?(:call)
349
+ raise ArgumentError, "#{name} must be callable (or nil to disable)"
350
+ end
351
+ end
352
+
353
+ def init_dsn(dsn_string)
354
+ return if dsn_string.nil? || dsn_string.empty?
355
+
356
+ DSN.new(dsn_string)
357
+ end
358
+
338
359
  def excluded_exception?(incoming_exception)
339
360
  excluded_exception_classes.any? do |excluded_exception|
340
361
  matches_exception?(excluded_exception, incoming_exception)
@@ -364,37 +385,6 @@ module Sentry
364
385
  nil
365
386
  end
366
387
 
367
- def detect_release_from_heroku
368
- return unless running_on_heroku?
369
- return if ENV['CI']
370
- log_warn(HEROKU_DYNO_METADATA_MESSAGE) && return unless ENV['HEROKU_SLUG_COMMIT']
371
-
372
- ENV['HEROKU_SLUG_COMMIT']
373
- end
374
-
375
- def running_on_heroku?
376
- File.directory?("/etc/heroku")
377
- end
378
-
379
- def detect_release_from_capistrano
380
- revision_file = File.join(project_root, 'REVISION')
381
- revision_log = File.join(project_root, '..', 'revisions.log')
382
-
383
- if File.exist?(revision_file)
384
- File.read(revision_file).strip
385
- elsif File.exist?(revision_log)
386
- File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1')
387
- end
388
- end
389
-
390
- def detect_release_from_git
391
- Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
392
- end
393
-
394
- def detect_release_from_env
395
- ENV['SENTRY_RELEASE']
396
- end
397
-
398
388
  def capture_in_environment?
399
389
  return true if enabled_in_current_env?
400
390
 
@@ -411,36 +401,24 @@ module Sentry
411
401
  end
412
402
  end
413
403
 
414
- def sample_allowed?
415
- return true if sample_rate == 1.0
416
-
417
- if Random.rand >= sample_rate
418
- @errors << "Excluded by random sample"
419
- false
420
- else
421
- true
422
- end
423
- end
424
-
425
- # Try to resolve the hostname to an FQDN, but fall back to whatever
426
- # the load name is.
427
- def resolve_hostname
428
- Socket.gethostname ||
429
- Socket.gethostbyname(hostname).first rescue server_name
430
- end
431
-
432
404
  def environment_from_env
433
- ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default'
405
+ ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
434
406
  end
435
407
 
436
408
  def server_name_from_env
437
409
  if running_on_heroku?
438
410
  ENV['DYNO']
439
411
  else
440
- resolve_hostname
412
+ # Try to resolve the hostname to an FQDN, but fall back to whatever
413
+ # the load name is.
414
+ Socket.gethostname || Socket.gethostbyname(hostname).first rescue server_name
441
415
  end
442
416
  end
443
417
 
418
+ def running_on_heroku?
419
+ File.directory?("/etc/heroku") && !ENV["CI"]
420
+ end
421
+
444
422
  def run_post_initialization_callbacks
445
423
  self.class.post_initialization_callbacks.each do |hook|
446
424
  instance_eval(&hook)
data/lib/sentry/event.rb CHANGED
@@ -6,6 +6,7 @@ require 'sentry/interface'
6
6
  require 'sentry/backtrace'
7
7
  require 'sentry/utils/real_ip'
8
8
  require 'sentry/utils/request_id'
9
+ require 'sentry/utils/custom_inspection'
9
10
 
10
11
  module Sentry
11
12
  class Event
@@ -21,6 +22,10 @@ module Sentry
21
22
 
22
23
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
23
24
 
25
+ SKIP_INSPECTION_ATTRIBUTES = [:@configuration, :@modules, :@backtrace]
26
+
27
+ include CustomInspection
28
+
24
29
  attr_writer(*WRITER_ATTRIBUTES)
25
30
  attr_reader(*SERIALIZEABLE_ATTRIBUTES)
26
31
 
data/lib/sentry/hub.rb CHANGED
@@ -90,10 +90,10 @@ module Sentry
90
90
  end
91
91
 
92
92
  def capture_exception(exception, **options, &block)
93
- return unless current_client
94
-
95
93
  check_argument_type!(exception, ::Exception)
96
94
 
95
+ return unless current_client
96
+
97
97
  options[:hint] ||= {}
98
98
  options[:hint][:exception] = exception
99
99
  event = current_client.event_from_exception(exception, options[:hint])
@@ -104,6 +104,8 @@ module Sentry
104
104
  end
105
105
 
106
106
  def capture_message(message, **options, &block)
107
+ check_argument_type!(message, ::String)
108
+
107
109
  return unless current_client
108
110
 
109
111
  options[:hint] ||= {}
@@ -114,10 +116,10 @@ module Sentry
114
116
  end
115
117
 
116
118
  def capture_event(event, **options, &block)
117
- return unless current_client
118
-
119
119
  check_argument_type!(event, Sentry::Event)
120
120
 
121
+ return unless current_client
122
+
121
123
  hint = options.delete(:hint) || {}
122
124
  scope = current_scope.dup
123
125
 
@@ -131,6 +133,11 @@ module Sentry
131
133
 
132
134
  event = current_client.capture_event(event, scope, hint)
133
135
 
136
+
137
+ if event && configuration.debug
138
+ configuration.log_debug(event.to_json_compatible)
139
+ end
140
+
134
141
  @last_event_id = event&.event_id
135
142
  event
136
143
  end
@@ -1,5 +1,14 @@
1
+ require "sentry/utils/exception_cause_chain"
2
+
1
3
  module Sentry
2
4
  class SingleExceptionInterface < Interface
5
+ include CustomInspection
6
+
7
+ SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace]
8
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]".freeze
9
+ OMISSION_MARK = "...".freeze
10
+ MAX_LOCAL_BYTES = 1024
11
+
3
12
  attr_reader :type, :value, :module, :thread_id, :stacktrace
4
13
 
5
14
  def initialize(exception:, stacktrace: nil)
@@ -20,6 +29,26 @@ module Sentry
20
29
  # also see `StacktraceBuilder.build`.
21
30
  def self.build_with_stacktrace(exception:, stacktrace_builder:)
22
31
  stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
32
+
33
+ if locals = exception.instance_variable_get(:@sentry_locals)
34
+ locals.each do |k, v|
35
+ locals[k] =
36
+ begin
37
+ v = v.inspect unless v.is_a?(String)
38
+
39
+ if v.length >= MAX_LOCAL_BYTES
40
+ v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK
41
+ end
42
+
43
+ v
44
+ rescue StandardError
45
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT
46
+ end
47
+ end
48
+
49
+ stacktrace.frames.last.vars = locals
50
+ end
51
+
23
52
  new(exception: exception, stacktrace: stacktrace)
24
53
  end
25
54
  end
@@ -10,6 +10,10 @@ module Sentry
10
10
  { frames: @frames.map(&:to_hash) }
11
11
  end
12
12
 
13
+ def inspect
14
+ @frames.map(&:to_s)
15
+ end
16
+
13
17
  private
14
18
 
15
19
  # Not actually an interface, but I want to use the same style
@@ -28,6 +32,10 @@ module Sentry
28
32
  @filename = compute_filename
29
33
  end
30
34
 
35
+ def to_s
36
+ "#{@filename}:#{@lineno}"
37
+ end
38
+
31
39
  def compute_filename
32
40
  return if abs_path.nil?
33
41
 
@@ -0,0 +1,36 @@
1
+ module Sentry
2
+ class ReleaseDetector
3
+ class << self
4
+ def detect_release(project_root:, running_on_heroku:)
5
+ detect_release_from_env ||
6
+ detect_release_from_git ||
7
+ detect_release_from_capistrano(project_root) ||
8
+ detect_release_from_heroku(running_on_heroku)
9
+ end
10
+
11
+ def detect_release_from_heroku(running_on_heroku)
12
+ return unless running_on_heroku
13
+ ENV['HEROKU_SLUG_COMMIT']
14
+ end
15
+
16
+ def detect_release_from_capistrano(project_root)
17
+ revision_file = File.join(project_root, 'REVISION')
18
+ revision_log = File.join(project_root, '..', 'revisions.log')
19
+
20
+ if File.exist?(revision_file)
21
+ File.read(revision_file).strip
22
+ elsif File.exist?(revision_log)
23
+ File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1')
24
+ end
25
+ end
26
+
27
+ def detect_release_from_git
28
+ Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
29
+ end
30
+
31
+ def detect_release_from_env
32
+ ENV['SENTRY_RELEASE']
33
+ end
34
+ end
35
+ end
36
+ end
@@ -129,7 +129,10 @@ module Sentry
129
129
  @name = UNLABELD_NAME
130
130
  end
131
131
 
132
- return unless @sampled || @parent_sampled
132
+ unless @sampled || @parent_sampled
133
+ hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
134
+ return
135
+ end
133
136
 
134
137
  event = hub.current_client.event_from_transaction(self)
135
138
  hub.capture_event(event)
@@ -5,18 +5,34 @@ module Sentry
5
5
  class Transport
6
6
  PROTOCOL_VERSION = '7'
7
7
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
8
+ CLIENT_REPORT_INTERVAL = 30
9
+
10
+ # https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload
11
+ CLIENT_REPORT_REASONS = [
12
+ :ratelimit_backoff,
13
+ :queue_overflow,
14
+ :cache_overflow, # NA
15
+ :network_error,
16
+ :sample_rate,
17
+ :before_send,
18
+ :event_processor
19
+ ]
8
20
 
9
21
  include LoggingHelper
10
22
 
11
- attr_accessor :configuration
12
- attr_reader :logger, :rate_limits
23
+ attr_reader :logger, :rate_limits, :discarded_events, :last_client_report_sent
13
24
 
14
25
  def initialize(configuration)
15
- @configuration = configuration
16
26
  @logger = configuration.logger
17
27
  @transport_configuration = configuration.transport
18
28
  @dsn = configuration.dsn
19
29
  @rate_limits = {}
30
+ @send_client_reports = configuration.send_client_reports
31
+
32
+ if @send_client_reports
33
+ @discarded_events = Hash.new(0)
34
+ @last_client_report_sent = Time.now
35
+ end
20
36
  end
21
37
 
22
38
  def send_data(data, options = {})
@@ -27,14 +43,9 @@ module Sentry
27
43
  event_hash = event.to_hash
28
44
  item_type = get_item_type(event_hash)
29
45
 
30
- unless configuration.sending_allowed?
31
- log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}")
32
-
33
- return
34
- end
35
-
36
46
  if is_rate_limited?(item_type)
37
47
  log_info("Envelope [#{item_type}] not sent: rate limiting")
48
+ record_lost_event(:ratelimit_backoff, item_type)
38
49
 
39
50
  return
40
51
  end
@@ -91,27 +102,79 @@ module Sentry
91
102
 
92
103
  def encode(event)
93
104
  # Convert to hash
94
- event_hash = event.to_hash
105
+ event_payload = event.to_hash
95
106
 
96
- event_id = event_hash[:event_id] || event_hash["event_id"]
97
- item_type = get_item_type(event_hash)
107
+ event_id = event_payload[:event_id] || event_payload["event_id"]
108
+ item_type = get_item_type(event_payload)
109
+
110
+ envelope_header = {
111
+ event_id: event_id,
112
+ dsn: @dsn.to_s,
113
+ sdk: Sentry.sdk_meta,
114
+ sent_at: Sentry.utc_now.iso8601
115
+ }
116
+
117
+ event_header = { type: item_type, content_type: 'application/json' }
98
118
 
99
119
  envelope = <<~ENVELOPE
100
- {"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{Sentry.utc_now.iso8601}"}
101
- {"type":"#{item_type}","content_type":"application/json"}
102
- #{JSON.generate(event_hash)}
120
+ #{JSON.generate(envelope_header)}
121
+ #{JSON.generate(event_header)}
122
+ #{JSON.generate(event_payload)}
103
123
  ENVELOPE
104
124
 
125
+ client_report = fetch_pending_client_report
126
+ envelope << client_report if client_report
127
+
105
128
  log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
106
129
 
107
130
  envelope
108
131
  end
109
132
 
133
+ def record_lost_event(reason, item_type)
134
+ return unless @send_client_reports
135
+ return unless CLIENT_REPORT_REASONS.include?(reason)
136
+
137
+ item_type ||= 'event'
138
+ @discarded_events[[reason, item_type]] += 1
139
+ end
140
+
110
141
  private
111
142
 
112
143
  def get_item_type(event_hash)
113
144
  event_hash[:type] || event_hash["type"] || "event"
114
145
  end
146
+
147
+ def fetch_pending_client_report
148
+ return nil unless @send_client_reports
149
+ return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
150
+ return nil if @discarded_events.empty?
151
+
152
+ discarded_events_hash = @discarded_events.map do |key, val|
153
+ reason, type = key
154
+
155
+ # 'event' has to be mapped to 'error'
156
+ category = type == 'transaction' ? 'transaction' : 'error'
157
+
158
+ { reason: reason, category: category, quantity: val }
159
+ end
160
+
161
+ item_header = { type: 'client_report' }
162
+
163
+ item_payload = {
164
+ timestamp: Sentry.utc_now.iso8601,
165
+ discarded_events: discarded_events_hash
166
+ }
167
+
168
+ client_report_item = <<~CLIENT_REPORT_ITEM
169
+ #{JSON.generate(item_header)}
170
+ #{JSON.generate(item_payload)}
171
+ CLIENT_REPORT_ITEM
172
+
173
+ @discarded_events = Hash.new(0)
174
+ @last_client_report_sent = Time.now
175
+
176
+ client_report_item
177
+ end
115
178
  end
116
179
  end
117
180
 
@@ -0,0 +1,12 @@
1
+ module Sentry
2
+ module CustomInspection
3
+ def inspect
4
+ attr_strings = (instance_variables - self.class::SKIP_INSPECTION_ATTRIBUTES).each_with_object([]) do |attr, result|
5
+ value = instance_variable_get(attr)
6
+ result << "#{attr}=#{value.inspect}" if value
7
+ end
8
+
9
+ "#<#{self.class.name} #{attr_strings.join(", ")}>"
10
+ end
11
+ end
12
+ end
@@ -2,18 +2,16 @@ module Sentry
2
2
  module Utils
3
3
  module ExceptionCauseChain
4
4
  def self.exception_to_array(exception)
5
- if exception.respond_to?(:cause) && exception.cause
6
- exceptions = [exception]
7
- while exception.cause
8
- exception = exception.cause
9
- break if exceptions.any? { |e| e.object_id == exception.object_id }
5
+ exceptions = [exception]
10
6
 
11
- exceptions << exception
12
- end
13
- exceptions
14
- else
15
- [exception]
7
+ while exception.cause
8
+ exception = exception.cause
9
+ break if exceptions.any? { |e| e.object_id == exception.object_id }
10
+
11
+ exceptions << exception
16
12
  end
13
+
14
+ exceptions
17
15
  end
18
16
  end
19
17
  end
@@ -1,3 +1,3 @@
1
1
  module Sentry
2
- VERSION = "4.7.0"
2
+ VERSION = "4.8.0"
3
3
  end
data/lib/sentry-ruby.rb CHANGED
@@ -37,6 +37,22 @@ module Sentry
37
37
  THREAD_LOCAL = :sentry_hub
38
38
 
39
39
  class << self
40
+ def exception_locals_tp
41
+ @exception_locals_tp ||= TracePoint.new(:raise) do |tp|
42
+ exception = tp.raised_exception
43
+
44
+ # don't collect locals again if the exception is re-raised
45
+ next if exception.instance_variable_get(:@sentry_locals)
46
+ next unless tp.binding
47
+
48
+ locals = tp.binding.local_variables.each_with_object({}) do |local, result|
49
+ result[local] = tp.binding.local_variable_get(local)
50
+ end
51
+
52
+ exception.instance_variable_set(:@sentry_locals, locals)
53
+ end
54
+ end
55
+
40
56
  attr_accessor :background_worker
41
57
 
42
58
  ##### Patch Registration #####
@@ -88,6 +104,10 @@ module Sentry
88
104
  Thread.current.thread_variable_set(THREAD_LOCAL, hub)
89
105
  @main_hub = hub
90
106
  @background_worker = Sentry::BackgroundWorker.new(config)
107
+
108
+ if config.capture_exception_frame_locals
109
+ exception_locals_tp.enable
110
+ end
91
111
  end
92
112
 
93
113
  # Returns an uri for security policy reporting that's generated from the given DSN
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.7.0
4
+ version: 4.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-01 00:00:00.000000000 Z
11
+ date: 2021-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -85,6 +85,7 @@ files:
85
85
  - lib/sentry/rack.rb
86
86
  - lib/sentry/rack/capture_exceptions.rb
87
87
  - lib/sentry/rake.rb
88
+ - lib/sentry/release_detector.rb
88
89
  - lib/sentry/scope.rb
89
90
  - lib/sentry/span.rb
90
91
  - lib/sentry/transaction.rb
@@ -94,6 +95,7 @@ files:
94
95
  - lib/sentry/transport/dummy_transport.rb
95
96
  - lib/sentry/transport/http_transport.rb
96
97
  - lib/sentry/utils/argument_checking_helper.rb
98
+ - lib/sentry/utils/custom_inspection.rb
97
99
  - lib/sentry/utils/exception_cause_chain.rb
98
100
  - lib/sentry/utils/logging_helper.rb
99
101
  - lib/sentry/utils/real_ip.rb