sentry-ruby-core 4.4.0 → 5.1.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile +9 -5
  5. data/LICENSE.txt +1 -1
  6. data/README.md +29 -175
  7. data/bin/console +5 -1
  8. data/lib/sentry/background_worker.rb +33 -3
  9. data/lib/sentry/backtrace.rb +1 -3
  10. data/lib/sentry/breadcrumb/sentry_logger.rb +3 -1
  11. data/lib/sentry/breadcrumb.rb +28 -2
  12. data/lib/sentry/breadcrumb_buffer.rb +16 -0
  13. data/lib/sentry/client.rb +66 -7
  14. data/lib/sentry/configuration.rb +156 -112
  15. data/lib/sentry/core_ext/object/deep_dup.rb +4 -0
  16. data/lib/sentry/core_ext/object/duplicable.rb +2 -0
  17. data/lib/sentry/dsn.rb +6 -1
  18. data/lib/sentry/envelope.rb +49 -0
  19. data/lib/sentry/event.rb +65 -23
  20. data/lib/sentry/exceptions.rb +2 -0
  21. data/lib/sentry/hub.rb +37 -6
  22. data/lib/sentry/integrable.rb +2 -0
  23. data/lib/sentry/interface.rb +3 -10
  24. data/lib/sentry/interfaces/exception.rb +13 -3
  25. data/lib/sentry/interfaces/request.rb +52 -21
  26. data/lib/sentry/interfaces/single_exception.rb +31 -0
  27. data/lib/sentry/interfaces/stacktrace.rb +14 -0
  28. data/lib/sentry/interfaces/stacktrace_builder.rb +39 -10
  29. data/lib/sentry/interfaces/threads.rb +12 -2
  30. data/lib/sentry/linecache.rb +3 -0
  31. data/lib/sentry/net/http.rb +79 -51
  32. data/lib/sentry/rack/capture_exceptions.rb +2 -0
  33. data/lib/sentry/rack.rb +2 -1
  34. data/lib/sentry/rake.rb +33 -9
  35. data/lib/sentry/redis.rb +88 -0
  36. data/lib/sentry/release_detector.rb +39 -0
  37. data/lib/sentry/scope.rb +76 -6
  38. data/lib/sentry/span.rb +84 -8
  39. data/lib/sentry/transaction.rb +50 -13
  40. data/lib/sentry/transaction_event.rb +19 -6
  41. data/lib/sentry/transport/configuration.rb +4 -2
  42. data/lib/sentry/transport/dummy_transport.rb +2 -0
  43. data/lib/sentry/transport/http_transport.rb +55 -42
  44. data/lib/sentry/transport.rb +101 -32
  45. data/lib/sentry/utils/argument_checking_helper.rb +2 -0
  46. data/lib/sentry/utils/custom_inspection.rb +14 -0
  47. data/lib/sentry/utils/exception_cause_chain.rb +10 -10
  48. data/lib/sentry/utils/logging_helper.rb +6 -4
  49. data/lib/sentry/utils/real_ip.rb +9 -1
  50. data/lib/sentry/utils/request_id.rb +2 -0
  51. data/lib/sentry/version.rb +3 -1
  52. data/lib/sentry-ruby.rb +247 -47
  53. data/sentry-ruby-core.gemspec +2 -3
  54. data/sentry-ruby.gemspec +2 -3
  55. metadata +10 -22
  56. data/.craft.yml +0 -29
  57. data/lib/sentry/benchmarks/benchmark_transport.rb +0 -14
  58. data/lib/sentry/rack/deprecations.rb +0 -19
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
@@ -17,9 +18,49 @@ module Sentry
17
18
  504 => "deadline_exceeded"
18
19
  }
19
20
 
