sentry-ruby 5.18.2 → 5.28.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -1
  3. data/Gemfile +9 -3
  4. data/README.md +12 -7
  5. data/Rakefile +9 -11
  6. data/bin/console +1 -0
  7. data/lib/sentry/attachment.rb +40 -0
  8. data/lib/sentry/background_worker.rb +2 -3
  9. data/lib/sentry/backpressure_monitor.rb +1 -1
  10. data/lib/sentry/backtrace.rb +7 -8
  11. data/lib/sentry/baggage.rb +7 -7
  12. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  13. data/lib/sentry/breadcrumb.rb +5 -4
  14. data/lib/sentry/check_in_event.rb +5 -4
  15. data/lib/sentry/client.rb +121 -19
  16. data/lib/sentry/configuration.rb +207 -38
  17. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  18. data/lib/sentry/cron/monitor_check_ins.rb +6 -4
  19. data/lib/sentry/cron/monitor_config.rb +1 -1
  20. data/lib/sentry/debug_structured_logger.rb +94 -0
  21. data/lib/sentry/dsn.rb +35 -3
  22. data/lib/sentry/envelope/item.rb +88 -0
  23. data/lib/sentry/envelope.rb +2 -85
  24. data/lib/sentry/event.rb +13 -8
  25. data/lib/sentry/excon/middleware.rb +77 -0
  26. data/lib/sentry/excon.rb +10 -0
  27. data/lib/sentry/faraday.rb +77 -0
  28. data/lib/sentry/graphql.rb +1 -1
  29. data/lib/sentry/hub.rb +57 -4
  30. data/lib/sentry/interfaces/mechanism.rb +1 -1
  31. data/lib/sentry/interfaces/request.rb +6 -6
  32. data/lib/sentry/interfaces/single_exception.rb +3 -3
  33. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +15 -2
  35. data/lib/sentry/linecache.rb +3 -3
  36. data/lib/sentry/log_event.rb +219 -0
  37. data/lib/sentry/log_event_buffer.rb +75 -0
  38. data/lib/sentry/logger.rb +1 -1
  39. data/lib/sentry/metrics/aggregator.rb +13 -13
  40. data/lib/sentry/metrics/configuration.rb +12 -2
  41. data/lib/sentry/metrics/set_metric.rb +2 -2
  42. data/lib/sentry/metrics/timing.rb +8 -0
  43. data/lib/sentry/metrics.rb +27 -15
  44. data/lib/sentry/net/http.rb +17 -38
  45. data/lib/sentry/profiler/helpers.rb +46 -0
  46. data/lib/sentry/profiler.rb +27 -57
  47. data/lib/sentry/propagation_context.rb +60 -23
  48. data/lib/sentry/rack/capture_exceptions.rb +2 -2
  49. data/lib/sentry/rack.rb +2 -2
  50. data/lib/sentry/rake.rb +2 -2
  51. data/lib/sentry/release_detector.rb +4 -4
  52. data/lib/sentry/rspec.rb +91 -0
  53. data/lib/sentry/scope.rb +27 -2
  54. data/lib/sentry/session_flusher.rb +10 -6
  55. data/lib/sentry/span.rb +17 -4
  56. data/lib/sentry/std_lib_logger.rb +55 -0
  57. data/lib/sentry/structured_logger.rb +138 -0
  58. data/lib/sentry/test_helper.rb +45 -1
  59. data/lib/sentry/threaded_periodic_worker.rb +3 -3
  60. data/lib/sentry/transaction.rb +41 -13
  61. data/lib/sentry/transaction_event.rb +5 -3
  62. data/lib/sentry/transport/debug_transport.rb +70 -0
  63. data/lib/sentry/transport/dummy_transport.rb +1 -0
  64. data/lib/sentry/transport/http_transport.rb +21 -18
  65. data/lib/sentry/transport.rb +26 -11
  66. data/lib/sentry/utils/env_helper.rb +21 -0
  67. data/lib/sentry/utils/http_tracing.rb +74 -0
  68. data/lib/sentry/utils/logging_helper.rb +10 -3
  69. data/lib/sentry/utils/real_ip.rb +1 -1
  70. data/lib/sentry/utils/sample_rand.rb +97 -0
  71. data/lib/sentry/utils/uuid.rb +13 -0
  72. data/lib/sentry/vernier/output.rb +89 -0
  73. data/lib/sentry/vernier/profiler.rb +132 -0
  74. data/lib/sentry/version.rb +1 -1
  75. data/lib/sentry-ruby.rb +106 -12
  76. data/sentry-ruby-core.gemspec +3 -1
  77. data/sentry-ruby.gemspec +4 -2
  78. metadata +28 -12
