sentry-ruby 5.16.1 → 5.21.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -0
  3. data/README.md +20 -10
  4. data/Rakefile +3 -1
  5. data/bin/console +2 -0
  6. data/lib/sentry/attachment.rb +40 -0
  7. data/lib/sentry/background_worker.rb +1 -1
  8. data/lib/sentry/backpressure_monitor.rb +2 -32
  9. data/lib/sentry/backtrace.rb +10 -8
  10. data/lib/sentry/baggage.rb +7 -7
  11. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  12. data/lib/sentry/check_in_event.rb +5 -5
  13. data/lib/sentry/client.rb +61 -11
  14. data/lib/sentry/configuration.rb +77 -31
  15. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  16. data/lib/sentry/cron/monitor_check_ins.rb +3 -1
  17. data/lib/sentry/cron/monitor_config.rb +1 -1
  18. data/lib/sentry/cron/monitor_schedule.rb +1 -1
  19. data/lib/sentry/dsn.rb +4 -4
  20. data/lib/sentry/envelope/item.rb +88 -0
  21. data/lib/sentry/envelope.rb +2 -68
  22. data/lib/sentry/error_event.rb +2 -2
  23. data/lib/sentry/event.rb +20 -18
  24. data/lib/sentry/faraday.rb +77 -0
  25. data/lib/sentry/graphql.rb +9 -0
  26. data/lib/sentry/hub.rb +23 -3
  27. data/lib/sentry/integrable.rb +4 -0
  28. data/lib/sentry/interface.rb +1 -0
  29. data/lib/sentry/interfaces/exception.rb +5 -3
  30. data/lib/sentry/interfaces/mechanism.rb +20 -0
  31. data/lib/sentry/interfaces/request.rb +7 -7
  32. data/lib/sentry/interfaces/single_exception.rb +9 -7
  33. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
  35. data/lib/sentry/logger.rb +1 -1
  36. data/lib/sentry/metrics/aggregator.rb +248 -0
  37. data/lib/sentry/metrics/configuration.rb +47 -0
  38. data/lib/sentry/metrics/counter_metric.rb +25 -0
  39. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  40. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  41. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  42. data/lib/sentry/metrics/metric.rb +19 -0
  43. data/lib/sentry/metrics/set_metric.rb +28 -0
  44. data/lib/sentry/metrics/timing.rb +43 -0
  45. data/lib/sentry/metrics.rb +56 -0
  46. data/lib/sentry/net/http.rb +18 -39
  47. data/lib/sentry/profiler/helpers.rb +46 -0
  48. data/lib/sentry/profiler.rb +25 -56
  49. data/lib/sentry/propagation_context.rb +10 -9
  50. data/lib/sentry/puma.rb +1 -1
  51. data/lib/sentry/rack/capture_exceptions.rb +16 -4
  52. data/lib/sentry/rack.rb +2 -2
  53. data/lib/sentry/rake.rb +4 -2
  54. data/lib/sentry/redis.rb +2 -1
  55. data/lib/sentry/release_detector.rb +4 -4
  56. data/lib/sentry/scope.rb +36 -26
  57. data/lib/sentry/session.rb +2 -2
  58. data/lib/sentry/session_flusher.rb +7 -39
  59. data/lib/sentry/span.rb +46 -5
  60. data/lib/sentry/test_helper.rb +5 -2
  61. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  62. data/lib/sentry/transaction.rb +19 -17
  63. data/lib/sentry/transaction_event.rb +6 -2
  64. data/lib/sentry/transport/configuration.rb +0 -1
  65. data/lib/sentry/transport/http_transport.rb +12 -12
  66. data/lib/sentry/transport.rb +18 -26
  67. data/lib/sentry/utils/argument_checking_helper.rb +6 -0
  68. data/lib/sentry/utils/env_helper.rb +21 -0
  69. data/lib/sentry/utils/http_tracing.rb +41 -0
  70. data/lib/sentry/utils/logging_helper.rb +0 -4
  71. data/lib/sentry/utils/real_ip.rb +2 -2
  72. data/lib/sentry/utils/request_id.rb +1 -1
  73. data/lib/sentry/vernier/output.rb +89 -0
  74. data/lib/sentry/vernier/profiler.rb +125 -0
  75. data/lib/sentry/version.rb +1 -1
  76. data/lib/sentry-ruby.rb +38 -6
  77. data/sentry-ruby-core.gemspec +3 -1
  78. data/sentry-ruby.gemspec +15 -6
  79. metadata +44 -7
