sentry-ruby-core 4.7.3 → 4.8.3

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Gemfile +6 -2
  4. data/README.md +7 -7
  5. data/bin/console +5 -1
  6. data/lib/sentry/background_worker.rb +30 -2
  7. data/lib/sentry/backtrace.rb +1 -0
  8. data/lib/sentry/breadcrumb/sentry_logger.rb +2 -0
  9. data/lib/sentry/breadcrumb.rb +2 -0
  10. data/lib/sentry/breadcrumb_buffer.rb +2 -0
  11. data/lib/sentry/client.rb +13 -1
  12. data/lib/sentry/configuration.rb +136 -112
  13. data/lib/sentry/core_ext/object/deep_dup.rb +2 -0
  14. data/lib/sentry/core_ext/object/duplicable.rb +1 -0
  15. data/lib/sentry/dsn.rb +2 -0
  16. data/lib/sentry/envelope.rb +26 -0
  17. data/lib/sentry/event.rb +5 -0
  18. data/lib/sentry/exceptions.rb +2 -0
  19. data/lib/sentry/hub.rb +7 -0
  20. data/lib/sentry/integrable.rb +2 -0
  21. data/lib/sentry/interface.rb +2 -0
  22. data/lib/sentry/interfaces/exception.rb +2 -0
  23. data/lib/sentry/interfaces/single_exception.rb +31 -0
  24. data/lib/sentry/interfaces/stacktrace.rb +10 -0
  25. data/lib/sentry/interfaces/stacktrace_builder.rb +2 -0
  26. data/lib/sentry/interfaces/threads.rb +2 -0
  27. data/lib/sentry/linecache.rb +3 -0
  28. data/lib/sentry/net/http.rb +3 -0
  29. data/lib/sentry/rack/capture_exceptions.rb +2 -0
  30. data/lib/sentry/rack.rb +2 -0
  31. data/lib/sentry/rake.rb +16 -6
  32. data/lib/sentry/release_detector.rb +39 -0
  33. data/lib/sentry/scope.rb +8 -4
  34. data/lib/sentry/span.rb +1 -0
  35. data/lib/sentry/transaction.rb +6 -1
  36. data/lib/sentry/transport/configuration.rb +2 -0
  37. data/lib/sentry/transport/dummy_transport.rb +2 -0
  38. data/lib/sentry/transport/http_transport.rb +6 -4
  39. data/lib/sentry/transport.rb +77 -19
  40. data/lib/sentry/utils/argument_checking_helper.rb +2 -0
  41. data/lib/sentry/utils/custom_inspection.rb +14 -0
  42. data/lib/sentry/utils/exception_cause_chain.rb +10 -10
  43. data/lib/sentry/utils/logging_helper.rb +2 -0
  44. data/lib/sentry/utils/real_ip.rb +2 -0
  45. data/lib/sentry/utils/request_id.rb +2 -0
  46. data/lib/sentry/version.rb +3 -1
  47. data/lib/sentry-ruby.rb +102 -28
  48. data/sentry-ruby.gemspec +1 -1
  49. metadata +6 -2
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
4
+ # @api private
2
5
  class LineCache
3
6
  def initialize
4
7
  @cache = {}
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "net/http"
2
4
 
3
5
  module Sentry
6
+ # @api private
4
7
  module Net
5
8
  module HTTP
6
9
  OP_NAME = "net.http"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rack
3
5
  class CaptureExceptions
data/lib/sentry/rack.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
 
3
5
  require 'sentry/rack/capture_exceptions'
data/lib/sentry/rake.rb CHANGED
@@ -1,11 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rake"
2
4
  require "rake/task"
3
5
 
4
6
  module Sentry
5
7
  module Rake
6
8
  module Application
9
+ # @api private
7
10
  def display_error_message(ex)
8
- Sentry.capture_exception(ex, hint: { background: false }) do |scope|
11
+ Sentry.capture_exception(ex) do |scope|
9
12
  task_name = top_level_tasks.join(' ')
10
13
  scope.set_transaction_name(task_name)
11
14
  scope.set_tag("rake_task", task_name)
@@ -16,16 +19,23 @@ module Sentry
16
19
  end
17
20
 
18
21
  module Task
22
+ # @api private
19
23
  def execute(args=nil)
20
24
  return super unless Sentry.initialized? && Sentry.get_current_hub
21
25
 
22
- Sentry.get_current_hub.with_background_worker_disabled do
23
- super
24
- end
26
+ super
25
27
  end
26
28
  end
27
29
  end
28
30
  end
29
31
 
