sentry-ruby-core 5.20.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.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/Rakefile +2 -0
- data/bin/console +1 -0
- data/lib/sentry/backtrace.rb +2 -2
- data/lib/sentry/baggage.rb +1 -1
- data/lib/sentry/configuration.rb +27 -9
- data/lib/sentry/cron/monitor_check_ins.rb +2 -0
- data/lib/sentry/envelope/item.rb +88 -0
- data/lib/sentry/envelope.rb +2 -85
- data/lib/sentry/interfaces/single_exception.rb +2 -2
- data/lib/sentry/profiler/helpers.rb +46 -0
- data/lib/sentry/profiler.rb +7 -37
- data/lib/sentry/rack/capture_exceptions.rb +1 -1
- data/lib/sentry/test_helper.rb +2 -0
- data/lib/sentry/transaction.rb +2 -2
- data/lib/sentry/transaction_event.rb +1 -2
- data/lib/sentry/vernier/output.rb +89 -0
- data/lib/sentry/vernier/profiler.rb +125 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +4 -3
- data/sentry-ruby-core.gemspec +2 -0
- data/sentry-ruby.gemspec +2 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 890c37ed857f90c7d0b5afbdb4fb07d223b8b3d1a820c1888d680bd653f28688
|
4
|
+
data.tar.gz: 348af55b8f56829cbf5fc5569ddc8308d4cd6ae508480de206443349e1221235
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5b8ad23c029b84648a38095ddd52b289983f9ad18096eeb3fb718a5b8106c6d2513476bc9e3c8ddcad35e66e3f1b20567edbce5f12b661448a811764f9ba4de
|
7
|
+
data.tar.gz: 8eacb9b0e326b529c743af650920b17e382b379032895d112a83689965a53fe42ae48eb9c070cd86085b1d7d5d9cc7300e0d9199b975edc25ee4aca1f093ecd6
|
data/Gemfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source "https://rubygems.org"
|
2
4
|
git_source(:github) { |name| "https://github.com/#{name}.git" }
|
3
5
|
|
@@ -14,6 +16,7 @@ gem "puma"
|
|
14
16
|
|
15
17
|
gem "timecop"
|
16
18
|
gem "stackprof" unless RUBY_PLATFORM == "java"
|
19
|
+
gem "vernier", platforms: :ruby if RUBY_VERSION >= "3.2.1"
|
17
20
|
|
18
21
|
gem "graphql", ">= 2.2.6" if RUBY_VERSION.to_f >= 2.7
|
19
22
|
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
data/lib/sentry/backtrace.rb
CHANGED
@@ -13,10 +13,10 @@ module Sentry
|
|
13
13
|
^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
|
14
14
|
(\d+)
|
15
15
|
(?: :in\s('|`)([^']+)')?$
|
16
|
-
/x
|
16
|
+
/x
|
17
17
|
|
18
18
|
# org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
|
19
|
-
JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)
|
19
|
+
JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/
|
20
20
|
|
21
21
|
# The file portion of the line (such as app/models/user.rb)
|
22
22
|
attr_reader :file
|
data/lib/sentry/baggage.rb
CHANGED
data/lib/sentry/configuration.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
require "concurrent/utility/processor_counter"
|
4
4
|
|
5
5
|
require "sentry/utils/exception_cause_chain"
|
6
|
-
require
|
7
|
-
require
|
6
|
+
require "sentry/utils/custom_inspection"
|
7
|
+
require "sentry/utils/env_helper"
|
8
8
|
require "sentry/dsn"
|
9
9
|
require "sentry/release_detector"
|
10
10
|
require "sentry/transport/configuration"
|
@@ -291,6 +291,10 @@ module Sentry
|
|
291
291
|
# @return [Symbol]
|
292
292
|
attr_reader :instrumenter
|
293
293
|
|
294
|
+
# The profiler class
|
295
|
+
# @return [Class]
|
296
|
+
attr_reader :profiler_class
|
297
|
+
|
294
298
|
# Take a float between 0.0 and 1.0 as the sample rate for capturing profiles.
|
295
299
|
# Note that this rate is relative to traces_sample_rate / traces_sampler,
|
296
300
|
# i.e. the profile is sampled by this rate after the transaction is sampled.
|
@@ -331,19 +335,19 @@ module Sentry
|
|
331
335
|
].freeze
|
332
336
|
|
333
337
|
HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\
|
334
|
-
"release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`"
|
338
|
+
"release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`"
|
335
339
|
|
336
|
-
LOG_PREFIX = "** [Sentry] "
|
337
|
-
MODULE_SEPARATOR = "::"
|
340
|
+
LOG_PREFIX = "** [Sentry] "
|
341
|
+
MODULE_SEPARATOR = "::"
|
338
342
|
SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder]
|
339
343
|
|
340
344
|
INSTRUMENTERS = [:sentry, :otel]
|
341
345
|
|
342
|
-
PROPAGATION_TARGETS_MATCH_ALL =
|
346
|
+
PROPAGATION_TARGETS_MATCH_ALL = /.*/
|
343
347
|
|
344
348
|
DEFAULT_PATCHES = %i[redis puma http].freeze
|
345
349
|
|
346
|
-
APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)
|
350
|
+
APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/
|
347
351
|
|
348
352
|
class << self
|
349
353
|
# Post initialization callbacks are called at the end of initialization process
|
@@ -387,9 +391,9 @@ module Sentry
|
|
387
391
|
self.auto_session_tracking = true
|
388
392
|
self.enable_backpressure_handling = false
|
389
393
|
self.trusted_proxies = []
|
390
|
-
self.dsn = ENV[
|
394
|
+
self.dsn = ENV["SENTRY_DSN"]
|
391
395
|
|
392
|
-
spotlight_env = ENV[
|
396
|
+
spotlight_env = ENV["SENTRY_SPOTLIGHT"]
|
393
397
|
spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
|
394
398
|
self.spotlight = spotlight_bool.nil? ? (spotlight_env || false) : spotlight_bool
|
395
399
|
self.server_name = server_name_from_env
|
@@ -403,6 +407,8 @@ module Sentry
|
|
403
407
|
self.traces_sampler = nil
|
404
408
|
self.enable_tracing = nil
|
405
409
|
|
410
|
+
self.profiler_class = Sentry::Profiler
|
411
|
+
|
406
412
|
@transport = Transport::Configuration.new
|
407
413
|
@cron = Cron::Configuration.new
|
408
414
|
@metrics = Metrics::Configuration.new
|
@@ -498,6 +504,18 @@ module Sentry
|
|
498
504
|
@profiles_sample_rate = profiles_sample_rate
|
499
505
|
end
|
500
506
|
|
507
|
+
def profiler_class=(profiler_class)
|
508
|
+
if profiler_class == Sentry::Vernier::Profiler
|
509
|
+
begin
|
510
|
+
require "vernier"
|
511
|
+
rescue LoadError
|
512
|
+
raise ArgumentError, "Please add the 'vernier' gem to your Gemfile to use the Vernier profiler with Sentry."
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
@profiler_class = profiler_class
|
517
|
+
end
|
518
|
+
|
501
519
|
def sending_allowed?
|
502
520
|
spotlight || sending_to_dsn_allowed?
|
503
521
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# @api private
|
5
|
+
class Envelope::Item
|
6
|
+
STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
|
7
|
+
MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000
|
8
|
+
|
9
|
+
SIZE_LIMITS = Hash.new(MAX_SERIALIZED_PAYLOAD_SIZE).update(
|
10
|
+
"profile" => 1024 * 1000 * 50
|
11
|
+
)
|
12
|
+
|
13
|
+
attr_reader :size_limit, :headers, :payload, :type, :data_category
|
14
|
+
|
15
|
+
# rate limits and client reports use the data_category rather than envelope item type
|
16
|
+
def self.data_category(type)
|
17
|
+
case type
|
18
|
+
when "session", "attachment", "transaction", "profile", "span" then type
|
19
|
+
when "sessions" then "session"
|
20
|
+
when "check_in" then "monitor"
|
21
|
+
when "statsd", "metric_meta" then "metric_bucket"
|
22
|
+
when "event" then "error"
|
23
|
+
when "client_report" then "internal"
|
24
|
+
else "default"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(headers, payload)
|
29
|
+
@headers = headers
|
30
|
+
@payload = payload
|
31
|
+
@type = headers[:type] || "event"
|
32
|
+
@data_category = self.class.data_category(type)
|
33
|
+
@size_limit = SIZE_LIMITS[type]
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
[JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def serialize
|
41
|
+
result = to_s
|
42
|
+
|
43
|
+
if result.bytesize > size_limit
|
44
|
+
remove_breadcrumbs!
|
45
|
+
result = to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
if result.bytesize > size_limit
|
49
|
+
reduce_stacktrace!
|
50
|
+
result = to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
[result, result.bytesize > size_limit]
|
54
|
+
end
|
55
|
+
|
56
|
+
def size_breakdown
|
57
|
+
payload.map do |key, value|
|
58
|
+
"#{key}: #{JSON.generate(value).bytesize}"
|
59
|
+
end.join(", ")
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def remove_breadcrumbs!
|
65
|
+
if payload.key?(:breadcrumbs)
|
66
|
+
payload.delete(:breadcrumbs)
|
67
|
+
elsif payload.key?("breadcrumbs")
|
68
|
+
payload.delete("breadcrumbs")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def reduce_stacktrace!
|
73
|
+
if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
|
74
|
+
exceptions.each do |exception|
|
75
|
+
# in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
|
76
|
+
traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
|
77
|
+
|
78
|
+
if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
|
79
|
+
size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
|
80
|
+
traces.replace(
|
81
|
+
traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/sentry/envelope.rb
CHANGED
@@ -3,91 +3,6 @@
|
|
3
3
|
module Sentry
|
4
4
|
# @api private
|
5
5
|
class Envelope
|
6
|
-
class Item
|
7
|
-
STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
|
8
|
-
MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000
|
9
|
-
|
10
|
-
attr_accessor :headers, :payload
|
11
|
-
|
12
|
-
def initialize(headers, payload)
|
13
|
-
@headers = headers
|
14
|
-
@payload = payload
|
15
|
-
end
|
16
|
-
|
17
|
-
def type
|
18
|
-
@headers[:type] || "event"
|
19
|
-
end
|
20
|
-
|
21
|
-
# rate limits and client reports use the data_category rather than envelope item type
|
22
|
-
def self.data_category(type)
|
23
|
-
case type
|
24
|
-
when "session", "attachment", "transaction", "profile", "span" then type
|
25
|
-
when "sessions" then "session"
|
26
|
-
when "check_in" then "monitor"
|
27
|
-
when "statsd", "metric_meta" then "metric_bucket"
|
28
|
-
when "event" then "error"
|
29
|
-
when "client_report" then "internal"
|
30
|
-
else "default"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def data_category
|
35
|
-
self.class.data_category(type)
|
36
|
-
end
|
37
|
-
|
38
|
-
def to_s
|
39
|
-
[JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
|
40
|
-
end
|
41
|
-
|
42
|
-
def serialize
|
43
|
-
result = to_s
|
44
|
-
|
45
|
-
if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
|
46
|
-
remove_breadcrumbs!
|
47
|
-
result = to_s
|
48
|
-
end
|
49
|
-
|
50
|
-
if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
|
51
|
-
reduce_stacktrace!
|
52
|
-
result = to_s
|
53
|
-
end
|
54
|
-
|
55
|
-
[result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE]
|
56
|
-
end
|
57
|
-
|
58
|
-
def size_breakdown
|
59
|
-
payload.map do |key, value|
|
60
|
-
"#{key}: #{JSON.generate(value).bytesize}"
|
61
|
-
end.join(", ")
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def remove_breadcrumbs!
|
67
|
-
if payload.key?(:breadcrumbs)
|
68
|
-
payload.delete(:breadcrumbs)
|
69
|
-
elsif payload.key?("breadcrumbs")
|
70
|
-
payload.delete("breadcrumbs")
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def reduce_stacktrace!
|
75
|
-
if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
|
76
|
-
exceptions.each do |exception|
|
77
|
-
# in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
|
78
|
-
traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
|
79
|
-
|
80
|
-
if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
|
81
|
-
size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
|
82
|
-
traces.replace(
|
83
|
-
traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
|
84
|
-
)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
6
|
attr_accessor :headers, :items
|
92
7
|
|
93
8
|
def initialize(headers = {})
|
@@ -108,3 +23,5 @@ module Sentry
|
|
108
23
|
end
|
109
24
|
end
|
110
25
|
end
|
26
|
+
|
27
|
+
require_relative "envelope/item"
|
@@ -7,8 +7,8 @@ module Sentry
|
|
7
7
|
include CustomInspection
|
8
8
|
|
9
9
|
SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace]
|
10
|
-
PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]"
|
11
|
-
OMISSION_MARK = "..."
|
10
|
+
PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]"
|
11
|
+
OMISSION_MARK = "..."
|
12
12
|
MAX_LOCAL_BYTES = 1024
|
13
13
|
|
14
14
|
attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class Profiler
|
7
|
+
module Helpers
|
8
|
+
def in_app?(abs_path)
|
9
|
+
abs_path.match?(@in_app_pattern)
|
10
|
+
end
|
11
|
+
|
12
|
+
# copied from stacktrace.rb since I don't want to touch existing code
|
13
|
+
# TODO-neel-profiler try to fetch this from stackprof once we patch
|
14
|
+
# the native extension
|
15
|
+
def compute_filename(abs_path, in_app)
|
16
|
+
return nil if abs_path.nil?
|
17
|
+
|
18
|
+
under_project_root = @project_root && abs_path.start_with?(@project_root)
|
19
|
+
|
20
|
+
prefix =
|
21
|
+
if under_project_root && in_app
|
22
|
+
@project_root
|
23
|
+
else
|
24
|
+
longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
|
25
|
+
|
26
|
+
if under_project_root
|
27
|
+
longest_load_path || @project_root
|
28
|
+
else
|
29
|
+
longest_load_path
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
|
34
|
+
end
|
35
|
+
|
36
|
+
def split_module(name)
|
37
|
+
# last module plus class/instance method
|
38
|
+
i = name.rindex("::")
|
39
|
+
function = i ? name[(i + 2)..-1] : name
|
40
|
+
mod = i ? name[0...i] : nil
|
41
|
+
|
42
|
+
[function, mod]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/sentry/profiler.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "securerandom"
|
4
|
+
require_relative "profiler/helpers"
|
4
5
|
|
5
6
|
module Sentry
|
6
7
|
class Profiler
|
8
|
+
include Profiler::Helpers
|
9
|
+
|
7
10
|
VERSION = "1"
|
8
11
|
PLATFORM = "ruby"
|
9
12
|
# 101 Hz in microseconds
|
@@ -44,6 +47,10 @@ module Sentry
|
|
44
47
|
log("Stopped")
|
45
48
|
end
|
46
49
|
|
50
|
+
def active_thread_id
|
51
|
+
"0"
|
52
|
+
end
|
53
|
+
|
47
54
|
# Sets initial sampling decision of the profile.
|
48
55
|
# @return [void]
|
49
56
|
def set_initial_sample_decision(transaction_sampled)
|
@@ -188,43 +195,6 @@ module Sentry
|
|
188
195
|
Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
|
189
196
|
end
|
190
197
|
|
191
|
-
def in_app?(abs_path)
|
192
|
-
abs_path.match?(@in_app_pattern)
|
193
|
-
end
|
194
|
-
|
195
|
-
# copied from stacktrace.rb since I don't want to touch existing code
|
196
|
-
# TODO-neel-profiler try to fetch this from stackprof once we patch
|
197
|
-
# the native extension
|
198
|
-
def compute_filename(abs_path, in_app)
|
199
|
-
return nil if abs_path.nil?
|
200
|
-
|
201
|
-
under_project_root = @project_root && abs_path.start_with?(@project_root)
|
202
|
-
|
203
|
-
prefix =
|
204
|
-
if under_project_root && in_app
|
205
|
-
@project_root
|
206
|
-
else
|
207
|
-
longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
|
208
|
-
|
209
|
-
if under_project_root
|
210
|
-
longest_load_path || @project_root
|
211
|
-
else
|
212
|
-
longest_load_path
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
|
217
|
-
end
|
218
|
-
|
219
|
-
def split_module(name)
|
220
|
-
# last module plus class/instance method
|
221
|
-
i = name.rindex("::")
|
222
|
-
function = i ? name[(i + 2)..-1] : name
|
223
|
-
mod = i ? name[0...i] : nil
|
224
|
-
|
225
|
-
[function, mod]
|
226
|
-
end
|
227
|
-
|
228
198
|
def record_lost_event(reason)
|
229
199
|
Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
|
230
200
|
end
|
data/lib/sentry/test_helper.rb
CHANGED
data/lib/sentry/transaction.rb
CHANGED
@@ -9,7 +9,7 @@ module Sentry
|
|
9
9
|
# @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
|
10
10
|
SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
|
11
11
|
|
12
|
-
UNLABELD_NAME = "<unlabeled transaction>"
|
12
|
+
UNLABELD_NAME = "<unlabeled transaction>"
|
13
13
|
MESSAGE_PREFIX = "[Tracing]"
|
14
14
|
|
15
15
|
# https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
|
@@ -85,7 +85,7 @@ module Sentry
|
|
85
85
|
@effective_sample_rate = nil
|
86
86
|
@contexts = {}
|
87
87
|
@measurements = {}
|
88
|
-
@profiler =
|
88
|
+
@profiler = @configuration.profiler_class.new(@configuration)
|
89
89
|
init_span_recorder
|
90
90
|
end
|
91
91
|
|
@@ -74,8 +74,7 @@ module Sentry
|
|
74
74
|
id: event_id,
|
75
75
|
name: transaction.name,
|
76
76
|
trace_id: transaction.trace_id,
|
77
|
-
|
78
|
-
active_thead_id: "0"
|
77
|
+
active_thread_id: transaction.profiler.active_thread_id.to_s
|
79
78
|
}
|
80
79
|
)
|
81
80
|
|
@@ -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,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require_relative "../profiler/helpers"
|
5
|
+
require_relative "output"
|
6
|
+
|
7
|
+
module Sentry
|
8
|
+
module Vernier
|
9
|
+
class Profiler
|
10
|
+
EMPTY_RESULT = {}.freeze
|
11
|
+
|
12
|
+
attr_reader :started, :event_id, :result
|
13
|
+
|
14
|
+
def initialize(configuration)
|
15
|
+
@event_id = SecureRandom.uuid.delete("-")
|
16
|
+
|
17
|
+
@started = false
|
18
|
+
@sampled = nil
|
19
|
+
|
20
|
+
@profiling_enabled = defined?(Vernier) && configuration.profiling_enabled?
|
21
|
+
@profiles_sample_rate = configuration.profiles_sample_rate
|
22
|
+
@project_root = configuration.project_root
|
23
|
+
@app_dirs_pattern = configuration.app_dirs_pattern
|
24
|
+
@in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_initial_sample_decision(transaction_sampled)
|
28
|
+
unless @profiling_enabled
|
29
|
+
@sampled = false
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
unless transaction_sampled
|
34
|
+
@sampled = false
|
35
|
+
log("Discarding profile because transaction not sampled")
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
case @profiles_sample_rate
|
40
|
+
when 0.0
|
41
|
+
@sampled = false
|
42
|
+
log("Discarding profile because sample_rate is 0")
|
43
|
+
return
|
44
|
+
when 1.0
|
45
|
+
@sampled = true
|
46
|
+
return
|
47
|
+
else
|
48
|
+
@sampled = Random.rand < @profiles_sample_rate
|
49
|
+
end
|
50
|
+
|
51
|
+
log("Discarding profile due to sampling decision") unless @sampled
|
52
|
+
end
|
53
|
+
|
54
|
+
def start
|
55
|
+
return unless @sampled
|
56
|
+
return if @started
|
57
|
+
|
58
|
+
::Vernier.start_profile
|
59
|
+
@started = true
|
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
|
+
|
79
|
+
log("Stopped")
|
80
|
+
end
|
81
|
+
|
82
|
+
def active_thread_id
|
83
|
+
Thread.current.object_id
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_hash
|
87
|
+
return EMPTY_RESULT unless @started
|
88
|
+
|
89
|
+
unless @sampled
|
90
|
+
record_lost_event(:sample_rate)
|
91
|
+
return EMPTY_RESULT
|
92
|
+
end
|
93
|
+
|
94
|
+
{ **profile_meta, profile: output.to_h }
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def log(message)
|
100
|
+
Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" }
|
101
|
+
end
|
102
|
+
|
103
|
+
def record_lost_event(reason)
|
104
|
+
Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
|
105
|
+
end
|
106
|
+
|
107
|
+
def profile_meta
|
108
|
+
{
|
109
|
+
event_id: @event_id,
|
110
|
+
version: "1",
|
111
|
+
platform: "ruby"
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def output
|
116
|
+
@output ||= Output.new(
|
117
|
+
result,
|
118
|
+
project_root: @project_root,
|
119
|
+
app_dirs_pattern: @app_dirs_pattern,
|
120
|
+
in_app_pattern: @in_app_pattern
|
121
|
+
)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/sentry/version.rb
CHANGED
data/lib/sentry-ruby.rb
CHANGED
@@ -25,6 +25,7 @@ require "sentry/session_flusher"
|
|
25
25
|
require "sentry/backpressure_monitor"
|
26
26
|
require "sentry/cron/monitor_check_ins"
|
27
27
|
require "sentry/metrics"
|
28
|
+
require "sentry/vernier/profiler"
|
28
29
|
|
29
30
|
[
|
30
31
|
"sentry/rake",
|
@@ -41,11 +42,11 @@ module Sentry
|
|
41
42
|
|
42
43
|
CAPTURED_SIGNATURE = :@__sentry_captured
|
43
44
|
|
44
|
-
LOGGER_PROGNAME = "sentry"
|
45
|
+
LOGGER_PROGNAME = "sentry"
|
45
46
|
|
46
|
-
SENTRY_TRACE_HEADER_NAME = "sentry-trace"
|
47
|
+
SENTRY_TRACE_HEADER_NAME = "sentry-trace"
|
47
48
|
|
48
|
-
BAGGAGE_HEADER_NAME = "baggage"
|
49
|
+
BAGGAGE_HEADER_NAME = "baggage"
|
49
50
|
|
50
51
|
THREAD_LOCAL = :sentry_hub
|
51
52
|
|
data/sentry-ruby-core.gemspec
CHANGED
data/sentry-ruby.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sentry-ruby-core
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.21.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sentry Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sentry-ruby
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 5.
|
19
|
+
version: 5.21.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 5.
|
26
|
+
version: 5.21.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: concurrent-ruby
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- lib/sentry/cron/monitor_schedule.rb
|
78
78
|
- lib/sentry/dsn.rb
|
79
79
|
- lib/sentry/envelope.rb
|
80
|
+
- lib/sentry/envelope/item.rb
|
80
81
|
- lib/sentry/error_event.rb
|
81
82
|
- lib/sentry/event.rb
|
82
83
|
- lib/sentry/exceptions.rb
|
@@ -106,6 +107,7 @@ files:
|
|
106
107
|
- lib/sentry/metrics/timing.rb
|
107
108
|
- lib/sentry/net/http.rb
|
108
109
|
- lib/sentry/profiler.rb
|
110
|
+
- lib/sentry/profiler/helpers.rb
|
109
111
|
- lib/sentry/propagation_context.rb
|
110
112
|
- lib/sentry/puma.rb
|
111
113
|
- lib/sentry/rack.rb
|
@@ -135,6 +137,8 @@ files:
|
|
135
137
|
- lib/sentry/utils/logging_helper.rb
|
136
138
|
- lib/sentry/utils/real_ip.rb
|
137
139
|
- lib/sentry/utils/request_id.rb
|
140
|
+
- lib/sentry/vernier/output.rb
|
141
|
+
- lib/sentry/vernier/profiler.rb
|
138
142
|
- lib/sentry/version.rb
|
139
143
|
- sentry-ruby-core.gemspec
|
140
144
|
- sentry-ruby.gemspec
|