sentry-ruby-core 4.7.2 → 5.0.2

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/Gemfile +6 -2
  4. data/README.md +9 -7
  5. data/bin/console +5 -1
  6. data/lib/sentry/background_worker.rb +33 -3
  7. data/lib/sentry/backtrace.rb +1 -3
  8. data/lib/sentry/breadcrumb/sentry_logger.rb +2 -0
  9. data/lib/sentry/breadcrumb.rb +24 -3
  10. data/lib/sentry/breadcrumb_buffer.rb +16 -0
  11. data/lib/sentry/client.rb +49 -3
  12. data/lib/sentry/configuration.rb +139 -114
  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 +58 -17
  18. data/lib/sentry/exceptions.rb +2 -0
  19. data/lib/sentry/hub.rb +16 -4
  20. data/lib/sentry/integrable.rb +2 -0
  21. data/lib/sentry/interface.rb +3 -10
  22. data/lib/sentry/interfaces/exception.rb +13 -3
  23. data/lib/sentry/interfaces/request.rb +34 -18
  24. data/lib/sentry/interfaces/single_exception.rb +31 -0
  25. data/lib/sentry/interfaces/stacktrace.rb +14 -0
  26. data/lib/sentry/interfaces/stacktrace_builder.rb +39 -10
  27. data/lib/sentry/interfaces/threads.rb +12 -2
  28. data/lib/sentry/linecache.rb +3 -0
  29. data/lib/sentry/net/http.rb +52 -64
  30. data/lib/sentry/rack/capture_exceptions.rb +2 -0
  31. data/lib/sentry/rack.rb +2 -0
  32. data/lib/sentry/rake.rb +16 -6
  33. data/lib/sentry/release_detector.rb +39 -0
  34. data/lib/sentry/scope.rb +75 -5
  35. data/lib/sentry/span.rb +84 -8
  36. data/lib/sentry/transaction.rb +48 -10
  37. data/lib/sentry/transaction_event.rb +8 -0
  38. data/lib/sentry/transport/configuration.rb +3 -2
  39. data/lib/sentry/transport/dummy_transport.rb +2 -0
  40. data/lib/sentry/transport/http_transport.rb +55 -42
  41. data/lib/sentry/transport.rb +80 -19
  42. data/lib/sentry/utils/argument_checking_helper.rb +2 -0
  43. data/lib/sentry/utils/custom_inspection.rb +14 -0
  44. data/lib/sentry/utils/exception_cause_chain.rb +10 -10
  45. data/lib/sentry/utils/logging_helper.rb +6 -4
  46. data/lib/sentry/utils/real_ip.rb +2 -0
  47. data/lib/sentry/utils/request_id.rb +2 -0
  48. data/lib/sentry/version.rb +3 -1
  49. data/lib/sentry-ruby.rb +142 -29
  50. data/sentry-ruby-core.gemspec +0 -1
  51. data/sentry-ruby.gemspec +0 -1
  52. metadata +6 -16