@@ -5,7 +5,7 @@ require "sentry/envelope"
5
5
 
6
6
  module Sentry
7
7
  class Transport
8
- PROTOCOL_VERSION = '7'
8
+ PROTOCOL_VERSION = "7"
9
9
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
10
10
  CLIENT_REPORT_INTERVAL = 30
11
11
 
@@ -26,11 +26,8 @@ module Sentry
26
26
 
27
27
  attr_reader :rate_limits, :discarded_events, :last_client_report_sent
28
28
 
29
- # @deprecated Use Sentry.logger to retrieve the current logger instead.
30
- attr_reader :logger
31
-
32
29
  def initialize(configuration)
33
- @logger = configuration.logger
30
+ @sdk_logger = configuration.sdk_logger
34
31
  @transport_configuration = configuration.transport
35
32
  @dsn = configuration.dsn
36
33
  @rate_limits = {}
@@ -133,18 +130,35 @@ module Sentry
133
130
 
134
131
  envelope = Envelope.new(envelope_headers)
135
132
 
136
- envelope.add_item(
137
- { type: item_type, content_type: 'application/json' },
138
- event_payload
139
- )
133
+ if event.is_a?(LogEvent)
134
+ envelope.add_item(
135
+ {
136
+ type: "log",
137
+ item_count: 1,
138
+ content_type: "application/vnd.sentry.items.log+json"
139
+ },
140
+ { items: [event_payload] }
141
+ )
142
+ else
143
+ envelope.add_item(
144
+ { type: item_type, content_type: "application/json" },
145
+ event_payload
146
+ )
147
+ end
140
148
 
141
149
  if event.is_a?(TransactionEvent) && event.profile
142
150
  envelope.add_item(
143
- { type: 'profile', content_type: 'application/json' },
151
+ { type: "profile", content_type: "application/json" },
144
152
  event.profile
145
153
  )
146
154
  end
147
155
 
156
+ if event.is_a?(Event) && event.attachments.any?
157
+ event.attachments.each do |attachment|
158
+ envelope.add_item(attachment.to_envelope_headers, attachment.payload)
159
+ end
160
+ end
161
+
148
162
  client_report_headers, client_report_payload = fetch_pending_client_report
149
163
  envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
150
164
 
@@ -179,7 +193,7 @@ module Sentry
179
193
  { reason: reason, category: category, quantity: val }
180
194
  end
181
195
 