20
-
21
- attr_reader :trace_id, :span_id, :parent_span_id, :sampled, :start_timestamp, :timestamp, :description, :op, :status, :tags, :data
22
- attr_accessor :span_recorder, :transaction
21
+ # An uuid that can be used to identify a trace.
22
+ # @return [String]
23
+ attr_reader :trace_id
24
+ # An uuid that can be used to identify the span.
25
+ # @return [String]
26
+ attr_reader :span_id
27
+ # Span parent's span_id.
28
+ # @return [String]
29
+ attr_reader :parent_span_id
30
+ # Sampling result of the span.
31
+ # @return [Boolean, nil]
32
+ attr_reader :sampled
33
+ # Starting timestamp of the span.
34
+ # @return [Float]
35
+ attr_reader :start_timestamp
36
+ # Finishing timestamp of the span.
37
+ # @return [Float]
38
+ attr_reader :timestamp
39
+ # Span description
40
+ # @return [String]
41
+ attr_reader :description
42
+ # Span operation
43
+ # @return [String]
44
+ attr_reader :op
45
+ # Span status
46
+ # @return [String]
47
+ attr_reader :status
48
+ # Span tags
49
+ # @return [Hash]
50
+ attr_reader :tags
51
+ # Span data
52
+ # @return [Hash]
53
+ attr_reader :data
54
+
55
+ # The SpanRecorder the current span belongs to.
56
+ # SpanRecorder holds all spans under the same Transaction object (including the Transaction itself).
57
+ # @return [SpanRecorder]
58
+ attr_accessor :span_recorder
59
+
60
+ # The Transaction object the Span belongs to.
61
+ # Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction.
62
+ # @return [Transaction]
63
+ attr_accessor :transaction
23
64
 
