sentry-ruby 5.4.2 → 5.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/Gemfile +13 -14
  4. data/README.md +11 -8
  5. data/Rakefile +8 -1
  6. data/lib/sentry/background_worker.rb +8 -1
  7. data/lib/sentry/backpressure_monitor.rb +75 -0
  8. data/lib/sentry/backtrace.rb +1 -1
  9. data/lib/sentry/baggage.rb +70 -0
  10. data/lib/sentry/breadcrumb.rb +8 -2
  11. data/lib/sentry/check_in_event.rb +60 -0
  12. data/lib/sentry/client.rb +77 -19
  13. data/lib/sentry/configuration.rb +177 -29
  14. data/lib/sentry/cron/configuration.rb +23 -0
  15. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  16. data/lib/sentry/cron/monitor_config.rb +53 -0
  17. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  18. data/lib/sentry/envelope.rb +2 -5
  19. data/lib/sentry/event.rb +7 -29
  20. data/lib/sentry/hub.rb +100 -4
  21. data/lib/sentry/integrable.rb +6 -0
  22. data/lib/sentry/interfaces/request.rb +6 -16
  23. data/lib/sentry/interfaces/single_exception.rb +13 -3
  24. data/lib/sentry/net/http.rb +37 -46
  25. data/lib/sentry/profiler.rb +233 -0
  26. data/lib/sentry/propagation_context.rb +134 -0
  27. data/lib/sentry/puma.rb +32 -0
  28. data/lib/sentry/rack/capture_exceptions.rb +4 -5
  29. data/lib/sentry/rake.rb +1 -14
  30. data/lib/sentry/redis.rb +41 -23
  31. data/lib/sentry/release_detector.rb +1 -1
  32. data/lib/sentry/scope.rb +81 -16
  33. data/lib/sentry/session.rb +5 -7
  34. data/lib/sentry/span.rb +57 -10
  35. data/lib/sentry/test_helper.rb +19 -11
  36. data/lib/sentry/transaction.rb +183 -30
  37. data/lib/sentry/transaction_event.rb +51 -0
  38. data/lib/sentry/transport/configuration.rb +74 -1
  39. data/lib/sentry/transport/http_transport.rb +68 -37
  40. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  41. data/lib/sentry/transport.rb +39 -24
  42. data/lib/sentry/utils/argument_checking_helper.rb +9 -3
  43. data/lib/sentry/utils/encoding_helper.rb +22 -0
  44. data/lib/sentry/version.rb +1 -1
  45. data/lib/sentry-ruby.rb +116 -41
  46. metadata +14 -3
  47. data/CODE_OF_CONDUCT.md +0 -74
data/lib/sentry/hub.rb CHANGED
@@ -76,8 +76,9 @@ module Sentry
76
76
  @stack.pop
77
77
  end
78
78
 
79
- def start_transaction(transaction: nil, custom_sampling_context: {}, **options)
79
+ def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options)
80
80
  return unless configuration.tracing_enabled?
81
+ return unless instrumenter == configuration.instrumenter
81
82
 
82
83
  transaction ||= Transaction.new(**options.merge(hub: self))
83
84
 
@@ -87,13 +88,39 @@ module Sentry
87
88
  }
88
89
 
89
90
  sampling_context.merge!(custom_sampling_context)
90
-
91
91
  transaction.set_initial_sample_decision(sampling_context: sampling_context)
92
+
93
+ transaction.start_profiler!
94
+
92
95
  transaction
93
96
  end
94
97
 
98
+ def with_child_span(instrumenter: :sentry, **attributes, &block)
99
+ return yield(nil) unless instrumenter == configuration.instrumenter
100
+
101
+ current_span = current_scope.get_span
102
+ return yield(nil) unless current_span
103
+
104
+ result = nil
105
+
106
+ begin
107
+ current_span.with_child_span(**attributes) do |child_span|
108
+ current_scope.set_span(child_span)
109
+ result = yield(child_span)
110
+ end
111
+ ensure
112
+ current_scope.set_span(current_span)
113
+ end
114
+
115
+ result
116
+ end
117
+
95
118
  def capture_exception(exception, **options, &block)
96
- check_argument_type!(exception, ::Exception)
119
+ if RUBY_PLATFORM == "java"
120
+ check_argument_type!(exception, ::Exception, ::Java::JavaLang::Throwable)
121
+ else
122
+ check_argument_type!(exception, ::Exception)
123
+ end
97
124
 
98
125
  return if Sentry.exception_captured?(exception)
99
126
 
@@ -101,6 +128,7 @@ module Sentry
101
128
 
102
129
  options[:hint] ||= {}
