sentry-ruby 5.10.0 → 5.26.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -1
  3. data/Gemfile +12 -13
  4. data/README.md +26 -11
  5. data/Rakefile +9 -11
  6. data/bin/console +2 -0
  7. data/lib/sentry/attachment.rb +40 -0
  8. data/lib/sentry/background_worker.rb +11 -5
  9. data/lib/sentry/backpressure_monitor.rb +45 -0
  10. data/lib/sentry/backtrace.rb +12 -9
  11. data/lib/sentry/baggage.rb +7 -7
  12. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  13. data/lib/sentry/breadcrumb.rb +13 -6
  14. data/lib/sentry/check_in_event.rb +61 -0
  15. data/lib/sentry/client.rb +214 -25
  16. data/lib/sentry/configuration.rb +221 -38
  17. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  18. data/lib/sentry/cron/configuration.rb +23 -0
  19. data/lib/sentry/cron/monitor_check_ins.rb +77 -0
  20. data/lib/sentry/cron/monitor_config.rb +53 -0
  21. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  22. data/lib/sentry/dsn.rb +4 -4
  23. data/lib/sentry/envelope/item.rb +88 -0
  24. data/lib/sentry/envelope.rb +2 -68
  25. data/lib/sentry/error_event.rb +2 -2
  26. data/lib/sentry/event.rb +28 -47
  27. data/lib/sentry/excon/middleware.rb +77 -0
  28. data/lib/sentry/excon.rb +10 -0
  29. data/lib/sentry/faraday.rb +77 -0
  30. data/lib/sentry/graphql.rb +9 -0
  31. data/lib/sentry/hub.rb +138 -6
  32. data/lib/sentry/integrable.rb +10 -0
  33. data/lib/sentry/interface.rb +1 -0
  34. data/lib/sentry/interfaces/exception.rb +5 -3
  35. data/lib/sentry/interfaces/mechanism.rb +20 -0
  36. data/lib/sentry/interfaces/request.rb +8 -8
  37. data/lib/sentry/interfaces/single_exception.rb +13 -9
  38. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  39. data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
  40. data/lib/sentry/linecache.rb +3 -3
  41. data/lib/sentry/log_event.rb +206 -0
  42. data/lib/sentry/log_event_buffer.rb +75 -0
  43. data/lib/sentry/logger.rb +1 -1
  44. data/lib/sentry/metrics/aggregator.rb +248 -0
  45. data/lib/sentry/metrics/configuration.rb +47 -0
  46. data/lib/sentry/metrics/counter_metric.rb +25 -0
  47. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  48. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  49. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  50. data/lib/sentry/metrics/metric.rb +19 -0
  51. data/lib/sentry/metrics/set_metric.rb +28 -0
  52. data/lib/sentry/metrics/timing.rb +51 -0
  53. data/lib/sentry/metrics.rb +56 -0
  54. data/lib/sentry/net/http.rb +27 -44
  55. data/lib/sentry/profiler/helpers.rb +46 -0
  56. data/lib/sentry/profiler.rb +41 -60
  57. data/lib/sentry/propagation_context.rb +135 -0
  58. data/lib/sentry/puma.rb +12 -5
  59. data/lib/sentry/rack/capture_exceptions.rb +17 -8
  60. data/lib/sentry/rack.rb +2 -2
  61. data/lib/sentry/rake.rb +4 -15
  62. data/lib/sentry/redis.rb +10 -4
  63. data/lib/sentry/release_detector.rb +5 -5
  64. data/lib/sentry/rspec.rb +91 -0
  65. data/lib/sentry/scope.rb +75 -39
  66. data/lib/sentry/session.rb +2 -2
  67. data/lib/sentry/session_flusher.rb +15 -43
  68. data/lib/sentry/span.rb +92 -8
  69. data/lib/sentry/std_lib_logger.rb +50 -0
  70. data/lib/sentry/structured_logger.rb +138 -0
  71. data/lib/sentry/test_helper.rb +42 -13
  72. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  73. data/lib/sentry/transaction.rb +44 -43
  74. data/lib/sentry/transaction_event.rb +10 -6
  75. data/lib/sentry/transport/configuration.rb +73 -1
  76. data/lib/sentry/transport/http_transport.rb +71 -41
  77. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  78. data/lib/sentry/transport.rb +53 -49
  79. data/lib/sentry/utils/argument_checking_helper.rb +12 -0
  80. data/lib/sentry/utils/env_helper.rb +21 -0
  81. data/lib/sentry/utils/http_tracing.rb +74 -0
  82. data/lib/sentry/utils/logging_helper.rb +10 -7
  83. data/lib/sentry/utils/real_ip.rb +2 -2
  84. data/lib/sentry/utils/request_id.rb +1 -1
  85. data/lib/sentry/utils/uuid.rb +13 -0
  86. data/lib/sentry/vernier/output.rb +89 -0
  87. data/lib/sentry/vernier/profiler.rb +132 -0
  88. data/lib/sentry/version.rb +1 -1
  89. data/lib/sentry-ruby.rb +206 -35
  90. data/sentry-ruby-core.gemspec +3 -1
  91. data/sentry-ruby.gemspec +15 -6
  92. metadata +61 -11
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "base64"
5
4
  require "sentry/envelope"