24
65
  def initialize(
25
66
  description: nil,
@@ -44,6 +85,8 @@ module Sentry
44
85
  @tags = {}
45
86
  end
46
87
 
88
+ # Finishes the span by adding a timestamp.
89
+ # @return [self]
47
90
  def finish
48
91
  # already finished
49
92
  return if @timestamp
@@ -52,6 +95,8 @@ module Sentry
52
95
  self
53
96
  end
54
97
 
98
+ # Generates a trace string that can be used to connect other transactions.
99
+ # @return [String]
55
100
  def to_sentry_trace
56
101
  sampled_flag = ""
57
102
  sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
@@ -59,6 +104,7 @@ module Sentry
59
104
  "#{@trace_id}-#{@span_id}-#{sampled_flag}"
60
105
  end
61
106
 
107
+ # @return [Hash]
62
108
  def to_hash
63
109
  {
64
110
  trace_id: @trace_id,
@@ -74,6 +120,8 @@ module Sentry
74
120
  }
75
121
  end
76
122
 
123
+ # Returns the span's context that can be used to embed in an Event.
124
+ # @return [Hash]
77
125
  def get_trace_context
78
126
  {
79
127
  trace_id: @trace_id,
@@ -85,9 +133,11 @@ module Sentry
85
133
  }
86
134
  end
87
135
 
88
- def start_child(**options)
89
- options = options.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
90
- new_span = Span.new(**options)
136
+ # Starts a child span with given attributes.
137
+ # @param attributes [Hash] the attributes for the child span.
138
+ def start_child(**attributes)
139
+ attributes = attributes.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
140
+ new_span = Span.new(**attributes)
91
141
  new_span.transaction = transaction
92
142
  new_span.span_recorder = span_recorder
93
143
 
@@ -98,8 +148,17 @@ module Sentry
98
148
  new_span
99
149
  end
100
150
 
101
- def with_child_span(**options, &block)
102
- child_span = start_child(**options)
151
+ # Starts a child span, yield it to the given block, and then finish the span after the block is executed.
152
+ # @example
153
+ # span.with_child_span do |child_span|
154
+ # # things happen here will be recorded in a child span
155
+ # end
156
+ #
157
+ # @param attributes [Hash] the attributes for the child span.
158
+ # @param block [Proc] the action to be recorded in the child span.
159
+ # @yieldparam child_span [Span]
160
+ def with_child_span(**attributes, &block)
161
+ child_span = start_child(**attributes)
103
162
 
104
163
  yield(child_span)
105
164
 
@@ -110,22 +169,33 @@ module Sentry
110
169
  dup
111
170
  end
112
171
 
172
+ # Sets the span's operation.
173
+ # @param op [String] operation of the span.
113
174
  def set_op(op)
114
175
  @op = op
115
176
  end
116
177
 
178
+ # Sets the span's description.
179
+ # @param description [String] description of the span.
117
180
  def set_description(description)
118
181
  @description = description
119
182
  end
120
183
 
184
+
185
+ # Sets the span's status.
186
+ # @param satus [String] status of the span.
121
187
  def set_status(status)
122
188
  @status = status
123
189
  end
124
190
 
191
+ # Sets the span's finish timestamp.
192
+ # @param timestamp [Float] finished time in float format (most precise).
125
193
  def set_timestamp(timestamp)
126
194
  @timestamp = timestamp
127
195
  end
128
196
 
197
+ # Sets the span's status with given http status code.
198
+ # @param status_code [String] example: "500".
129
199
  def set_http_status(status_code)
130
200
  status_code = status_code.to_i
131
201
  set_data("status_code", status_code)
@@ -139,10 +209,16 @@ module Sentry
139
209
  set_status(status)
140
210
  end
141
211
 
212
+ # Inserts a key-value pair to the span's data payload.
213
+ # @param key [String, Symbol]
214
+ # @param value [Object]
142
215
  def set_data(key, value)
143
216
  @data[key] = value
144
217
  end
145
218
 
219
+ # Sets a tag to the span.
220
+ # @param key [String, Symbol]
221
+ # @param value [String]
146
222
  def set_tag(key, value)
147
223
  @tags[key] = value
148
224
  end
@@ -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,10 +164,12 @@ module Sentry
129
164
  @name = UNLABELD_NAME
130
165
  end
131
166
 
132
- return unless @sampled || @parent_sampled
133
-
134
- event = hub.current_client.event_from_transaction(self)
135
- hub.capture_event(event)
167
+ if @sampled
168
+ event = hub.current_client.event_from_transaction(self)
169
+ hub.capture_event(event)
170
+ else
171
+ hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
172
+ end
136
173
  end
137
174
 
138
175
  protected
@@ -4,24 +4,37 @@ module Sentry
4
4
  class TransactionEvent < Event
5
5
  TYPE = "transaction"
6
6
 
7
- ATTRIBUTES = %i(
7
+ SERIALIZEABLE_ATTRIBUTES = %i(
8
8
  event_id level timestamp start_timestamp
9
9
  release environment server_name modules
10
10
  user tags contexts extra
11
11
  transaction platform sdk type
12
12
  )
13
13
 
14
- attr_accessor(*ATTRIBUTES)
14
+ WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp start_timestamp level)
15
+
16
+ attr_writer(*WRITER_ATTRIBUTES)
17
+ attr_reader(*SERIALIZEABLE_ATTRIBUTES)
18
+
19
+ # @return [<Array[Span]>]
15
20
  attr_accessor :spans
16
21
 
17
- def start_timestamp=(time)
18
- @start_timestamp = time.is_a?(Time) ? time.to_f : time
22
+ # @param configuration [Configuration]
23
+ # @param integration_meta [Hash, nil]
24
+ # @param message [String, nil]
25
+ def initialize(configuration:, integration_meta: nil, message: nil)
26
+ super
27
+ @type = TYPE
19
28
  end
20
29
 
21
- def type
22
- TYPE
30
+ # Sets the event's start_timestamp.
31
+ # @param time [Time, Float]
32
+ # @return [void]
33
+ def start_timestamp=(time)
34
+ @start_timestamp = time.is_a?(Time) ? time.to_f : time
23
35
  end
24
36
 
37
+ # @return [Hash]
25
38
  def to_hash
26
39
  data = super
27
40
  data[:spans] = @spans.map(&:to_hash) if @spans
@@ -1,8 +1,10 @@
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
- :transport_class, :encoding
6
+ attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding
7
+ attr_reader :transport_class
6
8
 
7
9
  def initialize
8
10
  @ssl_verification = true
@@ -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
- PROTOCOL_VERSION = '5'
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 = {})
@@ -24,28 +46,19 @@ module Sentry
24
46
  end