103
130
  options[:hint][:exception] = exception
131
+
104
132
  event = current_client.event_from_exception(exception, options[:hint])
105
133
 
106
134
  return unless event
@@ -128,6 +156,30 @@ module Sentry
128
156
  capture_event(event, **options, &block)
129
157
  end
130
158
 
159
+ def capture_check_in(slug, status, **options)
160
+ check_argument_type!(slug, ::String)
161
+ check_argument_includes!(status, Sentry::CheckInEvent::VALID_STATUSES)
162
+
163
+ return unless current_client
164
+
165
+ options[:hint] ||= {}
166
+ options[:hint][:slug] = slug
167
+
168
+ event = current_client.event_from_check_in(
169
+ slug,
170
+ status,
171
+ options[:hint],
172
+ duration: options.delete(:duration),
173
+ monitor_config: options.delete(:monitor_config),
174
+ check_in_id: options.delete(:check_in_id)
175
+ )
176
+
177
+ return unless event
178
+
179
+ capture_event(event, **options)
180
+ event.check_in_id
181
+ end
182
+
131
183
  def capture_event(event, **options, &block)
132
184
  check_argument_type!(event, Sentry::Event)
133
185
 
@@ -150,7 +202,7 @@ module Sentry
150
202
  configuration.log_debug(event.to_json_compatible)
151
203
  end
152
204
 
153
- @last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent)
205
+ @last_event_id = event&.event_id if event.is_a?(Sentry::ErrorEvent)
154
206
  event
155
207
  end
156
208
 
@@ -201,6 +253,50 @@ module Sentry
201
253
  end_session
202
254
  end
203
255
 
256
+ def get_traceparent
257
+ return nil unless current_scope
258
+
259
+ current_scope.get_span&.to_sentry_trace ||
260
+ current_scope.propagation_context.get_traceparent
261
+ end
262
+
263
+ def get_baggage
264
+ return nil unless current_scope
265
+
266
+ current_scope.get_span&.to_baggage ||
267
+ current_scope.propagation_context.get_baggage&.serialize
268
+ end
269
+
270
+ def get_trace_propagation_headers
271
+ headers = {}
272
+
273
+ traceparent = get_traceparent
274
+ headers[SENTRY_TRACE_HEADER_NAME] = traceparent if traceparent
275
+
276
+ baggage = get_baggage
277
+ headers[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty?
278
+
279
+ headers
280
+ end
281
+
282
+ def continue_trace(env, **options)
283
+ configure_scope { |s| s.generate_propagation_context(env) }
284
+
285
+ return nil unless configuration.tracing_enabled?
286
+
287
+ propagation_context = current_scope.propagation_context
288
+ return nil unless propagation_context.incoming_trace
289
+
290
+ Transaction.new(
291
+ hub: self,
292
+ trace_id: propagation_context.trace_id,
293
+ parent_span_id: propagation_context.parent_span_id,
294
+ parent_sampled: propagation_context.parent_sampled,
295
+ baggage: propagation_context.baggage,
296
+ **options
297
+ )
298
+ end
299
+
204
300
  private
205
301
 
206
302
  def current_layer
@@ -22,5 +22,11 @@ module Sentry
22
22
  options[:hint][:integration] = integration_name
23
23
  Sentry.capture_message(message, **options, &block)
24
24
  end
25
+
26
+ def capture_check_in(slug, status, **options, &block)
27
+ options[:hint] ||= {}
28
+ options[:hint][:integration] = integration_name
29
+ Sentry.capture_check_in(slug, status, **options, &block)
30
+ end
25
31
  end
26
32
  end
@@ -73,7 +73,7 @@ module Sentry
73
73
  request.POST
74
74
  elsif request.body # JSON requests, etc
75
75
  data = request.body.read(MAX_BODY_LIMIT)
76
- data = encode_to_utf_8(data.to_s)
76
+ data = Utils::EncodingHelper.encode_to_utf_8(data.to_s)
77
77
  request.body.rewind
78
78
  data
79
79
  end
@@ -94,7 +94,7 @@ module Sentry
94
94
  key = key.sub(/^HTTP_/, "")
95
95
  key = key.split('_').map(&:capitalize).join('-')
96
96
 
97
- memo[key] = encode_to_utf_8(value.to_s)
97
+ memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
98
98
  rescue StandardError => e
99
99
  # Rails adds objects to the Rack env that can sometimes raise exceptions
100
100
  # when `to_s` is called.
@@ -105,31 +105,21 @@ module Sentry
105
105
  end
106
106
  end
107
107
 
108
- def encode_to_utf_8(value)
109
- if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
110
- value = value.dup.force_encoding(Encoding::UTF_8)
111
- end
112
-
113
- if !value.valid_encoding?
114
- value = value.scrub
115
- end
116
-
117
- value
118
- end
119
-
120
108
  def is_skippable_header?(key)
121
109
  key.upcase != key || # lower-case envs aren't real http headers
122
110
  key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
123
111
  !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
124
112
  end
125
113
 
126
- # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
114
+ # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream
127
115
  # to think this is a Version header. Instead, this is mapped to
128
116
  # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
129
117
  # if the request has legitimately sent a Version header themselves.
130
118
  # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
131
- # NOTE: This will be removed in version 3.0+
132
119
  def is_server_protocol?(key, value, protocol_version)
120
+ rack_version = Gem::Version.new(::Rack.release)
121
+ return false if rack_version >= Gem::Version.new("3.0")
122
+
133
123
  key == 'HTTP_VERSION' && value == protocol_version
134
124
  end
135
125
 
@@ -11,11 +11,21 @@ module Sentry
11
11
  OMISSION_MARK = "...".freeze
12
12
  MAX_LOCAL_BYTES = 1024
13
13
 
14
- attr_reader :type, :value, :module, :thread_id, :stacktrace
14
+ attr_reader :type, :module, :thread_id, :stacktrace
15
+ attr_accessor :value
15
16
 
16
17
  def initialize(exception:, stacktrace: nil)
17
18
  @type = exception.class.to_s
18
- @value = (exception.message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
19
+ exception_message =
20
+ if exception.respond_to?(:detailed_message)
21
+ exception.detailed_message(highlight: false)
22
+ else
23
+ exception.message || ""
24
+ end
25
+ exception_message = exception_message.inspect unless exception_message.is_a?(String)
26
+
27
+ @value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES))
28
+
19
29
  @module = exception.class.to_s.split('::')[0...-1].join('::')