30
- Rake::Application.prepend(Sentry::Rake::Application)
31
- Rake::Task.prepend(Sentry::Rake::Task)
32
+ # @api private
33
+ module Rake
34
+ class Application
35
+ prepend(Sentry::Rake::Application)
36
+ end
37
+
38
+ class Task
39
+ prepend(Sentry::Rake::Task)
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class ReleaseDetector
6
+ class << self
7
+ def detect_release(project_root:, running_on_heroku:)
8
+ detect_release_from_env ||
9
+ detect_release_from_git ||
10
+ detect_release_from_capistrano(project_root) ||
11
+ detect_release_from_heroku(running_on_heroku)
12
+ end
13
+
14
+ def detect_release_from_heroku(running_on_heroku)
15
+ return unless running_on_heroku
16
+ ENV['HEROKU_SLUG_COMMIT']
17
+ end
18
+
19
+ def detect_release_from_capistrano(project_root)
20
+ revision_file = File.join(project_root, 'REVISION')
21
+ revision_log = File.join(project_root, '..', 'revisions.log')
22
+
23
+ if File.exist?(revision_file)
24
+ File.read(revision_file).strip
25
+ elsif File.exist?(revision_log)
26
+ File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1')
27
+ end
28
+ end
29
+
30
+ def detect_release_from_git
31
+ Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
32
+ end
33
+
34
+ def detect_release_from_env
35
+ ENV['SENTRY_RELEASE']
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/sentry/scope.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sentry/breadcrumb_buffer"
2
4
  require "etc"
3
5
 
@@ -112,7 +114,7 @@ module Sentry
112
114
  end
113
115
 
114
116
  def set_extra(key, value)
115
- @extra.merge!(key => value)
117
+ set_extras(key => value)
116
118
  end
117
119
 
118
120
  def set_tags(tags_hash)
@@ -121,17 +123,19 @@ module Sentry
121
123
  end
122
124
 
123
125
  def set_tag(key, value)
124
- @tags.merge!(key => value)
126
+ set_tags(key => value)
125
127
  end
126
128
 
127
129
  def set_contexts(contexts_hash)
128
130
  check_argument_type!(contexts_hash, Hash)
129
- @contexts.merge!(contexts_hash)
131
+ @contexts.merge!(contexts_hash) do |key, old, new|
132
+ new.merge(old)
133
+ end
130
134
  end
131
135
 
132
136
  def set_context(key, value)
133
137
  check_argument_type!(value, Hash)
134
- @contexts.merge!(key => value)
138
+ set_contexts(key => value)
135
139
  end
136
140
 
137
141
  def set_level(level)
data/lib/sentry/span.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "securerandom"
3
4
 
4
5
  module Sentry
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class Transaction < Span
3
5
  SENTRY_TRACE_REGEXP = Regexp.new(
@@ -129,7 +131,10 @@ module Sentry
129
131
  @name = UNLABELD_NAME
130
132
  end
131
133
 
132
- return unless @sampled || @parent_sampled
134
+ unless @sampled || @parent_sampled
135
+ hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
136
+ return
137
+ end
133
138
 
134
139
  event = hub.current_client.event_from_transaction(self)
135
140
  hub.capture_event(event)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class Transport
3
5
  class Configuration
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class DummyTransport < Transport
3
5
  attr_accessor :events
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'faraday'
2
4
  require 'zlib'
3
5
 
@@ -139,10 +141,10 @@ module Sentry
139
141
  end
140
142
 
141
143
  def ssl_configuration
142
- (@transport_configuration.ssl || {}).merge(
143
- :verify => @transport_configuration.ssl_verification,
144
- :ca_file => @transport_configuration.ssl_ca_file
145
- )
144
+ {
145
+ verify: @transport_configuration.ssl_verification,
146
+ ca_file: @transport_configuration.ssl_ca_file
147
+ }.merge(@transport_configuration.ssl || {})
146
148
  end
147
149
  end
148
150
  end
@@ -1,22 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "base64"
5
+ require "sentry/envelope"
3
6
 
4
7
  module Sentry
5
8
  class Transport
6
9
  PROTOCOL_VERSION = '7'
7
10
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
11
+ CLIENT_REPORT_INTERVAL = 30
12
+
13
+ # https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload
14
+ CLIENT_REPORT_REASONS = [
15
+ :ratelimit_backoff,
16
+ :queue_overflow,
17
+ :cache_overflow, # NA
18
+ :network_error,
19
+ :sample_rate,
20
+ :before_send,
21
+ :event_processor
22
+ ]
8
23
 
9
24
  include LoggingHelper
10
25
 
11
- attr_accessor :configuration
12
- attr_reader :logger, :rate_limits
26
+ attr_reader :logger, :rate_limits, :discarded_events, :last_client_report_sent
13
27
 
14
28
  def initialize(configuration)
15
- @configuration = configuration
16
29
  @logger = configuration.logger
17
30
  @transport_configuration = configuration.transport
18
31
  @dsn = configuration.dsn
19
32
  @rate_limits = {}
33
+ @send_client_reports = configuration.send_client_reports
34
+
35
+ if @send_client_reports
36
+ @discarded_events = Hash.new(0)
37
+ @last_client_report_sent = Time.now
38
+ end
20
39
  end
21
40
 
22
41
  def send_data(data, options = {})
@@ -27,14 +46,9 @@ module Sentry
27
46
  event_hash = event.to_hash
28
47
  item_type = get_item_type(event_hash)
29
48
 
30
- unless configuration.sending_allowed?
31
- log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}")
32
-
33
- return
34
- end
35
-
36
49
  if is_rate_limited?(item_type)