data/lib/sentry/hub.rb CHANGED
@@ -73,7 +73,13 @@ module Sentry
73
73
  end
74
74
 
75
75
  def pop_scope
76
- @stack.pop
76
+ if @stack.size > 1
77
+ @stack.pop
78
+ else
79
+ # We never want to enter a situation where we have no scope and no client
80
+ client = current_client
81
+ @stack = [Layer.new(client, Scope.new)]
82
+ end
77
83
  end
78
84
 
79
85
  def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options)
@@ -193,7 +199,14 @@ module Sentry
193
199
  elsif custom_scope = options[:scope]
194
200
  scope.update_from_scope(custom_scope)
195
201
  elsif !options.empty?
196
- scope.update_from_options(**options)
202
+ unsupported_option_keys = scope.update_from_options(**options)
203
+
204
+ unless unsupported_option_keys.empty?
205
+ configuration.log_debug <<~MSG
206
+ Options #{unsupported_option_keys} are not supported and will not be applied to the event.
207
+ You may want to set them under the `extra` option.
208
+ MSG
209
+ end
197
210
  end
198
211
 
199
212
  event = current_client.capture_event(event, scope, hint)
@@ -207,6 +220,7 @@ module Sentry
207
220
  end
208
221
 
209
222
  def add_breadcrumb(breadcrumb, hint: {})
223
+ return unless current_client
210
224
  return unless configuration.enabled_in_current_env?
211
225
 
212
226
  if before_breadcrumb = current_client.configuration.before_breadcrumb
@@ -245,7 +259,7 @@ module Sentry
245
259
  end
246
260
 
247
261
  def with_session_tracking(&block)
248
- return yield unless configuration.auto_session_tracking
262
+ return yield unless configuration.session_tracking?
249
263
 
250
264
  start_session
251
265
  yield
@@ -279,6 +293,12 @@ module Sentry
279
293
  headers
280
294
  end
281
295
 
296
+ def get_trace_propagation_meta
297
+ get_trace_propagation_headers.map do |k, v|
298
+ "<meta name=\"#{k}\" content=\"#{v}\">"
299
+ end.join("\n")
300
+ end
301
+
282
302
  def continue_trace(env, **options)
283
303
  configure_scope { |s| s.generate_propagation_context(env) }
284
304
 
@@ -14,6 +14,10 @@ module Sentry
14
14
  def capture_exception(exception, **options, &block)
15
15
  options[:hint] ||= {}
16
16
  options[:hint][:integration] = integration_name
17
+
18
+ # within an integration, we usually intercept uncaught exceptions so we set handled to false.
19
+ options[:hint][:mechanism] ||= Sentry::Mechanism.new(type: integration_name, handled: false)
20
+
17
21
  Sentry.capture_exception(exception, **options, &block)
18
22
  end
19
23
 
@@ -14,3 +14,4 @@ require "sentry/interfaces/request"
14
14
  require "sentry/interfaces/single_exception"
15
15
  require "sentry/interfaces/stacktrace"
16
16
  require "sentry/interfaces/threads"
17
+ require "sentry/interfaces/mechanism"
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "set"
3
4
 
4
5
  module Sentry
@@ -23,17 +24,18 @@ module Sentry
23
24
  # @param stacktrace_builder [StacktraceBuilder]
24
25
  # @see SingleExceptionInterface#build_with_stacktrace
25
26
  # @see SingleExceptionInterface#initialize