20
30
  @thread_id = Thread.current.object_id
21
31
  @stacktrace = stacktrace
@@ -42,7 +52,7 @@ module Sentry
42
52
  v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK
43
53
  end
44
54
 
45
- v
55
+ Utils::EncodingHelper.encode_to_utf_8(v)
46
56
  rescue StandardError
47
57
  PROBLEMATIC_LOCAL_VALUE_REPLACEMENT
48
58
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
+ require "resolv"
4
5
 
5
6
  module Sentry
6
7
  # @api private
@@ -26,31 +27,38 @@ module Sentry
26
27
  #
27
28
  # So we're only instrumenting request when `Net::HTTP` is already started
28
29
  def request(req, body = nil, &block)
29
- return super unless started?
30
-
31
- sentry_span = start_sentry_span
32
- set_sentry_trace_header(req, sentry_span)
33
-
34
- super.tap do |res|
35
- record_sentry_breadcrumb(req, res)
36
- record_sentry_span(req, res, sentry_span)
30
+ return super unless started? && Sentry.initialized?
31
+ return super if from_sentry_sdk?
32
+
33
+ Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span|
34
+ request_info = extract_request_info(req)
35
+
36
+ if propagate_trace?(request_info[:url], Sentry.configuration)
37
+ set_propagation_headers(req)
38
+ end
39
+
40
+ super.tap do |res|
41
+ record_sentry_breadcrumb(request_info, res)
42
+
43
+ if sentry_span
44
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
45
+ sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
46
+ sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
47
+ sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
48
+ sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, res.code.to_i)
49
+ end
50
+ end
37
51
  end
38
52
  end
39
53
 
40
54
  private
41
55
 
42
- def set_sentry_trace_header(req, sentry_span)
43
- return unless sentry_span
44
-
45
- trace = Sentry.get_current_client.generate_sentry_trace(sentry_span)
46
- req[SENTRY_TRACE_HEADER_NAME] = trace if trace
56
+ def set_propagation_headers(req)
57
+ Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
47
58
  end
48
59
 
49
- def record_sentry_breadcrumb(req, res)
60
+ def record_sentry_breadcrumb(request_info, res)
50
61
  return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
51
- return if from_sentry_sdk?
52
-
53
- request_info = extract_request_info(req)
54
62
 