37
50
  log_info("Envelope [#{item_type}] not sent: rate limiting")
51
+ record_lost_event(:ratelimit_backoff, item_type)
38
52
 
39
53
  return
40
54
  end
@@ -91,20 +105,38 @@ module Sentry
91
105
 
92
106
  def encode(event)
93
107
  # Convert to hash
94
- event_hash = event.to_hash
108
+ event_payload = event.to_hash
109
+ event_id = event_payload[:event_id] || event_payload["event_id"]
110
+ item_type = get_item_type(event_payload)
111
+
112
+ envelope = Envelope.new(
113
+ {
114
+ event_id: event_id,
115
+ dsn: @dsn.to_s,
116
+ sdk: Sentry.sdk_meta,
117
+ sent_at: Sentry.utc_now.iso8601
118
+ }
119
+ )
120
+
121
+ envelope.add_item(
122
+ { type: item_type, content_type: 'application/json' },
123
+ event_payload
124
+ )
125
+
126
+ client_report_headers, client_report_payload = fetch_pending_client_report
127
+ envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
95
128
 
96
- event_id = event_hash[:event_id] || event_hash["event_id"]
97
- item_type = get_item_type(event_hash)
129
+ log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
98
130
 
99
- 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)}
103
- ENVELOPE
131
+ envelope.to_s
132
+ end
104
133
 
105
- log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
134
+ def record_lost_event(reason, item_type)
135
+ return unless @send_client_reports
136
+ return unless CLIENT_REPORT_REASONS.include?(reason)
106
137
 
107
- envelope
138
+ item_type ||= 'event'
139
+ @discarded_events[[reason, item_type]] += 1
108
140
  end
109
141
 
110
142
  private
@@ -112,6 +144,32 @@ module Sentry
112
144
  def get_item_type(event_hash)
113
145
  event_hash[:type] || event_hash["type"] || "event"
114
146
  end
147
+
148
+ def fetch_pending_client_report
149
+ return nil unless @send_client_reports
150
+ return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
151
+ return nil if @discarded_events.empty?
152
+
153
+ discarded_events_hash = @discarded_events.map do |key, val|
154
+ reason, type = key
155
+
156
+ # 'event' has to be mapped to 'error'
157
+ category = type == 'transaction' ? 'transaction' : 'error'
158
+
159
+ { reason: reason, category: category, quantity: val }
160
+ end
161
+
162
+ item_header = { type: 'client_report' }
163
+ item_payload = {
164
+ timestamp: Sentry.utc_now.iso8601,
165
+ discarded_events: discarded_events_hash
166
+ }
167
+
168
+ @discarded_events = Hash.new(0)
169
+ @last_client_report_sent = Time.now
170
+
171
+ [item_header, item_payload]
172
+ end
115
173
  end
116
174
  end
117
175
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module ArgumentCheckingHelper
3
5
  private
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module CustomInspection
5
+ def inspect
6
+ attr_strings = (instance_variables - self.class::SKIP_INSPECTION_ATTRIBUTES).each_with_object([]) do |attr, result|
7
+ value = instance_variable_get(attr)
8
+ result << "#{attr}=#{value.inspect}" if value
9
+ end
10
+
11
+ "#<#{self.class.name} #{attr_strings.join(", ")}>"
12
+ end
13
+ end
14
+ end
@@ -1,19 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Utils
3
5
  module ExceptionCauseChain
4
6
  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 }
7
+ exceptions = [exception]
8
+
9
+ while exception.cause
10
+ exception = exception.cause
11
+ break if exceptions.any? { |e| e.object_id == exception.object_id }
10
12
 
11
- exceptions << exception
12
- end
13
- exceptions
14
- else
15
- [exception]
13
+ exceptions << exception
16
14
  end
15
+
16
+ exceptions
17
17
  end
18
18
  end
19
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module LoggingHelper
3
5
  def log_error(message, exception, debug: false)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ipaddr'
2
4
 
3
5
  # Based on ActionDispatch::RemoteIp. All security-related precautions from that
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Utils
3
5
  module RequestId
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
- VERSION = "4.7.3"
4
+ VERSION = "4.8.3"
3
5
  end