27
+ # @param mechanism [Mechanism]
26
28
  # @return [ExceptionInterface]
27
- def self.build(exception:, stacktrace_builder:)
29
+ def self.build(exception:, stacktrace_builder:, mechanism:)
28
30
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
29
31
  processed_backtrace_ids = Set.new
30
32
 
31
33
  exceptions = exceptions.map do |e|
32
34
  if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
33
35
  processed_backtrace_ids << e.backtrace.object_id
34
- SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder)
36
+ SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism)
35
37
  else
36
- SingleExceptionInterface.new(exception: exception)
38
+ SingleExceptionInterface.new(exception: exception, mechanism: mechanism)
37
39
  end
38
40
  end
39
41
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Mechanism < Interface
5
+ # Generic identifier, mostly the source integration for this exception.
6
+ # @return [String]
7
+ attr_accessor :type
8
+
9
+ # A manually captured exception has handled set to true,
10
+ # false if coming from an integration where we intercept an uncaught exception.
11
+ # Defaults to true here and will be set to false explicitly in integrations.
12
+ # @return [Boolean]
13
+ attr_accessor :handled
14
+
15
+ def initialize(type: "generic", handled: true)
16
+ @type = type
17
+ @handled = handled
18
+ end
19
+ end
20
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Sentry
4
4
  class RequestInterface < Interface
5
- REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
6
- CONTENT_HEADERS = %w(CONTENT_TYPE CONTENT_LENGTH).freeze
5
+ REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
6
+ CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
7
7
  IP_HEADERS = [
8
8
  "REMOTE_ADDR",
9
9
  "HTTP_CLIENT_IP",
@@ -59,7 +59,7 @@ module Sentry
59
59
  self.query_string = request.query_string
60
60
  end
61
61
 
62
- self.url = request.scheme && request.url.split('?').first
62
+ self.url = request.scheme && request.url.split("?").first
63
63
  self.method = request.request_method
64
64
 
65
65
  self.headers = filter_and_format_headers(env, send_default_pii)
@@ -85,14 +85,14 @@ module Sentry
85
85
  env.each_with_object({}) do |(key, value), memo|
86
86
  begin
87
87
  key = key.to_s # rack env can contain symbols
88
- next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
88
+ next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
89
89
  next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
90
90
  next if is_skippable_header?(key)
91
91
  next if key == "HTTP_AUTHORIZATION" && !send_default_pii
92
92
 
93
93
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
94
94
  key = key.sub(/^HTTP_/, "")
95
- key = key.split('_').map(&:capitalize).join('-')
95
+ key = key.split("_").map(&:capitalize).join("-")
96
96
 
97
97
  memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
98
98
  rescue StandardError => e
@@ -108,7 +108,7 @@ module Sentry
108
108
  def is_skippable_header?(key)
109
109
  key.upcase != key || # lower-case envs aren't real http headers
110
110
  key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
111
- !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
111
+ !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
112
112
  end
113
113
 
114
114
  # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream
@@ -120,7 +120,7 @@ module Sentry
120
120
  rack_version = Gem::Version.new(::Rack.release)
121
121
  return false if rack_version >= Gem::Version.new("3.0")
122
122
 
123
- key == 'HTTP_VERSION' && value == protocol_version
123
+ key == "HTTP_VERSION" && value == protocol_version
124
124
  end
125
125
 
126
126
  def filter_and_format_env(env, rack_env_whitelist)
@@ -7,14 +7,14 @@ module Sentry
7
7
  include CustomInspection
8
8
 
9
9
  SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace]
10
- PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]".freeze
11
- OMISSION_MARK = "...".freeze
10
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]"
11
+ OMISSION_MARK = "..."
12
12
  MAX_LOCAL_BYTES = 1024
13
13
 
14
- attr_reader :type, :module, :thread_id, :stacktrace
14
+ attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
15
15
  attr_accessor :value
16
16
 
