sentry-ruby-core 6.6.2 → 7.0.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/test +15 -0
  3. data/gemfiles/ruby-2.7_rack-2_redis-4.gemfile.lock +159 -0
  4. data/gemfiles/ruby-2.7_rack-3.1_redis-4.gemfile.lock +159 -0
  5. data/gemfiles/ruby-2.7_rack-3_redis-4.gemfile.lock +159 -0
  6. data/gemfiles/ruby-3.0_rack-2_redis-4.gemfile.lock +173 -0
  7. data/gemfiles/ruby-3.0_rack-3.1_redis-4.gemfile.lock +173 -0
  8. data/gemfiles/ruby-3.0_rack-3_redis-4.gemfile.lock +173 -0
  9. data/gemfiles/ruby-3.1_rack-2_redis-4.gemfile.lock +241 -0
  10. data/gemfiles/ruby-3.1_rack-3.1_redis-4.gemfile.lock +241 -0
  11. data/gemfiles/ruby-3.1_rack-3_redis-4.gemfile.lock +241 -0
  12. data/gemfiles/ruby-3.2_rack-0_redis-5.gemfile.lock +263 -0
  13. data/gemfiles/ruby-3.2_rack-2_redis-4.gemfile.lock +260 -0
  14. data/gemfiles/ruby-3.2_rack-2_redis-5.gemfile.lock +266 -0
  15. data/gemfiles/ruby-3.2_rack-3.1_redis-4.gemfile.lock +260 -0
  16. data/gemfiles/ruby-3.2_rack-3_redis-4.gemfile.lock +260 -0
  17. data/gemfiles/ruby-3.2_rack-3_redis-5.gemfile.lock +266 -0
  18. data/gemfiles/ruby-3.3_rack-2_redis-4.gemfile.lock +263 -0
  19. data/gemfiles/ruby-3.3_rack-3.1_redis-4.gemfile.lock +263 -0
  20. data/gemfiles/ruby-3.3_rack-3.1_redis-5.3.gemfile.lock +269 -0
  21. data/gemfiles/ruby-3.3_rack-3_redis-4.gemfile.lock +263 -0
  22. data/gemfiles/ruby-3.4_rack-2_redis-4.gemfile.lock +254 -0
  23. data/gemfiles/ruby-3.4_rack-3.1_redis-4.gemfile.lock +254 -0
  24. data/gemfiles/ruby-3.4_rack-3.1_redis-5.3.gemfile.lock +260 -0
  25. data/gemfiles/ruby-3.4_rack-3_redis-4.gemfile.lock +254 -0
  26. data/gemfiles/ruby-4.0_rack-2_redis-4.gemfile.lock +254 -0
  27. data/gemfiles/ruby-4.0_rack-3.1_redis-4.gemfile.lock +254 -0
  28. data/gemfiles/ruby-4.0_rack-3_redis-4.gemfile.lock +254 -0
  29. data/gemfiles/ruby-jruby-9.4.14.0_rack-2_redis-4.gemfile.lock +266 -0
  30. data/gemfiles/ruby-jruby-9.4.14.0_rack-3.1_redis-4.gemfile.lock +266 -0
  31. data/gemfiles/ruby-jruby-9.4.14.0_rack-3_redis-4.gemfile.lock +266 -0
  32. data/lib/sentry/client.rb +9 -8
  33. data/lib/sentry/configuration.rb +74 -12
  34. data/lib/sentry/data_collection/key_value_collection.rb +136 -0
  35. data/lib/sentry/data_collection.rb +203 -0
  36. data/lib/sentry/debug_structured_logger.rb +2 -15
  37. data/lib/sentry/envelope/item.rb +21 -0
  38. data/lib/sentry/error_event.rb +6 -1
  39. data/lib/sentry/event.rb +8 -4
  40. data/lib/sentry/excon/middleware.rb +3 -8
  41. data/lib/sentry/faraday.rb +3 -4
  42. data/lib/sentry/hub.rb +2 -2
  43. data/lib/sentry/interfaces/exception.rb +8 -2
  44. data/lib/sentry/interfaces/request.rb +42 -46
  45. data/lib/sentry/interfaces/single_exception.rb +2 -2
  46. data/lib/sentry/net/http.rb +3 -4
  47. data/lib/sentry/redis.rb +1 -1
  48. data/lib/sentry/scope.rb +42 -3
  49. data/lib/sentry/span.rb +1 -0
  50. data/lib/sentry/std_lib_logger.rb +2 -6
  51. data/lib/sentry/telemetry_event_buffer.rb +9 -2
  52. data/lib/sentry/test_helper.rb +8 -7
  53. data/lib/sentry/threaded_periodic_worker.rb +8 -3
  54. data/lib/sentry/transport.rb +14 -3
  55. data/lib/sentry/utils/http_tracing.rb +42 -26
  56. data/lib/sentry/utils/telemetry_attributes.rb +26 -14
  57. data/lib/sentry/version.rb +1 -1
  58. data/lib/sentry-ruby.rb +94 -23
  59. data/sentry-ruby-core.gemspec +1 -1
  60. data/sentry-ruby.gemspec +1 -1
  61. metadata +35 -3