25
47
 
26
48
  def send_event(event)
27
- event_hash = event.to_hash
28
- item_type = get_item_type(event_hash)
29
-
30
- unless configuration.sending_allowed?
31
- log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}")
49
+ envelope = envelope_from_event(event)
50
+ send_envelope(envelope)
32
51
 
33
- return
34
- end
35
-
36
- if is_rate_limited?(item_type)
37
- log_info("Envelope [#{item_type}] not sent: rate limiting")
38
-
39
- return
40
- end
41
-
42
- encoded_data = encode(event)
52
+ event
53
+ end
43
54
 
44
- return nil unless encoded_data
55
+ def send_envelope(envelope)
56
+ reject_rate_limited_items(envelope)
45
57
 
46
- send_data(encoded_data)
58
+ return if envelope.items.empty?
47
59
 
48
- event
60
+ log_info("[Transport] Sending envelope with items [#{envelope.item_types.join(', ')}] #{envelope.event_id} to Sentry")
61
+ send_data(envelope.to_s)
49
62
  end
50
63
 
51
64
  def is_rate_limited?(item_type)
@@ -89,29 +102,85 @@ module Sentry
89
102
  'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
90
103
  end
91
104
 
92
- def encode(event)
105
+ def envelope_from_event(event)
93
106
  # Convert to hash
94
- event_hash = event.to_hash
107
+ event_payload = event.to_hash
108
+ event_id = event_payload[:event_id] || event_payload["event_id"]
109
+ item_type = get_item_type(event_payload)
95
110
 
96
- event_id = event_hash[:event_id] || event_hash["event_id"]
97
- item_type = get_item_type(event_hash)
111
+ envelope = Envelope.new(
112
+ {
113
+ event_id: event_id,
114
+ dsn: @dsn.to_s,
115
+ sdk: Sentry.sdk_meta,
116
+ sent_at: Sentry.utc_now.iso8601
117
+ }
118
+ )
98
119
 
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
120
+ envelope.add_item(
121
+ { type: item_type, content_type: 'application/json' },
122
+ event_payload
123
+ )
124
+
125
+ client_report_headers, client_report_payload = fetch_pending_client_report
126
+ envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
104
127
 
105
- log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
106
128
 
107
129
  envelope
108
130
  end
109
131
 
132
+ def record_lost_event(reason, item_type)
133
+ return unless @send_client_reports
134
+ return unless CLIENT_REPORT_REASONS.include?(reason)
135
+
136
+ item_type ||= 'event'
137
+ @discarded_events[[reason, item_type]] += 1
138
+ end
139
+
110
140
  private
111
141
 
112
142
  def get_item_type(event_hash)
113
143
  event_hash[:type] || event_hash["type"] || "event"
114
144
  end
145
+
146
+ def fetch_pending_client_report
147
+ return nil unless @send_client_reports
148
+ return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
149
+ return nil if @discarded_events.empty?
150
+
151
+ discarded_events_hash = @discarded_events.map do |key, val|
152
+ reason, type = key
153
+
154
+ # 'event' has to be mapped to 'error'
155
+ category = type == 'transaction' ? 'transaction' : 'error'
156
+
157
+ { reason: reason, category: category, quantity: val }
158
+ end
159
+
160
+ item_header = { type: 'client_report' }
161
+ item_payload = {
162
+ timestamp: Sentry.utc_now.iso8601,
163
+ discarded_events: discarded_events_hash
164
+ }
165
+
166
+ @discarded_events = Hash.new(0)
167
+ @last_client_report_sent = Time.now
168
+
169
+ [item_header, item_payload]
170
+ end
171
+
172
+ def reject_rate_limited_items(envelope)
173
+ envelope.items.reject! do |item|
174
+ if is_rate_limited?(item.type)
175
+ log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
176
+ record_lost_event(:ratelimit_backoff, item.type)
177
+
178
+ true
179
+ else
180
+ false
181
+ end
182
+ end
183
+ end
115
184
  end
116
185
  end
117
186