@@ -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(
@@ -12,7 +14,22 @@ module Sentry
12
14
 
13
15
  include LoggingHelper
14
16
 
15
- attr_reader :name, :parent_sampled, :hub, :configuration, :logger
17
+ # The name of the transaction.
18
+ # @return [String]
19
+ attr_reader :name
20
+
21
+ # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
22
+ # @return [String]
23
+ attr_reader :parent_sampled
24
+
25
+ # @deprecated Use Sentry.get_current_hub instead.
26
+ attr_reader :hub
27
+
28
+ # @deprecated Use Sentry.configuration instead.
29
+ attr_reader :configuration
30
+
31
+ # @deprecated Use Sentry.logger instead.
32
+ attr_reader :logger
16
33
 
17
34
  def initialize(name: nil, parent_sampled: nil, hub:, **options)
18
35
  super(**options)
@@ -21,11 +38,23 @@ module Sentry
21
38
  @parent_sampled = parent_sampled
22
39
  @transaction = self
23
40
  @hub = hub
24
- @configuration = hub.configuration
25
- @logger = configuration.logger
41
+ @configuration = hub.configuration # to be removed
42
+ @tracing_enabled = hub.configuration.tracing_enabled?
43
+ @traces_sampler = hub.configuration.traces_sampler
44
+ @traces_sample_rate = hub.configuration.traces_sample_rate
45
+ @logger = hub.configuration.logger
26
46
  init_span_recorder
27
47
  end
28
48
 
49
+ # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
50
+ #
51
+ # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
52
+ #
53
+ # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
54
+ # @param sentry_trace [String] the trace string from the previous transaction.
55
+ # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
56
+ # @param options [Hash] the options you want to use to initialize a Transaction instance.
57
+ # @return [Transaction, nil]
29
58
  def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
30
59
  return unless hub.configuration.tracing_enabled?
31
60
  return unless sentry_trace
@@ -44,12 +73,14 @@ module Sentry
44
73
  new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
45
74
  end
46
75
 
76
+ # @return [Hash]
47
77
  def to_hash
48
78
  hash = super
49
79
  hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
50
80
  hash
51
81
  end
52
82
 
83
+ # @return [Transaction]
53
84
  def deep_dup
54
85
  copy = super
55
86
  copy.init_span_recorder(@span_recorder.max_length)
@@ -63,23 +94,24 @@ module Sentry
63
94
  copy
64
95
  end
65
96
 
97
+ # Sets initial sampling decision of the transaction.
98
+ # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
99
+ # @return [void]
66
100
  def set_initial_sample_decision(sampling_context:)
67
- unless configuration.tracing_enabled?
101
+ unless @tracing_enabled
68
102
  @sampled = false
69
103
  return
70
104
  end
71
105
 
72
106
  return unless @sampled.nil?
73
107
 
74
- traces_sampler = configuration.traces_sampler
75
-
76
108
  sample_rate =
77
- if traces_sampler.is_a?(Proc)
78
- traces_sampler.call(sampling_context)
109
+ if @traces_sampler.is_a?(Proc)
110
+ @traces_sampler.call(sampling_context)
79
111
  elsif !sampling_context[:parent_sampled].nil?
80
112
  sampling_context[:parent_sampled]
81
113
  else
82
- configuration.traces_sample_rate
114
+ @traces_sample_rate
83
115
  end
84
116
 
85
117
  transaction_description = generate_transaction_description
@@ -111,6 +143,9 @@ module Sentry
111
143
  end
112
144
  end
113
145
 
146
+ # Finishes the transaction's recording and send it to Sentry.
147
+ # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
148
+ # @return [TransactionEvent]
114
149
  def finish(hub: nil)
115
150
  if hub
116
151
  log_warn(
@@ -129,7 +164,10 @@ module Sentry
129
164
  @name = UNLABELD_NAME
130
165
  end
131
166
 
132
- return unless @sampled || @parent_sampled
167
+ unless @sampled || @parent_sampled
168
+ hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
169
+ return
170
+ end
133
171
 
134
172
  event = hub.current_client.event_from_transaction(self)
135
173
  hub.capture_event(event)
@@ -16,17 +16,25 @@ module Sentry
16
16
  attr_writer(*WRITER_ATTRIBUTES)
17
17
  attr_reader(*SERIALIZEABLE_ATTRIBUTES)
18
18
 
19
+ # @return [<Array[Span]>]
19
20
  attr_accessor :spans
20
21
 
22
+ # @param configuration [Configuration]
23
+ # @param integration_meta [Hash, nil]
24
+ # @param message [String, nil]
21
25
  def initialize(configuration:, integration_meta: nil, message: nil)
22
26
  super
23
27
  @type = TYPE
24
28
  end
25
29
 
30
+ # Sets the event's start_timestamp.
31
+ # @param time [Time, Float]
32
+ # @return [void]
26
33
  def start_timestamp=(time)
27
34
  @start_timestamp = time.is_a?(Time) ? time.to_f : time
28
35
  end
29
36
 
37
+ # @return [Hash]
30
38
  def to_hash
31
39
  data = super
32
40
  data[:spans] = @spans.map(&:to_hash) if @spans
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class Transport
3
5
  class Configuration
4
- attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :http_adapter, :faraday_builder,
5
- :encoding
6
+ attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding
6
7
  attr_reader :transport_class
7
8
 
8
9
  def initialize
@@ -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,5 +1,7 @@
1
- require 'faraday'
2
- require 'zlib'
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zlib"
3
5
 
4
6
  module Sentry
5
7
  class HTTPTransport < Transport
@@ -10,14 +12,13 @@ module Sentry
10
12
  DEFAULT_DELAY = 60
11
13
  RETRY_AFTER_HEADER = "retry-after"
12
14
  RATE_LIMIT_HEADER = "x-sentry-rate-limits"
13
-
14
- attr_reader :conn, :adapter
15
+ USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
15
16
 
16
17
  def initialize(*args)
17
18
  super
18
- @adapter = @transport_configuration.http_adapter || Faraday.default_adapter
19
- @conn = set_conn
20
19
  @endpoint = @dsn.envelope_endpoint
20
+
21
+ log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
21
22
  end
22
23
 
23
24
  def send_data(data)
@@ -28,29 +29,37 @@ module Sentry
28
29
  encoding = GZIP_ENCODING
29
30
  end
30
31
 
31
- response = conn.post @endpoint do |req|
32
- req.headers['Content-Type'] = CONTENT_TYPE
33
- req.headers['Content-Encoding'] = encoding
34
- req.headers['X-Sentry-Auth'] = generate_auth_header
35
- req.body = data
32
+ headers = {
33
+ 'Content-Type' => CONTENT_TYPE,
34
+ 'Content-Encoding' => encoding,
35
+ 'X-Sentry-Auth' => generate_auth_header,
36
+ 'User-Agent' => USER_AGENT
37
+ }
38
+
39
+ response = conn.start do |http|
40
+ request = ::Net::HTTP::Post.new(@endpoint, headers)
41
+ request.body = data
42
+ http.request(request)
36
43
  end
37
44
 
38
- if has_rate_limited_header?(response.headers)
39
- handle_rate_limited_response(response.headers)
40
- end
41
- rescue Faraday::Error => e
42
- error_info = e.message
45
+ if response.code.match?(/\A2\d{2}/)
46
+ if has_rate_limited_header?(response)
47
+ handle_rate_limited_response(response)
48
+ end
49
+ else
50
+ error_info = "the server responded with status #{response.code}"
43
51
 
44
- if e.response
45
- if e.response[:status] == 429
46
- handle_rate_limited_response(e.response[:headers])
52
+ if response.code == "429"
53
+ handle_rate_limited_response(response)
47
54
  else
48
- error_info += "\nbody: #{e.response[:body]}"
49
- error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error']
55
+ error_info += "\nbody: #{response.body}"
56
+ error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
50
57
  end
51
- end
52
58
 
53
- raise Sentry::ExternalError, error_info
59
+ raise Sentry::ExternalError, error_info
60
+ end
61
+ rescue SocketError => e
62
+ raise Sentry::ExternalError.new(e.message)
54
63
  end
55
64
 
56
65
  private
@@ -117,32 +126,36 @@ module Sentry
117
126
  @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
118
127
  end
119
128
 
120
- def set_conn
121
- server = @dsn.server
129
+ def conn
130
+ server = URI(@dsn.server)
122
131
 
123
- log_debug("Sentry HTTP Transport connecting to #{server}")
132
+ connection =
133
+ if proxy = @transport_configuration.proxy
134
+ ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
135
+ else
136
+ ::Net::HTTP.new(server.hostname, server.port, nil)
137
+ end
124
138
 
125
- Faraday.new(server, :ssl => ssl_configuration, :proxy => @transport_configuration.proxy) do |builder|
126
- @transport_configuration.faraday_builder&.call(builder)
127
- builder.response :raise_error
128
- builder.options.merge! faraday_opts
129
- builder.headers[:user_agent] = "sentry-ruby/#{Sentry::VERSION}"
130
- builder.adapter(*adapter)
131
- end
132
- end
139
+ connection.use_ssl = server.scheme == "https"
140
+ connection.read_timeout = @transport_configuration.timeout
141
+ connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
142
+ connection.open_timeout = @transport_configuration.open_timeout
133
143
 
134
- # TODO: deprecate and replace where possible w/Faraday Builder
135
- def faraday_opts
136
- [:timeout, :open_timeout].each_with_object({}) do |opt, memo|
137
- memo[opt] = @transport_configuration.public_send(opt) if @transport_configuration.public_send(opt)
144
+ ssl_configuration.each do |key, value|
145
+ connection.send("#{key}=", value)
138
146
  end
147
+
148
+ connection
139
149
  end
140
150
 
141
151
  def ssl_configuration
142
- (@transport_configuration.ssl || {}).merge(
143
- :verify => @transport_configuration.ssl_verification,
144
- :ca_file => @transport_configuration.ssl_ca_file
145
- )
152
+ configuration = {
153
+ verify: @transport_configuration.ssl_verification,
154
+ ca_file: @transport_configuration.ssl_ca_file
155
+ }.merge(@transport_configuration.ssl || {})
156
+
157
+ configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
158
+ configuration
146
159
  end
147
160
  end
148
161
  end
@@ -1,22 +1,44 @@
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 :rate_limits, :discarded_events, :last_client_report_sent
27
+
28
+ # @deprecated Use Sentry.logger to retrieve the current logger instead.
29
+ attr_reader :logger
13
30
 
14
31
  def initialize(configuration)
15
- @configuration = configuration
16
32
  @logger = configuration.logger
17
33
  @transport_configuration = configuration.transport
18
34
  @dsn = configuration.dsn
19
35
  @rate_limits = {}
36
+ @send_client_reports = configuration.send_client_reports
37
+
38
+ if @send_client_reports
39
+ @discarded_events = Hash.new(0)
40
+ @last_client_report_sent = Time.now
41
+ end
20
42
  end
21
43
 
22
44
  def send_data(data, options = {})
@@ -27,14 +49,9 @@ module Sentry
27
49
  event_hash = event.to_hash
28
50
  item_type = get_item_type(event_hash)
29
51
 
30
- unless configuration.sending_allowed?
31
- log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}")
32
-
33
- return
34
- end
35
-
36
52
  if is_rate_limited?(item_type)
37
53
  log_info("Envelope [#{item_type}] not sent: rate limiting")
54
+ record_lost_event(:ratelimit_backoff, item_type)
38
55
 
39
56
  return
40
57
  end
@@ -91,20 +108,38 @@ module Sentry
91
108
 
92
109
  def encode(event)
93
110
  # Convert to hash
94
- event_hash = event.to_hash
111
+ event_payload = event.to_hash
112
+ event_id = event_payload[:event_id] || event_payload["event_id"]
113
+ item_type = get_item_type(event_payload)
114
+
115
+ envelope = Envelope.new(
116
+ {
117
+ event_id: event_id,
118
+ dsn: @dsn.to_s,
119
+ sdk: Sentry.sdk_meta,
120
+ sent_at: Sentry.utc_now.iso8601
121
+ }
122
+ )
123
+
124
+ envelope.add_item(
125
+ { type: item_type, content_type: 'application/json' },
126
+ event_payload
127
+ )
128
+
129
+ client_report_headers, client_report_payload = fetch_pending_client_report
130
+ envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
95
131
 
96
- event_id = event_hash[:event_id] || event_hash["event_id"]
97
- item_type = get_item_type(event_hash)
132
+ log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
98
133
 
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
134
+ envelope.to_s
135
+ end
104
136
 
105
- log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
137
+ def record_lost_event(reason, item_type)
138
+ return unless @send_client_reports
139
+ return unless CLIENT_REPORT_REASONS.include?(reason)
106
140
 
107
- envelope
141
+ item_type ||= 'event'
142
+ @discarded_events[[reason, item_type]] += 1
108
143
  end
109
144
 
110
145
  private
@@ -112,6 +147,32 @@ module Sentry
112
147
  def get_item_type(event_hash)
113
148
  event_hash[:type] || event_hash["type"] || "event"
114
149
  end
150
+
151
+ def fetch_pending_client_report
152
+ return nil unless @send_client_reports
153
+ return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
154
+ return nil if @discarded_events.empty?
155
+
156
+ discarded_events_hash = @discarded_events.map do |key, val|
157
+ reason, type = key
158
+
159
+ # 'event' has to be mapped to 'error'
160
+ category = type == 'transaction' ? 'transaction' : 'error'
161
+
162
+ { reason: reason, category: category, quantity: val }
163
+ end
164
+
165
+ item_header = { type: 'client_report' }
166
+ item_payload = {
167
+ timestamp: Sentry.utc_now.iso8601,
168
+ discarded_events: discarded_events_hash
169
+ }
170
+
171
+ @discarded_events = Hash.new(0)
172
+ @last_client_report_sent = Time.now
173
+
174
+ [item_header, item_payload]
175
+ end
115
176
  end
116
177
  end
117
178
 
@@ -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,24 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module LoggingHelper
3
5
  def log_error(message, exception, debug: false)
4
6
  message = "#{message}: #{exception.message}"
5
7
  message += "\n#{exception.backtrace.join("\n")}" if debug
6
8
 
7
- logger.error(LOGGER_PROGNAME) do
9
+ @logger.error(LOGGER_PROGNAME) do
8
10
  message
9
11
  end
10
12
  end
11
13
 
12
14
  def log_info(message)
13
- logger.info(LOGGER_PROGNAME) { message }
15
+ @logger.info(LOGGER_PROGNAME) { message }
14
16
  end
15
17
 
16
18
  def log_debug(message)
17
- logger.debug(LOGGER_PROGNAME) { message }
19
+ @logger.debug(LOGGER_PROGNAME) { message }
18
20
  end
19
21
 
20
22
  def log_warn(message)
21
- logger.warn(LOGGER_PROGNAME) { message }
23
+ @logger.warn(LOGGER_PROGNAME) { message }
22
24
  end
23
25
  end
24
26
  end
@@ -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.2"
4
+ VERSION = "5.0.2"
3
5
  end