data/lib/sentry/hub.rb CHANGED
@@ -224,7 +224,7 @@ module Sentry
224
224
  end
225
225
 
226
226
  def capture_log_event(message, **options)
227
- return unless current_client && current_client.configuration.enable_logs
227
+ return unless current_client
228
228
 
229
229
  event = current_client.event_from_log(message, **options)
230
230
 
@@ -242,7 +242,7 @@ module Sentry
242
242
  # @param attributes [Hash, nil] (optional) additional attributes for the metric
243
243
  # @return [void]
244
244
  def capture_metric(name:, type:, value:, unit: nil, attributes: nil)
245
- return unless current_client&.configuration.enable_metrics
245
+ return unless current_client
246
246
 
247
247
  metric = MetricEvent.new(
248
248
  name: name,
@@ -25,15 +25,21 @@ module Sentry
25
25
  # @see SingleExceptionInterface#build_with_stacktrace
26
26
  # @see SingleExceptionInterface#initialize
27
27
  # @param mechanism [Mechanism]
28
+ # @param data_collection [DataCollection]
28
29
  # @return [ExceptionInterface]
29
- def self.build(exception:, stacktrace_builder:, mechanism:)
30
+ def self.build(exception:, stacktrace_builder:, mechanism:, data_collection:)
30
31
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
31
32
  processed_backtrace_ids = Set.new
32
33
 
33
34
  exceptions = exceptions.map do |e|
34
35
  if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
35
36
  processed_backtrace_ids << e.backtrace.object_id
36
- SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism)
37
+ SingleExceptionInterface.build_with_stacktrace(
38
+ exception: e,
39
+ stacktrace_builder: stacktrace_builder,
40
+ mechanism: mechanism,
41
+ data_collection: data_collection
42
+ )
37
43
  else
38
44
  SingleExceptionInterface.new(exception: exception, mechanism: mechanism)
39
45
  end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Sentry
4
6
  class RequestInterface < Interface
5
7
  REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
6
8
  CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
7
- IP_HEADERS = [
8
- "REMOTE_ADDR",
9
- "HTTP_CLIENT_IP",
10
- "HTTP_X_REAL_IP",
11
- "HTTP_X_FORWARDED_FOR"
12
- ].freeze
13
9
 
14
10
  # Regex to detect lowercase chars — match? is allocation-free (no MatchData/String)
15
11
  LOWERCASE_PATTERN = /[a-z]/.freeze
@@ -27,10 +23,10 @@ module Sentry
27
23
  # @return [Hash]
28
24
  attr_accessor :data
29
25
 
30
- # @return [String]
26
+ # @return [String, Hash]
31
27
  attr_accessor :query_string
32
28
 
33
- # @return [String]
29
+ # @return [Hash]
34
30
  attr_accessor :cookies
35
31
 
36
32
  # @return [Hash]
@@ -40,33 +36,22 @@ module Sentry
40
36
  attr_accessor :env
41
37
 
42
38
  # @param env [Hash]
43
- # @param send_default_pii [Boolean]
39
+ # @param data_collection [DataCollection]
44
40
  # @param rack_env_whitelist [Array]
45
- # @see Configuration#send_default_pii
41
+ # @see Configuration#data_collection
46
42
  # @see Configuration#rack_env_whitelist
47
- def initialize(env:, send_default_pii:, rack_env_whitelist:)
43
+ def initialize(env:, data_collection:, rack_env_whitelist:)
48
44
  env = env.dup