17
- def initialize(exception:, stacktrace: nil)
17
+ def initialize(exception:, mechanism:, stacktrace: nil)
18
18
  @type = exception.class.to_s
19
19
  exception_message =
20
20
  if exception.respond_to?(:detailed_message)
@@ -26,20 +26,22 @@ module Sentry
26
26
 
27
27
  @value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES))
28
28
 
29
- @module = exception.class.to_s.split('::')[0...-1].join('::')
29
+ @module = exception.class.to_s.split("::")[0...-1].join("::")
30
30
  @thread_id = Thread.current.object_id
31
31
  @stacktrace = stacktrace
32
+ @mechanism = mechanism
32
33
  end
33
34
 
34
35
  def to_hash
35
36
  data = super
36
37
  data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
38
+ data[:mechanism] = data[:mechanism].to_hash
37
39
  data
38
40
  end
39
41
 
40
42
  # patch this method if you want to change an exception's stacktrace frames
41
43
  # also see `StacktraceBuilder.build`.
42
- def self.build_with_stacktrace(exception:, stacktrace_builder:)
44
+ def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:)
43
45
  stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
44
46
 
45
47
  if locals = exception.instance_variable_get(:@sentry_locals)
@@ -61,7 +63,7 @@ module Sentry
61
63
  stacktrace.frames.last.vars = locals
62
64
  end
63
65
 
64
- new(exception: exception, stacktrace: stacktrace)
66
+ new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
65
67
  end
66
68
  end
67
69
  end
@@ -27,8 +27,9 @@ module Sentry
27
27
  attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
28
28
  :lineno, :module, :pre_context, :post_context, :vars
29
29
 
30
- def initialize(project_root, line)
30
+ def initialize(project_root, line, strip_backtrace_load_path = true)
31
31
  @project_root = project_root
32
+ @strip_backtrace_load_path = strip_backtrace_load_path
32
33
 
33
34
  @abs_path = line.file
34
35
  @function = line.method if line.method
@@ -44,6 +45,7 @@ module Sentry
44
45
 
45
46
  def compute_filename
46
47
  return if abs_path.nil?
48
+ return abs_path unless @strip_backtrace_load_path
47
49
 
48
50
  prefix =
49
51
  if under_project_root? && in_app
@@ -17,22 +17,35 @@ module Sentry
17
17
  # @return [Proc, nil]
18
18
  attr_reader :backtrace_cleanup_callback
19
19
 
20
+ # @return [Boolean]
21
+ attr_reader :strip_backtrace_load_path
22
+
20
23
  # @param project_root [String]
21
24
  # @param app_dirs_pattern [Regexp, nil]
22
25
  # @param linecache [LineCache]
23
26
  # @param context_lines [Integer, nil]
24
27
  # @param backtrace_cleanup_callback [Proc, nil]
28
+ # @param strip_backtrace_load_path [Boolean]
25
29
  # @see Configuration#project_root
26
30
  # @see Configuration#app_dirs_pattern
27
31
  # @see Configuration#linecache
28
32
  # @see Configuration#context_lines
29
33
  # @see Configuration#backtrace_cleanup_callback
30
- def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
34
+ # @see Configuration#strip_backtrace_load_path
35
+ def initialize(
36
+ project_root:,
37
+ app_dirs_pattern:,
38
+ linecache:,
39
+ context_lines:,
40
+ backtrace_cleanup_callback: nil,
41
+ strip_backtrace_load_path: true
42
+ )
31
43
  @project_root = project_root
32
44
  @app_dirs_pattern = app_dirs_pattern
33
45
  @linecache = linecache
34
46
  @context_lines = context_lines
35
47
  @backtrace_cleanup_callback = backtrace_cleanup_callback
48
+ @strip_backtrace_load_path = strip_backtrace_load_path
36
49
  end
37
50
 
38
51
  # Generates a StacktraceInterface with the given backtrace.
@@ -62,10 +75,18 @@ module Sentry
62
75
  StacktraceInterface.new(frames: frames)
63
76
  end
64
77
 