55
63
  crumb = Sentry::Breadcrumb.new(
56
64
  level: :info,
@@ -64,52 +72,35 @@ module Sentry
64
72
  Sentry.add_breadcrumb(crumb)
65
73
  end
66
74
 
67
- def record_sentry_span(req, res, sentry_span)
68
- return unless Sentry.initialized? && sentry_span
69
-
70
- request_info = extract_request_info(req)
71
- sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
72
- sentry_span.set_data(:status, res.code.to_i)
73
- finish_sentry_span(sentry_span)
74
- end
75
-
76
- def start_sentry_span
77
- return unless Sentry.initialized? && span = Sentry.get_current_scope.get_span
78
- return if from_sentry_sdk?
79
- return if span.sampled == false
80
-
81
- span.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
82
- end
83
-
84
- def finish_sentry_span(sentry_span)
85
- return unless Sentry.initialized? && sentry_span
86
-
87
- sentry_span.set_timestamp(Sentry.utc_now.to_f)
88
- end
89
-
90
75
  def from_sentry_sdk?
91
76
  dsn = Sentry.configuration.dsn
92
77
  dsn && dsn.host == self.address
93
78
  end
94
79
 
95
80
  def extract_request_info(req)
96
- uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}")
81
+ # IPv6 url could look like '::1/path', and that won't parse without
82
+ # wrapping it in square brackets.
83
+ hostname = address =~ Resolv::IPv6::Regex ? "[#{address}]" : address
84
+ uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{req.path}")
97
85
  url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
98
86
 
99
87
  result = { method: req.method, url: url }
100
88
 
101
89
  if Sentry.configuration.send_default_pii
102
- result[:url] = result[:url] + "?#{uri.query}"
90
+ result[:query] = uri.query
103
91
  result[:body] = req.body
104
92
  end
105
93
 
106
94
  result
107
95
  end
96
+
97
+ def propagate_trace?(url, configuration)
98
+ url &&
99
+ configuration.propagate_traces &&
100
+ configuration.trace_propagation_targets.any? { |target| url.match?(target) }
101
+ end
108
102
  end
109
103
  end
110
104
  end
111
105
 