49
-
50
- unless send_default_pii
51
- # need to completely wipe out ip addresses
52
- RequestInterface::IP_HEADERS.each do |header|
53
- env.delete(header)
54
- end
55
- end
56
-
57
45
  request = ::Rack::Request.new(env)
58
-
59
- if send_default_pii
60
- self.data = read_data_from(request)
61
- self.cookies = request.cookies
62
- self.query_string = request.query_string
63
- end
64
-
65
- self.url = request.scheme && request.url.split("?").first
66
- self.method = request.request_method
67
-
68
- self.headers = filter_and_format_headers(env, send_default_pii)
69
- self.env = filter_and_format_env(env, rack_env_whitelist)
46
+ query = data_collection.url_query_params.filter(request.GET) rescue nil
47
+
48
+ self.method = request.request_method
49
+ self.url = request.scheme && request.url.split("?").first
50
+ self.query_string = query unless query&.empty?
51
+ self.cookies = data_collection.cookies.filter(request.cookies, cookie: true)
52
+ self.data = read_data_from(request) if data_collection.collect_incoming_http_body?
53
+ self.headers = filter_and_format_headers(env, data_collection.http_headers.request)
54
+ self.env = filter_and_format_env(env, data_collection.http_headers.request, rack_env_whitelist)
70
55
  end
71
56
 
72
57
  private
@@ -75,25 +60,31 @@ module Sentry
75
60
  return "Skipped non-rewindable request body" unless request.body.respond_to?(:rewind)
76
61
 
77
62
  if request.form_data?
78
- request.POST
79
- elsif request.body # JSON requests, etc
80
- data = request.body.read(MAX_BODY_LIMIT)
81
- data = Utils::EncodingHelper.encode_to_utf_8(data.to_s)
82
- request.body.rewind
83
- data
63
+ DataCollection.filter(request.POST)
64
+ else
65
+ body = request.body.read(MAX_BODY_LIMIT)
66
+ body = Utils::EncodingHelper.encode_to_utf_8(body.to_s)
67
+
68
+ if request.media_type == "application/json" || request.media_type&.end_with?("+json")
69
+ parsed_body = JSON.parse(body)
70
+ parsed_body.is_a?(Hash) ? DataCollection.filter(parsed_body) : parsed_body
71
+ else
72
+ body
73
+ end
84
74
  end
85
- rescue IOError => e
75
+ rescue JSON::ParserError, IOError => e
86
76
  e.message
77
+ ensure
78
+ request.body.rewind if request.body.respond_to?(:rewind)
87
79
  end
88
80
 
89
- def filter_and_format_headers(env, send_default_pii)
81
+ def filter_and_format_headers(env, collection)
90
82
  env.each_with_object({}) do |(key, value), memo|
91
83
  begin
92
84
  key = key.to_s # rack env can contain symbols
93
85
  next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
94
86
  next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
95
87
  next if is_skippable_header?(key)
96
- next if key == "HTTP_AUTHORIZATION" && !send_default_pii
97
88
 
98
89
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
99
90
  key = key.delete_prefix("HTTP_")
@@ -107,12 +98,13 @@ module Sentry
107
98
  Sentry.sdk_logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
108
99
  next
109
100
  end
101
+ end.then do |e|
102
+ collection.filter(e)
110
103
  end
111
104
  end
112
105
 
113
106
  def is_skippable_header?(key)
114
107
  key.match?(LOWERCASE_PATTERN) || # lower-case envs aren't real http headers
115
- key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
116
108
  !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
117
109
  end
118
110
 
@@ -134,11 +126,15 @@ module Sentry
134
126
  Gem::Version.new(::Rack.release) >= Gem::Version.new("3.0")
135
127
  end
136
128
 
137
- def filter_and_format_env(env, rack_env_whitelist)
138
- return env if rack_env_whitelist.empty?
139
-
140
- env.select do |k, _v|
141
- rack_env_whitelist.include? k.to_s
129
+ def filter_and_format_env(env, collection, rack_env_whitelist)
130
+ if rack_env_whitelist.empty?
131
+ env
132
+ else
133
+ env.select do |k, _v|
134
+ rack_env_whitelist.include? k.to_s
135
+ end
136
+ end.then do |e|
137
+ collection.filter(e)
142
138
  end
