sentry-ruby 5.4.2 → 5.16.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 (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