182
- item_header = { type: 'client_report' }
196
+ item_header = { type: "client_report" }
183
197
  item_payload = {
184
198
  timestamp: Sentry.utc_now.iso8601,
185
199
  discarded_events: discarded_events_hash
@@ -209,3 +223,4 @@ end
209
223
  require "sentry/transport/dummy_transport"
210
224
  require "sentry/transport/http_transport"
211
225
  require "sentry/transport/spotlight_transport"
226
+ require "sentry/transport/debug_transport"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ module EnvHelper
6
+ TRUTHY_ENV_VALUES = %w[t true yes y 1 on].freeze
7
+ FALSY_ENV_VALUES = %w[f false no n 0 off].freeze
8
+
9
+ def self.env_to_bool(value, strict: false)
10
+ value = value.to_s
11
+ normalized = value.downcase
12
+
13
+ return false if FALSY_ENV_VALUES.include?(normalized)
14
+
15
+ return true if TRUTHY_ENV_VALUES.include?(normalized)
16
+
17
+ strict ? nil : !(value.nil? || value.empty?)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ module HttpTracing
6
+ def set_span_info(sentry_span, request_info, response_status)
7
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
8
+ sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
9
+ sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
10
+ sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
11
+ sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status)
12
+ end
13
+
14
+ def set_propagation_headers(req)
15
+ Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
16
+ end
17
+
18
+ def record_sentry_breadcrumb(request_info, response_status)
19
+ crumb = Sentry::Breadcrumb.new(
20
+ level: get_level(response_status),
21
+ category: self.class::BREADCRUMB_CATEGORY,
22
+ type: "info",
23
+ data: { status: response_status, **request_info }
24
+ )
25
+
26
+ Sentry.add_breadcrumb(crumb)
27
+ end
28
+
29
+ def record_sentry_breadcrumb?
30
+ Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
31
+ end
32
+
33
+ def propagate_trace?(url)
34
+ url &&
35
+ Sentry.initialized? &&
36
+ Sentry.configuration.propagate_traces &&
37
+ Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
38
+ end
39
+
40
+ # Kindly borrowed from Rack::Utils
41
+ def build_nested_query(value, prefix = nil)
42
+ case value
43
+ when Array
44
+ value.map { |v|
45
+ build_nested_query(v, "#{prefix}[]")
46
+ }.join("&")
47
+ when Hash
48
+ value.map { |k, v|
49
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
50
+ }.delete_if(&:empty?).join("&")
51
+ when nil
52
+ URI.encode_www_form_component(prefix)
53
+ else
54
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
55
+ "#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def get_level(status)
62
+ return :info unless status && status.is_a?(Integer)
63
+
64
+ if status >= 500
65
+ :error
66
+ elsif status >= 400
67
+ :warning
68
+ else
69
+ :info
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,22 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
+ # @private
4
5
  module LoggingHelper
6
+ # @!visibility private
7
+ attr_reader :sdk_logger
8
+
9
+ # @!visibility private
5
10
  def log_error(message, exception, debug: false)
6
11
  message = "#{message}: #{exception.message}"
7
12
  message += "\n#{exception.backtrace.join("\n")}" if debug
8
13
 
9
- @logger.error(LOGGER_PROGNAME) do
14
+ sdk_logger.error(LOGGER_PROGNAME) do
10
15
  message
11
16
  end
12
17
  end
13
18
 
19
+ # @!visibility private
14
20
  def log_debug(message)
15
- @logger.debug(LOGGER_PROGNAME) { message }
21
+ sdk_logger.debug(LOGGER_PROGNAME) { message }
16
22
  end
17
23
 
24
+ # @!visibility private
18
25
  def log_warn(message)
19
- @logger.warn(LOGGER_PROGNAME) { message }
26
+ sdk_logger.warn(LOGGER_PROGNAME) { message }
20
27
  end
21
28
  end
22
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ipaddr'
3
+ require "ipaddr"
4
4
 
5
5
  # Based on ActionDispatch::RemoteIp. All security-related precautions from that