143
139
  end
144
140
  end
@@ -41,7 +41,7 @@ module Sentry
41
41
 
42
42
  # patch this method if you want to change an exception's stacktrace frames
43
43
  # also see `StacktraceBuilder.build`.
44
- def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:)
44
+ def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:, data_collection:)
45
45
  stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
46
46
 
47
47
  if locals = exception.instance_variable_get(:@sentry_locals)
@@ -60,7 +60,7 @@ module Sentry
60
60
  end
61
61
  end
62
62
 
63
- stacktrace.frames.last&.vars = locals
63
+ stacktrace.frames.last&.vars = data_collection.stack_frame_variables.filter(locals)
64
64
  end
65
65
 
66
66
  new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
@@ -72,10 +72,9 @@ module Sentry
72
72
 
73
73
  result = { method: req.method, url: url }
74
74
 
75
- if Sentry.configuration.send_default_pii
76
- result[:query] = uri.query
77
- result[:body] = req.body
78
- end
75
+ query = filter_query_params(uri.query)
76
+ result[:query] = query if query
77
+ result[:body] = req.body if Sentry.configuration.data_collection.collect_outgoing_http_body?
79
78
 
80
79
  result
81
80
  end
data/lib/sentry/redis.rb CHANGED
@@ -62,7 +62,7 @@ module Sentry
62
62
  command_set = { command: command.to_s.upcase }
63
63
  command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key)
64
64
 
65
- if Sentry.configuration.send_default_pii
65
+ if Sentry.configuration.data_collection.database_query_data
66
66
  command_set[:arguments] = arguments
67
67
  .select { |a| Utils::EncodingHelper.valid_utf_8?(a) }
68
68
  .join(" ")
data/lib/sentry/scope.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require "sentry/breadcrumb_buffer"
4
5
  require "sentry/propagation_context"
5
6
  require "sentry/attachment"
@@ -24,7 +25,8 @@ module Sentry
24
25
  :span,
25
26
  :session,
26
27
  :attachments,
27
- :propagation_context
28
+ :propagation_context,
29
+ :attributes
28
30
  ]
29
31
 
30
32
  attr_reader(*ATTRIBUTES)
@@ -84,7 +86,15 @@ module Sentry
84
86
  # @param telemetry [MetricEvent, LogEvent] the telemetry event to apply scope context to
85
87
  # @return [MetricEvent, LogEvent] the telemetry event with scope context applied
86
88
  def apply_to_telemetry(telemetry)
87
- # TODO-neel when new scope set_attribute api is added: add them here
89
+ # Compare as strings since String and Symbol keys serialize to the same wire key.
90
+ existing_keys = telemetry.attributes.keys.map(&:to_s).to_set
91
+
92
+ attributes.each do |key, value|
93
+ next if existing_keys.include?(key)
94
+
95
+ telemetry.attributes[key] = value
96
+ end
97
+
88
98
  trace_context = get_trace_context
89
99
  telemetry.trace_id = trace_context[:trace_id]
90
100
  telemetry.span_id = trace_context[:span_id]
@@ -136,6 +146,7 @@ module Sentry
136
146
  copy.propagation_context = propagation_context.deep_dup
137
147
  copy.attachments = attachments.dup
138
148
  copy.event_processors = event_processors.dup
149
+ copy.attributes = attributes.deep_dup
139
150
  copy
140
151
  end
141
152
 
@@ -154,11 +165,12 @@ module Sentry
154
165
  self.span = scope.span
155
166
  self.propagation_context = scope.propagation_context
156
167
  self.attachments = scope.attachments
168
+ self.attributes = scope.attributes
157
169
  end
158
170
 
159
171
  # Updates the scope's data from the given options.
160
172
  # @param contexts [Hash]
161
- # @param extras [Hash]
173
+ # @param extra [Hash]
162
174
  # @param tags [Hash]
163
175
  # @param user [Hash]
164
176
  # @param level [String, Symbol]
@@ -256,6 +268,32 @@ module Sentry
256
268
  set_contexts(key => value)
257
269
  end
258
270
 