112
- Sentry.register_patch do
113
- patch = Sentry::Net::HTTP
114
- Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
115
- end
106
+ Sentry.register_patch(:http, Sentry::Net::HTTP, Net::HTTP)
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Sentry
6
+ class Profiler
7
+ VERSION = '1'
8
+ PLATFORM = 'ruby'
9
+ # 101 Hz in microseconds
10
+ DEFAULT_INTERVAL = 1e6 / 101
11
+ MICRO_TO_NANO_SECONDS = 1e3
12
+ MIN_SAMPLES_REQUIRED = 2
13
+
14
+ attr_reader :sampled, :started, :event_id
15
+
16
+ def initialize(configuration)
17
+ @event_id = SecureRandom.uuid.delete('-')
18
+ @started = false
19
+ @sampled = nil
20
+
21
+ @profiling_enabled = defined?(StackProf) && 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 || Backtrace::APP_DIRS_PATTERN
25
+ @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
26
+ end
27
+
28
+ def start
29
+ return unless @sampled
30
+
31
+ @started = StackProf.start(interval: DEFAULT_INTERVAL,
32
+ mode: :wall,
33
+ raw: true,
34
+ aggregate: false)
35
+
36
+ @started ? log('Started') : log('Not started since running elsewhere')
37
+ end
38
+
39
+ def stop
40
+ return unless @sampled
41
+ return unless @started
42
+
43
+ StackProf.stop
44
+ log('Stopped')
45
+ end
46
+
47
+ # Sets initial sampling decision of the profile.
48
+ # @return [void]
49
+ def set_initial_sample_decision(transaction_sampled)
50
+ unless @profiling_enabled
51
+ @sampled = false
52
+ return
53
+ end
54
+
55
+ unless transaction_sampled
56
+ @sampled = false
57
+ log('Discarding profile because transaction not sampled')
58
+ return
59
+ end
60
+
61
+ case @profiles_sample_rate
62
+ when 0.0
63
+ @sampled = false
64
+ log('Discarding profile because sample_rate is 0')
65
+ return
66
+ when 1.0
67
+ @sampled = true
68
+ return
69
+ else
70
+ @sampled = Random.rand < @profiles_sample_rate
71
+ end
72
+
73
+ log('Discarding profile due to sampling decision') unless @sampled
74
+ end
75
+
76
+ def to_hash
77
+ unless @sampled
78
+ record_lost_event(:sample_rate)
79
+ return {}
80
+ end
81
+
82
+ return {} unless @started
83
+
84
+ results = StackProf.results
85
+
86
+ if !results || results.empty? || results[:samples] == 0 || !results[:raw]
87
+ record_lost_event(:insufficient_data)
88
+ return {}
89
+ end
90
+
91
+ frame_map = {}
92
+
93
+ frames = results[:frames].to_enum.with_index.map do |frame, idx|
94
+ frame_id, frame_data = frame
95
+
96
+ # need to map over stackprof frame ids to ours
97
+ frame_map[frame_id] = idx
98
+
99
+ file_path = frame_data[:file]
100
+ in_app = in_app?(file_path)
101
+ filename = compute_filename(file_path, in_app)
102
+ function, mod = split_module(frame_data[:name])
103
+
104
+ frame_hash = {
105
+ abs_path: file_path,
106
+ function: function,
107
+ filename: filename,
108
+ in_app: in_app
109
+ }
110
+
111
+ frame_hash[:module] = mod if mod
112
+ frame_hash[:lineno] = frame_data[:line] if frame_data[:line] && frame_data[:line] >= 0
113
+
114
+ frame_hash
115
+ end
116
+
117
+ idx = 0
118
+ stacks = []
119
+ num_seen = []
120
+
121
+ # extract stacks from raw
122
+ # raw is a single array of [.., len_stack, *stack_frames(len_stack), num_stack_seen , ..]
123
+ while (len = results[:raw][idx])
124
+ idx += 1
125
+
126
+ # our call graph is reversed
127
+ stack = results[:raw].slice(idx, len).map { |id| frame_map[id] }.compact.reverse
128
+ stacks << stack
129
+
130
+ num_seen << results[:raw][idx + len]
131
+ idx += len + 1
132
+
133
+ log('Unknown frame in stack') if stack.size != len
134
+ end
135
+
136
+ idx = 0
137
+ elapsed_since_start_ns = 0
138
+ samples = []
139
+
140
+ num_seen.each_with_index do |n, i|
141
+ n.times do
142
+ # stackprof deltas are in microseconds
143
+ delta = results[:raw_timestamp_deltas][idx]
144
+ elapsed_since_start_ns += (delta * MICRO_TO_NANO_SECONDS).to_i
145
+ idx += 1
146
+
147
+ # Not sure why but some deltas are very small like 0/1 values,
148
+ # they pollute our flamegraph so just ignore them for now.
149
+ # Open issue at https://github.com/tmm1/stackprof/issues/201
150
+ next if delta < 10
151
+
152
+ samples << {
153
+ stack_id: i,
154
+ # TODO-neel-profiler we need to patch rb_profile_frames and write our own C extension to enable threading info.
155
+ # Till then, on multi-threaded servers like puma, we will get frames from other active threads when the one
156
+ # we're profiling is idle/sleeping/waiting for IO etc.
157
+ # https://bugs.ruby-lang.org/issues/10602
158
+ thread_id: '0',
159
+ elapsed_since_start_ns: elapsed_since_start_ns.to_s
160
+ }
161
+ end
162
+ end
163
+
164
+ log('Some samples thrown away') if samples.size != results[:samples]
165
+
166
+ if samples.size <= MIN_SAMPLES_REQUIRED
167
+ log('Not enough samples, discarding profiler')
168
+ record_lost_event(:insufficient_data)
169
+ return {}
170
+ end
171
+
172
+ profile = {
173
+ frames: frames,
174
+ stacks: stacks,
175
+ samples: samples
176
+ }
177
+
178
+ {
179
+ event_id: @event_id,
180
+ platform: PLATFORM,
181
+ version: VERSION,
182
+ profile: profile
183
+ }
184
+ end
185
+
186
+ private
187
+
188
+ def log(message)
189
+ Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
190
+ end
191
+
192
+ def in_app?(abs_path)
193
+ abs_path.match?(@in_app_pattern)
194
+ end
195
+
196
+ # copied from stacktrace.rb since I don't want to touch existing code
197
+ # TODO-neel-profiler try to fetch this from stackprof once we patch
198
+ # the native extension
199
+ def compute_filename(abs_path, in_app)
200
+ return nil if abs_path.nil?
201
+
202
+ under_project_root = @project_root && abs_path.start_with?(@project_root)
203
+
204
+ prefix =
205
+ if under_project_root && in_app
206
+ @project_root
207
+ else
208
+ longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
209
+
210
+ if under_project_root
211
+ longest_load_path || @project_root
212
+ else
213
+ longest_load_path
214
+ end
215
+ end
216
+
217
+ prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
218
+ end
219
+
220
+ def split_module(name)
221
+ # last module plus class/instance method
222
+ i = name.rindex('::')
223
+ function = i ? name[(i + 2)..-1] : name
224
+ mod = i ? name[0...i] : nil
225
+
226
+ [function, mod]
227
+ end
228
+
229
+ def record_lost_event(reason)
230
+ Sentry.get_current_client&.transport&.record_lost_event(reason, 'profile')
231
+ end
232
+ end
233
+ end