78
+ # Get the code location hash for a single line for where metrics where added.
79
+ # @return [Hash]
80
+ def metrics_code_location(unparsed_line)
81
+ parsed_line = Backtrace::Line.parse(unparsed_line)
82
+ frame = convert_parsed_line_into_frame(parsed_line)
83
+ frame.to_hash.reject { |k, _| %i[project_root in_app].include?(k) }
84
+ end
85
+
65
86
  private
66
87
 
67
88
  def convert_parsed_line_into_frame(line)
68
- frame = StacktraceInterface::Frame.new(project_root, line)
89
+ frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path)
69
90
  frame.set_context(linecache, context_lines) if context_lines
70
91
  frame
71
92
  end
data/lib/sentry/logger.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
3
+ require "logger"
4
4
 
5
5
  module Sentry
6
6
  class Logger < ::Logger
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Metrics
5
+ class Aggregator < ThreadedPeriodicWorker
6
+ FLUSH_INTERVAL = 5
7
+ ROLLUP_IN_SECONDS = 10
8
+
9
+ # this is how far removed from user code in the backtrace we are
10
+ # when we record code locations
11
+ DEFAULT_STACKLEVEL = 4
12
+
13
+ KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.]+/
14
+ UNIT_SANITIZATION_REGEX = /[^a-zA-Z0-9_]+/
15
+ TAG_KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.\/]+/
16
+
17
+ TAG_VALUE_SANITIZATION_MAP = {
18
+ "\n" => "\\n",
19
+ "\r" => "\\r",
20
+ "\t" => "\\t",
21
+ "\\" => "\\\\",
22
+ "|" => "\\u{7c}",
23
+ "," => "\\u{2c}"
24
+ }
25
+
26
+ METRIC_TYPES = {
27
+ c: CounterMetric,
28
+ d: DistributionMetric,
29
+ g: GaugeMetric,
30
+ s: SetMetric
31
+ }
32
+
33
+ # exposed only for testing
34
+ attr_reader :client, :thread, :buckets, :flush_shift, :code_locations
35
+
36
+ def initialize(configuration, client)
37
+ super(configuration.logger, FLUSH_INTERVAL)
38
+ @client = client
39
+ @before_emit = configuration.metrics.before_emit
40
+ @enable_code_locations = configuration.metrics.enable_code_locations
41
+ @stacktrace_builder = configuration.stacktrace_builder
42
+
43
+ @default_tags = {}
44
+ @default_tags["release"] = configuration.release if configuration.release
45
+ @default_tags["environment"] = configuration.environment if configuration.environment
46
+
47
+ @mutex = Mutex.new
48
+
49
+ # a nested hash of timestamp -> bucket keys -> Metric instance
50
+ @buckets = {}
51
+
52
+ # the flush interval needs to be shifted once per startup to create jittering
53
+ @flush_shift = Random.rand * ROLLUP_IN_SECONDS
54
+
55
+ # a nested hash of timestamp (start of day) -> meta keys -> frame
56
+ @code_locations = {}
57
+ end
58
+
59
+ def add(type,
60
+ key,
61
+ value,
62
+ unit: "none",
63
+ tags: {},
64
+ timestamp: nil,
65
+ stacklevel: nil)
66
+ return unless ensure_thread
67
+ return unless METRIC_TYPES.keys.include?(type)
68
+
69
+ updated_tags = get_updated_tags(tags)
70
+ return if @before_emit && !@before_emit.call(key, updated_tags)
71
+
72
+ timestamp ||= Sentry.utc_now
73
+
74
+ # this is integer division and thus takes the floor of the division
75
+ # and buckets into 10 second intervals
76
+ bucket_timestamp = (timestamp.to_i / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS
77
+
78
+ serialized_tags = serialize_tags(updated_tags)
79
+ bucket_key = [type, key, unit, serialized_tags]
80
+
81
+ added = @mutex.synchronize do
82
+ record_code_location(type, key, unit, timestamp, stacklevel: stacklevel) if @enable_code_locations
83
+ process_bucket(bucket_timestamp, bucket_key, type, value)
84
+ end
85
+
86
+ # for sets, we pass on if there was a new entry to the local gauge
87
+ local_value = type == :s ? added : value
88
+ process_span_aggregator(bucket_key, local_value)
89
+ end
90
+
91
+ def flush(force: false)
92
+ flushable_buckets = get_flushable_buckets!(force)
93
+ code_locations = get_code_locations!
94
+ return if flushable_buckets.empty? && code_locations.empty?
95
+
96
+ envelope = Envelope.new
97
+
98
+ unless flushable_buckets.empty?
99
+ payload = serialize_buckets(flushable_buckets)
100
+ envelope.add_item(
101
+ { type: "statsd", length: payload.bytesize },
102
+ payload
103
+ )
104
+ end
105
+
106
+ unless code_locations.empty?
107
+ code_locations.each do |timestamp, locations|
108
+ payload = serialize_locations(timestamp, locations)
109
+ envelope.add_item(
110
+ { type: "metric_meta", content_type: "application/json" },
111
+ payload
112
+ )
113
+ end
114
+ end
115
+
116
+ @client.capture_envelope(envelope)
117
+ end
118
+
119
+ alias_method :run, :flush
120
+
121
+ private
122
+
123
+ # important to sort for key consistency
124
+ def serialize_tags(tags)
125
+ tags.flat_map do |k, v|
126
+ if v.is_a?(Array)
127
+ v.map { |x| [k.to_s, x.to_s] }
128
+ else
129
+ [[k.to_s, v.to_s]]
130
+ end
131
+ end.sort
132
+ end
133
+
134
+ def get_flushable_buckets!(force)
135
+ @mutex.synchronize do
136
+ flushable_buckets = {}
137
+
138
+ if force
139
+ flushable_buckets = @buckets
140
+ @buckets = {}
141
+ else
142
+ cutoff = Sentry.utc_now.to_i - ROLLUP_IN_SECONDS - @flush_shift
143
+ flushable_buckets = @buckets.select { |k, _| k <= cutoff }
144
+ @buckets.reject! { |k, _| k <= cutoff }
145
+ end
146
+
147
+ flushable_buckets
148
+ end
149
+ end
150
+
151
+ def get_code_locations!
152
+ @mutex.synchronize do
153
+ code_locations = @code_locations
154
+ @code_locations = {}
155
+ code_locations
156
+ end
157
+ end
158
+
159
+ # serialize buckets to statsd format
160
+ def serialize_buckets(buckets)
161
+ buckets.map do |timestamp, timestamp_buckets|
162
+ timestamp_buckets.map do |metric_key, metric|
163
+ type, key, unit, tags = metric_key
164
+ values = metric.serialize.join(":")
165
+ sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(",")
166
+
167
+ "#{sanitize_key(key)}@#{sanitize_unit(unit)}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}"
168
+ end
169
+ end.flatten.join("\n")
170
+ end
171
+
172
+ def serialize_locations(timestamp, locations)
173
+ mapping = locations.map do |meta_key, location|
174
+ type, key, unit = meta_key
175
+ mri = "#{type}:#{sanitize_key(key)}@#{sanitize_unit(unit)}"
176
+
177
+ # note this needs to be an array but it really doesn't serve a purpose right now
178
+ [mri, [location.merge(type: "location")]]
179
+ end.to_h
180
+
181
+ { timestamp: timestamp, mapping: mapping }
182
+ end
183
+
184
+ def sanitize_key(key)
185
+ key.gsub(KEY_SANITIZATION_REGEX, "_")
186
+ end
187
+
188
+ def sanitize_unit(unit)
189
+ unit.gsub(UNIT_SANITIZATION_REGEX, "")
190
+ end
191
+
192
+ def sanitize_tag_key(key)
193
+ key.gsub(TAG_KEY_SANITIZATION_REGEX, "")
194
+ end
195
+
196
+ def sanitize_tag_value(value)
197
+ value.chars.map { |c| TAG_VALUE_SANITIZATION_MAP[c] || c }.join
198
+ end
199
+
200
+ def get_transaction_name
201
+ scope = Sentry.get_current_scope
202
+ return nil unless scope && scope.transaction_name
203
+ return nil if scope.transaction_source_low_quality?
204
+
205
+ scope.transaction_name
206
+ end
207
+
208
+ def get_updated_tags(tags)
209
+ updated_tags = @default_tags.merge(tags)
210
+
211
+ transaction_name = get_transaction_name
212
+ updated_tags["transaction"] = transaction_name if transaction_name
213
+
214
+ updated_tags
215
+ end
216
+
217
+ def process_span_aggregator(key, value)
218
+ scope = Sentry.get_current_scope
219
+ return nil unless scope && scope.span
220
+ return nil if scope.transaction_source_low_quality?
221
+
222
+ scope.span.metrics_local_aggregator.add(key, value)
223
+ end
224
+
225
+ def process_bucket(timestamp, key, type, value)
226
+ @buckets[timestamp] ||= {}
227
+
228
+ if (metric = @buckets[timestamp][key])
229
+ old_weight = metric.weight
230
+ metric.add(value)
231
+ metric.weight - old_weight
232
+ else
233
+ metric = METRIC_TYPES[type].new(value)
234
+ @buckets[timestamp][key] = metric
235
+ metric.weight
236
+ end
237
+ end
238
+
239
+ def record_code_location(type, key, unit, timestamp, stacklevel: nil)
240
+ meta_key = [type, key, unit]
241
+ start_of_day = Time.utc(timestamp.year, timestamp.month, timestamp.day).to_i
242
+
243
+ @code_locations[start_of_day] ||= {}
244
+ @code_locations[start_of_day][meta_key] ||= @stacktrace_builder.metrics_code_location(caller[stacklevel || DEFAULT_STACKLEVEL])
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Metrics
5
+ class Configuration
6
+ include ArgumentCheckingHelper
7
+
8
+ # Enable metrics usage.
9
+ # Starts a new {Sentry::Metrics::Aggregator} instance to aggregate metrics
10
+ # and a thread to aggregate flush every 5 seconds.
11
+ # @return [Boolean]
12
+ attr_accessor :enabled
13
+
14
+ # Enable code location reporting.
15
+ # Will be sent once per day.
16
+ # True by default.
17
+ # @return [Boolean]
18
+ attr_accessor :enable_code_locations
19
+
20
+ # Optional Proc, called before emitting a metric to the aggregator.
21
+ # Use it to filter keys (return false/nil) or update tags.
22
+ # Make sure to return true at the end.
23
+ #
24
+ # @example
25
+ # config.metrics.before_emit = lambda do |key, tags|
26
+ # return nil if key == 'foo'
27
+ # tags[:bar] = 42
28
+ # tags.delete(:baz)
29
+ # true
30
+ # end
31
+ #
32
+ # @return [Proc, nil]
33
+ attr_reader :before_emit
34
+
35
+ def initialize
36
+ @enabled = false
37
+ @enable_code_locations = true
38
+ end
39
+
40
+ def before_emit=(value)
41
+ check_callable!("metrics.before_emit", value)
42
+
43
+ @before_emit = value
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Metrics
5
+ class CounterMetric < Metric
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value.to_f
10
+ end
11
+
12
+ def add(value)
13
+ @value += value.to_f
14
+ end
15
+
16
+ def serialize
17
+ [value]
18
+ end
19
+
20
+ def weight
21
+ 1
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Metrics
5
+ class DistributionMetric < Metric
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = [value.to_f]
10
+ end
11
+
12
+ def add(value)
13
+ @value << value.to_f
14
+ end
15
+
16
+ def serialize
17
+ value
18
+ end
19
+
20
+ def weight
21
+ value.size
22
+ end
23
+ end
24
+ end
25
+ end