271
+ # Updates the scope's attributes by merging with the old value.
272
+ # @param attributes_hash [Hash]
273
+ # @return [Hash]
274
+ def set_attributes(attributes_hash)
275
+ check_argument_type!(attributes_hash, Hash)
276
+ attributes_hash.each { |key, value| @attributes[key.to_s] = value }
277
+ @attributes
278
+ end
279
+
280
+ # Sets a single attribute on the scope.
281
+ # @param key [String, Symbol]
282
+ # @param value [Object]
283
+ # @param unit [String, Symbol, nil] an optional measurement unit for the value
284
+ # @return [Hash]
285
+ def set_attribute(key, value, unit: nil)
286
+ value = { value: value, unit: unit } unless unit.nil?
287
+ set_attributes(key => value)
288
+ end
289
+
290
+ # Removes a single attribute from the scope. No-op if the attribute is not set.
291
+ # @param key [String, Symbol]
292
+ # @return [void]
293
+ def remove_attribute(key)
294
+ @attributes.delete(key.to_s)
295
+ end
296
+
259
297
  # Sets the scope's level attribute.
260
298
  # @param level [String, Symbol]
261
299
  # @return [void]
@@ -361,6 +399,7 @@ module Sentry
361
399
  @span = nil
362
400
  @session = nil
363
401
  @attachments = []
402
+ @attributes = {}
364
403
  generate_propagation_context
365
404
  set_new_breadcrumb_buffer
366
405
  end
data/lib/sentry/span.rb CHANGED
@@ -45,6 +45,7 @@ module Sentry
45
45
  FUNCTION = "code.function"
46
46
  NAMESPACE = "code.namespace"
47
47
 
48
+ MESSAGING_SYSTEM = "messaging.system"
48
49
  MESSAGING_MESSAGE_ID = "messaging.message.id"
49
50
  MESSAGING_DESTINATION_NAME = "messaging.destination.name"
50
51
  MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency"
@@ -50,10 +50,6 @@ module Sentry
50
50
  end
51
51
  end
52
52
 
53
- Sentry.register_patch(:logger) do |config|
54
- if config.enable_logs
55
- ::Logger.prepend(Sentry::StdLibLogger)
56
- else
57
- config.sdk_logger.warn(":logger patch enabled but `enable_logs` is turned off - skipping applying patch")
58
- end
53
+ Sentry.register_patch(:logger) do
54
+ ::Logger.prepend(Sentry::StdLibLogger)
59
55
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require "sentry/threaded_periodic_worker"
4
5
  require "sentry/envelope"
5
6
 
@@ -58,7 +59,11 @@ module Sentry
58
59
 
59
60
  if size >= @max_items_before_drop
60
61
  log_debug("[#{self.class}] exceeded max capacity, dropping event")
61
- @client.transport.record_lost_event(:queue_overflow, @data_category)
62
+ @client.transport.record_lost_event(
63
+ :queue_overflow,
64
+ @data_category,
65
+ num_bytes: JSON.generate(item.to_h).bytesize
66
+ )
62
67
  else
63
68
  @pending_items << item
64
69
  end
@@ -92,6 +97,7 @@ module Sentry
92
97
  )
93
98
 
94
99
  discarded_count = 0
100
+ discarded_bytes = 0
95
101
  envelope_items = []
96
102
 
97
103
  if @before_send
@@ -102,6 +108,7 @@ module Sentry
102
108
  envelope_items << processed_item.to_h
103
109
  else
104
110
  discarded_count += 1
111
+ discarded_bytes += JSON.generate(item.to_h).bytesize
105
112
  end
106
113
  end
107
114
  else
@@ -109,7 +116,7 @@ module Sentry
109
116
  end
110
117
 
111
118
  unless discarded_count.zero?
112
- @client.transport.record_lost_event(:before_send, @data_category, num: discarded_count)
119
+ @client.transport.record_lost_event(:before_send, @data_category, num: discarded_count, num_bytes: discarded_bytes)
113
120
  end
114
121
 
115
122
  return if envelope_items.empty?
@@ -33,7 +33,7 @@ module Sentry
33
33
  dummy_config.background_worker_threads = 0
34
34
 
35
35
  # user can overwrite some of the configs, with a few exceptions like:
36
- # - include_local_variables
36
+ # - data_collection
37
37
  # - auto_session_tracking
38
38
  block&.call(dummy_config)
39
39
 