6
5
 
7
6
  module Sentry
8
7
  class Transport
9
- PROTOCOL_VERSION = '7'
8
+ PROTOCOL_VERSION = "7"
10
9
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
11
10
  CLIENT_REPORT_INTERVAL = 30
12
11
 
@@ -18,18 +17,17 @@ module Sentry
18
17
  :network_error,
19
18
  :sample_rate,
20
19
  :before_send,
21
- :event_processor
20
+ :event_processor,
21
+ :insufficient_data,
22
+ :backpressure
22
23
  ]
23
24
 
24
25
  include LoggingHelper
25
26
 
26
27
  attr_reader :rate_limits, :discarded_events, :last_client_report_sent
27
28
 
28
- # @deprecated Use Sentry.logger to retrieve the current logger instead.
29
- attr_reader :logger
30
-
31
29
  def initialize(configuration)
32
- @logger = configuration.logger
30
+ @sdk_logger = configuration.sdk_logger
33
31
  @transport_configuration = configuration.transport
34
32
  @dsn = configuration.dsn
35
33
  @rate_limits = {}
@@ -60,7 +58,7 @@ module Sentry
60
58
  data, serialized_items = serialize_envelope(envelope)
61
59
 
62
60
  if data
63
- log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
61
+ log_debug("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
64
62
  send_data(data)
65
63
  end
66
64
  end
@@ -73,7 +71,7 @@ module Sentry
73
71
  result, oversized = item.serialize
74
72
 
75
73
  if oversized
76
- log_info("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
74
+ log_debug("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
77
75
 
78
76
  next
79
77
  end
@@ -87,18 +85,9 @@ module Sentry
87
85
  [data, serialized_items]
88
86
  end
89
87
 
90
- def is_rate_limited?(item_type)
88
+ def is_rate_limited?(data_category)
91
89
  # check category-specific limit
92
- category_delay =
93
- case item_type
94
- when "transaction"
95
- @rate_limits["transaction"]
96
- when "sessions"
97
- @rate_limits["session"]
98
- else
99
- @rate_limits["error"]
100
- end
101
-
90
+ category_delay = @rate_limits[data_category]
102
91
  # check universal limit if not category limit
103
92
  universal_delay = @rate_limits[nil]
104
93
 
@@ -118,16 +107,8 @@ module Sentry
118
107
  !!delay && delay > Time.now
119
108
  end
120
109
 
121
- def generate_auth_header
122
- now = Sentry.utc_now.to_i
123
- fields = {
124
- 'sentry_version' => PROTOCOL_VERSION,
125
- 'sentry_client' => USER_AGENT,
126
- 'sentry_timestamp' => now,
127
- 'sentry_key' => @dsn.public_key
128
- }
129
- fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
130
- 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
110
+ def any_rate_limited?
111
+ @rate_limits.values.any? { |t| t && t > Time.now }
131
112
  end
132
113
 
133
114
  def envelope_from_event(event)
@@ -143,54 +124,76 @@ module Sentry
143
124
  sent_at: Sentry.utc_now.iso8601
144
125
  }
145
126
 
146
- if event.is_a?(TransactionEvent) && event.dynamic_sampling_context
127
+ if event.is_a?(Event) && event.dynamic_sampling_context
147
128
  envelope_headers[:trace] = event.dynamic_sampling_context
148
129
  end
149
130
 
150
131
  envelope = Envelope.new(envelope_headers)
151
132
 
152
- envelope.add_item(
153
- { type: item_type, content_type: 'application/json' },
154
- event_payload
155
- )
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
156
148
 
157
149
  if event.is_a?(TransactionEvent) && event.profile
158
150
  envelope.add_item(
159
- { type: 'profile', content_type: 'application/json' },
151
+ { type: "profile", content_type: "application/json" },
160
152
  event.profile
161
153
  )
162
154
  end
163
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
+
164
162
  client_report_headers, client_report_payload = fetch_pending_client_report
165
163
  envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
166
164
 
167
165
  envelope
168
166
  end
169
167
 
170
- def record_lost_event(reason, item_type)
168
+ def record_lost_event(reason, data_category, num: 1)
171
169
  return unless @send_client_reports
172
170
  return unless CLIENT_REPORT_REASONS.include?(reason)
173
171
 
174
- @discarded_events[[reason, item_type]] += 1
172
+ @discarded_events[[reason, data_category]] += num
173
+ end
174
+
175
+ def flush
176
+ client_report_headers, client_report_payload = fetch_pending_client_report(force: true)
177
+ return unless client_report_headers
178
+
179
+ envelope = Envelope.new
180
+ envelope.add_item(client_report_headers, client_report_payload)
181
+ send_envelope(envelope)
175
182
  end
176
183
 
177
184
  private
178
185
 
179
- def fetch_pending_client_report
186
+ def fetch_pending_client_report(force: false)
180
187
  return nil unless @send_client_reports
181
- return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
188
+ return nil if !force && @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
182
189
  return nil if @discarded_events.empty?
183
190
 
184
191
  discarded_events_hash = @discarded_events.map do |key, val|
185
- reason, type = key
186
-
187
- # 'event' has to be mapped to 'error'
188
- category = type == 'transaction' ? 'transaction' : 'error'
189
-
192
+ reason, category = key
190
193
  { reason: reason, category: category, quantity: val }
191
194
  end
192
195
 
193
- item_header = { type: 'client_report' }
196
+ item_header = { type: "client_report" }
194
197
  item_payload = {
195
198
  timestamp: Sentry.utc_now.iso8601,
196
199
  discarded_events: discarded_events_hash
@@ -204,9 +207,9 @@ module Sentry
204
207
 
205
208
  def reject_rate_limited_items(envelope)
206
209
  envelope.items.reject! do |item|
207
- if is_rate_limited?(item.type)
208
- log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
209
- record_lost_event(:ratelimit_backoff, item.type)
210
+ if is_rate_limited?(item.data_category)
211
+ log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
212
+ record_lost_event(:ratelimit_backoff, item.data_category)
210
213
 
211
214
  true
212
215
  else
@@ -219,3 +222,4 @@ end
219
222
 
220
223
  require "sentry/transport/dummy_transport"
221
224
  require "sentry/transport/http_transport"
225
+ require "sentry/transport/spotlight_transport"
@@ -9,5 +9,17 @@ module Sentry
9
9
  raise ArgumentError, "expect the argument to be a #{expected_types.join(' or ')}, got #{argument.class} (#{argument.inspect})"
10
10
  end
11
11
  end
12
+
13
+ def check_argument_includes!(argument, values)
14
+ unless values.include?(argument)
15
+ raise ArgumentError, "expect the argument to be one of #{values.map(&:inspect).join(' or ')}, got #{argument.inspect}"
16
+ end
17
+ end
18
+
19
+ def check_callable!(name, value)
20
+ unless value == nil || value.respond_to?(:call)
21
+ raise ArgumentError, "#{name} must be callable (or nil to disable)"
22
+ end
23
+ end
12
24
  end
13
25
  end
@@ -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,26 +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
 
14
- def log_info(message)
15
- @logger.info(LOGGER_PROGNAME) { message }
16
- end
17
-
19
+ # @!visibility private
18
20
  def log_debug(message)
19
- @logger.debug(LOGGER_PROGNAME) { message }
21
+ sdk_logger.debug(LOGGER_PROGNAME) { message }
20
22
  end
21
23
 
24
+ # @!visibility private
22
25
  def log_warn(message)
23
- @logger.warn(LOGGER_PROGNAME) { message }
26
+ sdk_logger.warn(LOGGER_PROGNAME) { message }
24
27
  end
25
28
  end
26
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,
@@ -15,7 +15,7 @@ module Sentry
15
15
  "fc00::/7", # private IPv6 range fc00::/7
16
16
  "10.0.0.0/8", # private IPv4 range 10.x.x.x
17
17
  "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
18
- "192.168.0.0/16", # private IPv4 range 192.168.x.x
18
+ "192.168.0.0/16" # private IPv4 range 192.168.x.x
19
19
  ]
20
20
 
21
21
  attr_reader :ip
@@ -3,7 +3,7 @@
3
3
  module Sentry
4
4
  module Utils
5
5
  module RequestId
6
- REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
6
+ REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
7
7
 
8
8
  # Request ID based on ActionDispatch::RequestId
9
9
  def self.read_from(env)
@@ -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.10.0"
4
+ VERSION = "5.26.0"
5
5
  end