6
6
  # middleware have been removed, because the Event IP just needs to be accurate,
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ class SampleRand
6
+ PRECISION = 1_000_000.0
7
+ FORMAT_PRECISION = 6
8
+
9
+ attr_reader :trace_id
10
+
11
+ def self.valid?(value)
12
+ return false unless value
13
+ value >= 0.0 && value < 1.0
14
+ end
15
+
16
+ def self.format(value)
17
+ return unless value
18
+
19
+ truncated = (value * PRECISION).floor / PRECISION
20
+ "%.#{FORMAT_PRECISION}f" % truncated
21
+ end
22
+
23
+ def initialize(trace_id: nil)
24
+ @trace_id = trace_id
25
+ end
26
+
27
+ def generate_from_trace_id
28
+ (random_from_trace_id * PRECISION).floor / PRECISION
29
+ end
30
+
31
+ def generate_from_sampling_decision(sampled, sample_rate)
32
+ if invalid_sample_rate?(sample_rate)
33
+ fallback_generation
34
+ else
35
+ generate_based_on_sampling(sampled, sample_rate)
36
+ end
37
+ end
38
+
39
+ def generate_from_value(sample_rand_value)
40
+ parsed_value = parse_value(sample_rand_value)
41
+
42
+ if self.class.valid?(parsed_value)
43
+ parsed_value
44
+ else
45
+ fallback_generation
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def random_from_trace_id
52
+ if @trace_id
53
+ Random.new(@trace_id[0, 16].to_i(16))
54
+ else
55
+ Random.new
56
+ end.rand(1.0)
57
+ end
58
+
59
+ def invalid_sample_rate?(sample_rate)
60
+ sample_rate.nil? || sample_rate <= 0.0 || sample_rate > 1.0
61
+ end
62
+
63
+ def fallback_generation
64
+ if @trace_id
65
+ (random_from_trace_id * PRECISION).floor / PRECISION
66
+ else
67
+ format_random(Random.rand(1.0))
68
+ end
69
+ end
70
+
71
+ def generate_based_on_sampling(sampled, sample_rate)
72
+ random = random_from_trace_id
73
+
74
+ result = if sampled
75
+ random * sample_rate
76
+ elsif sample_rate == 1.0
77
+ random
78
+ else
79
+ sample_rate + random * (1.0 - sample_rate)
80
+ end
81
+
82
+ format_random(result)
83
+ end
84
+
85
+ def format_random(value)
86
+ truncated = (value * PRECISION).floor / PRECISION
87
+ ("%.#{FORMAT_PRECISION}f" % truncated).to_f
88
+ end
89
+
90
+ def parse_value(sample_rand_value)
91
+ Float(sample_rand_value)
92
+ rescue ArgumentError
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Sentry
6
+ module Utils
7
+ DELIMITER = "-"
8
+
9
+ def self.uuid
10
+ SecureRandom.uuid.delete(DELIMITER)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+
6
+ module Sentry
7
+ module Vernier
8
+ class Output
9
+ include Profiler::Helpers
10
+
11
+ attr_reader :profile
12
+
13
+ def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:)
14
+ @profile = profile
15
+ @project_root = project_root
16
+ @in_app_pattern = in_app_pattern
17
+ @app_dirs_pattern = app_dirs_pattern
18
+ end
19
+
20
+ def to_h
21
+ @to_h ||= {
22
+ frames: frames,
23
+ stacks: stacks,
24
+ samples: samples,
25
+ thread_metadata: thread_metadata
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def thread_metadata
32
+ profile.threads.map { |thread_id, thread_info|
33
+ [thread_id, { name: thread_info[:name] }]
34
+ }.to_h
35
+ end
36
+
37
+ def samples
38
+ profile.threads.flat_map { |thread_id, thread_info|
39
+ started_at = thread_info[:started_at]
40
+ samples, timestamps = thread_info.values_at(:samples, :timestamps)
41
+
42
+ samples.zip(timestamps).map { |stack_id, timestamp|
43
+ elapsed_since_start_ns = timestamp - started_at
44
+
45
+ next if elapsed_since_start_ns < 0
46
+
47
+ {
48
+ thread_id: thread_id.to_s,
49
+ stack_id: stack_id,
50
+ elapsed_since_start_ns: elapsed_since_start_ns.to_s
51
+ }
52
+ }.compact
53
+ }
54
+ end
55
+
56
+ def frames
57
+ funcs = stack_table_hash[:frame_table].fetch(:func)
58
+ lines = stack_table_hash[:func_table].fetch(:first_line)
59
+
60
+ funcs.map do |idx|
61
+ function, mod = split_module(stack_table_hash[:func_table][:name][idx])
62
+
63
+ abs_path = stack_table_hash[:func_table][:filename][idx]
64
+ in_app = in_app?(abs_path)
65
+ filename = compute_filename(abs_path, in_app)
66
+
67
+ {
68
+ function: function,
69
+ module: mod,
70
+ filename: filename,
71
+ abs_path: abs_path,
72
+ lineno: (lineno = lines[idx]) > 0 ? lineno : nil,
73
+ in_app: in_app
74
+ }.compact
75
+ end
76
+ end
77
+
78
+ def stacks
79
+ profile._stack_table.stack_count.times.map do |stack_id|
80
+ profile.stack(stack_id).frames.map(&:idx)
81
+ end
82
+ end
83
+
84
+ def stack_table_hash
85
+ @stack_table_hash ||= profile._stack_table.to_h
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "../profiler/helpers"
5
+ require_relative "output"
6
+ require "sentry/utils/uuid"
7
+
8
+ module Sentry
9
+ module Vernier
10
+ class Profiler
11
+ EMPTY_RESULT = {}.freeze
12
+
13
+ attr_reader :started, :event_id, :result
14
+
15
+ def initialize(configuration)
16
+ @event_id = Utils.uuid
17
+
18
+ @started = false
19
+ @sampled = nil
20
+
21
+ @profiling_enabled = defined?(Vernier) && configuration.profiling_enabled?
22
+ @profiles_sample_rate = configuration.profiles_sample_rate
23
+ @project_root = configuration.project_root
24
+ @app_dirs_pattern = configuration.app_dirs_pattern
25
+ @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
26
+ end
27
+
28
+ def set_initial_sample_decision(transaction_sampled)
29
+ unless @profiling_enabled
30
+ @sampled = false
31
+ return
32
+ end
33
+
34
+ unless transaction_sampled
35
+ @sampled = false
36
+ log("Discarding profile because transaction not sampled")
37
+ return
38
+ end
39
+
40
+ case @profiles_sample_rate
41
+ when 0.0
42
+ @sampled = false
43
+ log("Discarding profile because sample_rate is 0")
44
+ return
45
+ when 1.0
46
+ @sampled = true
47
+ return
48
+ else
49
+ @sampled = Random.rand < @profiles_sample_rate
50
+ end
51
+
52
+ log("Discarding profile due to sampling decision") unless @sampled
53
+ end
54
+
55
+ def start
56
+ return unless @sampled
57
+ return if @started
58
+
59
+ @started = ::Vernier.start_profile
60
+
61
+ log("Started")
62
+
63
+ @started
64
+ rescue RuntimeError => e
65
+ # TODO: once Vernier raises something more dedicated, we should catch that instead
66
+ if e.message.include?("Profile already started")
67
+ log("Not started since running elsewhere")
68
+ else
69
+ log("Failed to start: #{e.message}")
70
+ end
71
+ end
72
+
73
+ def stop
74
+ return unless @sampled
75
+ return unless @started
76
+
77
+ @result = ::Vernier.stop_profile
78
+ @started = false
79
+
80
+ log("Stopped")
81
+ rescue RuntimeError => e
82
+ if e.message.include?("Profile not started")
83
+ log("Not stopped since not started")
84
+ else
85
+ log("Failed to stop Vernier: #{e.message}")
86
+ end
87
+ end
88
+
89
+ def active_thread_id
90
+ Thread.current.object_id
91
+ end
92
+
93
+ def to_hash
94
+ unless @sampled
95
+ record_lost_event(:sample_rate)
96
+ return EMPTY_RESULT
97
+ end
98
+
99
+ return EMPTY_RESULT unless result
100
+
101
+ { **profile_meta, profile: output.to_h }
102
+ end
103
+
104
+ private
105
+
106
+ def log(message)
107
+ Sentry.sdk_logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" }
108
+ end
109
+
110
+ def record_lost_event(reason)
111
+ Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
112
+ end
113
+
114
+ def profile_meta
115
+ {
116
+ event_id: @event_id,
117
+ version: "1",
118
+ platform: "ruby"
119
+ }
120
+ end
121
+
122
+ def output
123
+ @output ||= Output.new(
124
+ result,
125
+ project_root: @project_root,
126
+ app_dirs_pattern: @app_dirs_pattern,
127
+ in_app_pattern: @in_app_pattern
128
+ )
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.18.2"
4
+ VERSION = "5.28.1"
5
5
  end