@@ -52,10 +52,10 @@ module Sentry
52
52
  test_client = Sentry::Client.new(dummy_config.dup)
53
53
  main_hub.bind_client(test_client)
54
54
 
55
- # Realign the current thread's hub with the main hub so direct
55
+ # Realign the current execution context's hub with the main hub so direct
56
56
  # `sentry_events` reads and any hub the Rack middleware clones from the
57
57
  # main hub all observe the same DummyTransport.
58
- Thread.current.thread_variable_set(Sentry::THREAD_LOCAL, main_hub)
58
+ Sentry.set_current_hub_internal(main_hub)
59
59
  end
60
60
 
61
61
  # Clears all stored events and envelopes.
@@ -90,9 +90,7 @@ module Sentry
90
90
  transport.clear if transport.respond_to?(:clear)
91
91
  end
92
92
 
93
- if Sentry.configuration.enable_logs && sentry_logger.respond_to?(:clear)
94
- sentry_logger.clear
95
- end
93
+ sentry_logger.clear if sentry_logger.respond_to?(:clear)
96
94
  end
97
95
 
98
96
  # @return [Sentry::StructuredLogger, Sentry::DebugStructuredLogger]
@@ -159,10 +157,13 @@ module Sentry
159
157
  # Don't check initialized? because sometimes we stub it in tests
160
158
  if Sentry.instance_variable_defined?(:@main_hub)
161
159
  Sentry::GLOBALS.each do |var|
160
+ worker = Sentry.instance_variable_get(:"@#{var}")
161
+ worker.kill if worker.respond_to?(:kill)
162
+
162
163
  Sentry.instance_variable_set(:"@#{var}", nil)
163
164
  end
164
165
 
165
- Thread.current.thread_variable_set(Sentry::THREAD_LOCAL, nil)
166
+ Sentry.set_current_hub_internal(nil)
166
167
  end
167
168
  end
168
169
  end
@@ -30,10 +30,15 @@ module Sentry
30
30
  end
31
31
 
32
32
  def kill
33
- log_debug("[#{self.class.name}] thread killed")
34
-
35
33
  @exited = true
36
- @thread&.kill
34
+
35
+ # Only a started worker has a thread to kill (and to log about).
36
+ # Guarding here keeps a never-started worker's teardown silent, so
37
+ # killing one during test reset can't emit a stray debug line.
38
+ return unless @thread
39
+
40
+ log_debug("[#{self.class.name}] thread killed")
41
+ @thread.kill
37
42
  end
38
43
  end
39
44
  end
@@ -64,7 +64,7 @@ module Sentry
64
64
  end
65
65
  rescue Sentry::SizeExceededError
66
66
  serialized_items&.each do |item|
67
- record_lost_event(:send_error, item.data_category)
67
+ record_lost_event(:send_error, item.data_category, num: item.item_count, num_bytes: item.lost_event_byte_size)
68
68
  end
69
69
  end
70
70
 
@@ -167,11 +167,16 @@ module Sentry
167
167
  envelope
168
168
  end
169
169
 
170
- def record_lost_event(reason, data_category, num: 1)
170
+ def record_lost_event(reason, data_category, num: 1, num_bytes: nil)
171
171
  return unless @send_client_reports
172
172
  return unless CLIENT_REPORT_REASONS.include?(reason)
173
173
 
174
174
  @discarded_events[[reason, data_category]] += num
175
+
176
+ return unless num_bytes
177
+
178
+ byte_category = Envelope::Item.byte_data_category(data_category)
179
+ @discarded_events[[reason, byte_category]] += num_bytes if byte_category
175
180
  end
176
181
 
177
182
  def flush
@@ -211,7 +216,13 @@ module Sentry
211
216
  envelope.items.reject! do |item|
212
217
  if is_rate_limited?(item.data_category)
213
218
  log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
214
- record_lost_event(:ratelimit_backoff, item.data_category)
219
+
220
+ record_lost_event(
221
+ :ratelimit_backoff,
222
+ item.data_category,
223
+ num: item.item_count,
224
+ num_bytes: item.lost_event_byte_size
225
+ )
215
226
 
216
227
  true
217
228
  else
@@ -1,11 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "uri"
4
+
3
5
  module Sentry
4
6
  module Utils
5
7
  module HttpTracing
8
+ def filter_query_params(query)
9
+ return nil unless query
10
+ return nil unless query.is_a?(String) || query.is_a?(Hash)
11
+
12
+ query_hash = if query.is_a?(String)
13
+ URI.decode_www_form(query).each_with_object({}) do |(key, value), params|
14
+ params[key] = params.key?(key) ? Array(params[key]) + [value] : value
15
+ end
16
+ else
17
+ query
18
+ end
19
+
20
+ filtered_query_hash = Sentry.configuration.data_collection.url_query_params.filter(query_hash)
21
+ return nil if filtered_query_hash.empty?
22
+
23
+ HttpTracing.format_query(filtered_query_hash)
24
+ rescue
25
+ nil
26
+ end
27
+
28
+ def self.format_query(query)
29
+ query.flat_map do |key, value|
30
+ Array(value).map { |item| "#{key}=#{item}" }
31
+ end.join("&")
32
+ end
33
+
6
34
  def set_span_info(sentry_span, request_info, response_status)
7
35
  sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
8
- sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
36
+ sentry_span.set_data(Span::DataConventions::URL, url_with_query(request_info))
9
37
  sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
10
38
  sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
11
39
  sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status)
@@ -43,37 +71,25 @@ module Sentry
43
71
  Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
44
72
  end
45
73
 
46
- # Kindly borrowed from Rack::Utils
47
- def build_nested_query(value, prefix = nil)
48
- case value
49
- when Array
50
- value.map { |v|
51
- build_nested_query(v, "#{prefix}[]")
52
- }.join("&")
53
- when Hash
54
- value.map { |k, v|
55
- build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
56
- }.delete_if(&:empty?).join("&")
57
- when nil
58
- URI.encode_www_form_component(prefix)
59
- else
60
- raise ArgumentError, "value must be a Hash" if prefix.nil?
61
- "#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
62
- end
63
- end
64
74
 
65
75
  private
66
76
 
77
+ def url_with_query(request_info)
78
+ return request_info[:url] unless request_info[:query]
79
+
80
+ "#{request_info[:url]}?#{request_info[:query]}"
81
+ end
82
+
67
83
  def get_level(status)
68
84
  return :info unless status && status.is_a?(Integer)
69
85
 
70
- if status >= 500
71
- :error
72
- elsif status >= 400
73
- :warning
74
- else
75
- :info
76
- end
86
+ if status >= 500
87
+ :error
88
+ elsif status >= 400
89
+ :warning
90
+ else
91
+ :info
92
+ end
77
93
  end
78
94
  end
79
95
  end
@@ -7,23 +7,35 @@ module Sentry
7
7
  module TelemetryAttributes
8
8
  private
9
9
 
10
- def attribute_hash(value)
11
- case value
12
- when String
13
- { value: value, type: "string" }
14
- when TrueClass, FalseClass
15
- { value: value, type: "boolean" }
16
- when Integer
17
- { value: value, type: "integer" }
18
- when Float
19
- { value: value, type: "double" }
10
+ def attribute_hash(raw_value)
11
+ if raw_value.is_a?(Hash) && (raw_value.key?(:value) || raw_value.key?("value"))
12
+ value = raw_value.key?(:value) ? raw_value[:value] : raw_value["value"]
13
+ unit = raw_value.key?(:unit) ? raw_value[:unit] : raw_value["unit"]
20
14
  else
21
- begin
22
- { value: JSON.generate(value), type: "string" }
23
- rescue
15
+ value = raw_value
16
+ unit = nil
17
+ end
18
+
19
+ result =
20
+ case value
21
+ when String
24
22
  { value: value, type: "string" }
23
+ when TrueClass, FalseClass
24
+ { value: value, type: "boolean" }
25
+ when Integer
26
+ { value: value, type: "integer" }
27
+ when Float
28
+ { value: value, type: "double" }
29
+ else
30
+ begin
31
+ { value: JSON.generate(value), type: "string" }
32
+ rescue
33
+ { value: value, type: "string" }
34
+ end
25
35
  end
26
- end
36
+
37
+ result[:unit] = unit.to_s if unit.is_a?(String) || unit.is_a?(Symbol)
38
+ result
27
39
  end
28
40
  end
29
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "6.6.2"
4
+ VERSION = "7.0.0"
5
5
  end