sentry-ruby 5.3.1 → 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.
- checksums.yaml +4 -4
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/Gemfile +26 -0
- data/Makefile +4 -0
- data/README.md +11 -8
- data/Rakefile +20 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +79 -0
- data/lib/sentry/backpressure_monitor.rb +75 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/baggage.rb +70 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +76 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/check_in_event.rb +60 -0
- data/lib/sentry/client.rb +248 -0
- data/lib/sentry/configuration.rb +650 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +75 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +93 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +156 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +316 -0
- data/lib/sentry/integrable.rb +32 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +134 -0
- data/lib/sentry/interfaces/single_exception.rb +67 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +106 -0
- data/lib/sentry/profiler.rb +233 -0
- data/lib/sentry/propagation_context.rb +134 -0
- data/lib/sentry/puma.rb +32 -0
- data/lib/sentry/rack/capture_exceptions.rb +79 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +28 -0
- data/lib/sentry/redis.rb +108 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +360 -0
- data/lib/sentry/session.rb +33 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +273 -0
- data/lib/sentry/test_helper.rb +84 -0
- data/lib/sentry/transaction.rb +359 -0
- data/lib/sentry/transaction_event.rb +80 -0
- data/lib/sentry/transport/configuration.rb +98 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +206 -0
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +225 -0
- data/lib/sentry/utils/argument_checking_helper.rb +19 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +580 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- metadata +75 -16
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class StacktraceBuilder
|
5
|
+
# @return [String]
|
6
|
+
attr_reader :project_root
|
7
|
+
|
8
|
+
# @return [Regexp, nil]
|
9
|
+
attr_reader :app_dirs_pattern
|
10
|
+
|
11
|
+
# @return [LineCache]
|
12
|
+
attr_reader :linecache
|
13
|
+
|
14
|
+
# @return [Integer, nil]
|
15
|
+
attr_reader :context_lines
|
16
|
+
|
17
|
+
# @return [Proc, nil]
|
18
|
+
attr_reader :backtrace_cleanup_callback
|
19
|
+
|
20
|
+
# @param project_root [String]
|
21
|
+
# @param app_dirs_pattern [Regexp, nil]
|
22
|
+
# @param linecache [LineCache]
|
23
|
+
# @param context_lines [Integer, nil]
|
24
|
+
# @param backtrace_cleanup_callback [Proc, nil]
|
25
|
+
# @see Configuration#project_root
|
26
|
+
# @see Configuration#app_dirs_pattern
|
27
|
+
# @see Configuration#linecache
|
28
|
+
# @see Configuration#context_lines
|
29
|
+
# @see Configuration#backtrace_cleanup_callback
|
30
|
+
def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
|
31
|
+
@project_root = project_root
|
32
|
+
@app_dirs_pattern = app_dirs_pattern
|
33
|
+
@linecache = linecache
|
34
|
+
@context_lines = context_lines
|
35
|
+
@backtrace_cleanup_callback = backtrace_cleanup_callback
|
36
|
+
end
|
37
|
+
|
38
|
+
# Generates a StacktraceInterface with the given backtrace.
|
39
|
+
# You can pass a block to customize/exclude frames:
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# builder.build(backtrace) do |frame|
|
43
|
+
# if frame.module.match?(/a_gem/)
|
44
|
+
# nil
|
45
|
+
# else
|
46
|
+
# frame
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
# @param backtrace [Array<String>]
|
50
|
+
# @param frame_callback [Proc]
|
51
|
+
# @yieldparam frame [StacktraceInterface::Frame]
|
52
|
+
# @return [StacktraceInterface]
|
53
|
+
def build(backtrace:, &frame_callback)
|
54
|
+
parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
|
55
|
+
|
56
|
+
frames = parsed_lines.reverse.map do |line|
|
57
|
+
frame = convert_parsed_line_into_frame(line)
|
58
|
+
frame = frame_callback.call(frame) if frame_callback
|
59
|
+
frame
|
60
|
+
end.compact
|
61
|
+
|
62
|
+
StacktraceInterface.new(frames: frames)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def convert_parsed_line_into_frame(line)
|
68
|
+
frame = StacktraceInterface::Frame.new(project_root, line)
|
69
|
+
frame.set_context(linecache, context_lines) if context_lines
|
70
|
+
frame
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_backtrace_lines(backtrace)
|
74
|
+
Backtrace.parse(
|
75
|
+
backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
|
76
|
+
).lines
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class ThreadsInterface
|
5
|
+
# @param crashed [Boolean]
|
6
|
+
# @param stacktrace [Array]
|
7
|
+
def initialize(crashed: false, stacktrace: nil)
|
8
|
+
@id = Thread.current.object_id
|
9
|
+
@name = Thread.current.name
|
10
|
+
@current = true
|
11
|
+
@crashed = crashed
|
12
|
+
@stacktrace = stacktrace
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Hash]
|
16
|
+
def to_hash
|
17
|
+
{
|
18
|
+
values: [
|
19
|
+
{
|
20
|
+
id: @id,
|
21
|
+
name: @name,
|
22
|
+
crashed: @crashed,
|
23
|
+
current: @current,
|
24
|
+
stacktrace: @stacktrace&.to_hash
|
25
|
+
}
|
26
|
+
]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Builds the ThreadsInterface with given backtrace and stacktrace_builder.
|
31
|
+
# Patch this method if you want to change a threads interface's stacktrace frames.
|
32
|
+
# @see StacktraceBuilder.build
|
33
|
+
# @param backtrace [Array]
|
34
|
+
# @param stacktrace_builder [StacktraceBuilder]
|
35
|
+
# @param crashed [Hash]
|
36
|
+
# @return [ThreadsInterface]
|
37
|
+
def self.build(backtrace:, stacktrace_builder:, **options)
|
38
|
+
stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace
|
39
|
+
new(**options, stacktrace: stacktrace)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# @api private
|
5
|
+
class LineCache
|
6
|
+
def initialize
|
7
|
+
@cache = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Any linecache you provide to Sentry must implement this method.
|
11
|
+
# Returns an Array of Strings representing the lines in the source
|
12
|
+
# file. The number of lines retrieved is (2 * context) + 1, the middle
|
13
|
+
# line should be the line requested by lineno. See specs for more information.
|
14
|
+
def get_file_context(filename, lineno, context)
|
15
|
+
return nil, nil, nil unless valid_path?(filename)
|
16
|
+
|
17
|
+
lines = Array.new(2 * context + 1) do |i|
|
18
|
+
getline(filename, lineno - context + i)
|
19
|
+
end
|
20
|
+
[lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def valid_path?(path)
|
26
|
+
lines = getlines(path)
|
27
|
+
!lines.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def getlines(path)
|
31
|
+
@cache[path] ||= begin
|
32
|
+
IO.readlines(path)
|
33
|
+
rescue
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def getline(path, n)
|
39
|
+
return nil if n < 1
|
40
|
+
|
41
|
+
lines = getlines(path)
|
42
|
+
return nil if lines.nil?
|
43
|
+
|
44
|
+
lines[n - 1]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class Logger < ::Logger
|
7
|
+
LOG_PREFIX = "** [Sentry] "
|
8
|
+
PROGNAME = "sentry"
|
9
|
+
|
10
|
+
def initialize(*)
|
11
|
+
super
|
12
|
+
@level = ::Logger::INFO
|
13
|
+
original_formatter = ::Logger::Formatter.new
|
14
|
+
@default_formatter = proc do |severity, datetime, _progname, msg|
|
15
|
+
msg = "#{LOG_PREFIX}#{msg}"
|
16
|
+
original_formatter.call(severity, datetime, PROGNAME, msg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "resolv"
|
5
|
+
|
6
|
+
module Sentry
|
7
|
+
# @api private
|
8
|
+
module Net
|
9
|
+
module HTTP
|
10
|
+
OP_NAME = "http.client"
|
11
|
+
BREADCRUMB_CATEGORY = "net.http"
|
12
|
+
|
13
|
+
# To explain how the entire thing works, we need to know how the original Net::HTTP#request works
|
14
|
+
# Here's part of its definition. As you can see, it usually calls itself inside a #start block
|
15
|
+
#
|
16
|
+
# ```
|
17
|
+
# def request(req, body = nil, &block)
|
18
|
+
# unless started?
|
19
|
+
# start {
|
20
|
+
# req['connection'] ||= 'close'
|
21
|
+
# return request(req, body, &block) # <- request will be called for the second time from the first call
|
22
|
+
# }
|
23
|
+
# end
|
24
|
+
# # .....
|
25
|
+
# end
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# So we're only instrumenting request when `Net::HTTP` is already started
|
29
|
+
def request(req, body = nil, &block)
|
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
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def set_propagation_headers(req)
|
57
|
+
Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
|
58
|
+
end
|
59
|
+
|
60
|
+
def record_sentry_breadcrumb(request_info, res)
|
61
|
+
return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
|
62
|
+
|
63
|
+
crumb = Sentry::Breadcrumb.new(
|
64
|
+
level: :info,
|
65
|
+
category: BREADCRUMB_CATEGORY,
|
66
|
+
type: :info,
|
67
|
+
data: {
|
68
|
+
status: res.code.to_i,
|
69
|
+
**request_info
|
70
|
+
}
|
71
|
+
)
|
72
|
+
Sentry.add_breadcrumb(crumb)
|
73
|
+
end
|
74
|
+
|
75
|
+
def from_sentry_sdk?
|
76
|
+
dsn = Sentry.configuration.dsn
|
77
|
+
dsn && dsn.host == self.address
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_request_info(req)
|
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}")
|
85
|
+
url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
|
86
|
+
|
87
|
+
result = { method: req.method, url: url }
|
88
|
+
|
89
|
+
if Sentry.configuration.send_default_pii
|
90
|
+
result[:query] = uri.query
|
91
|
+
result[:body] = req.body
|
92
|
+
end
|
93
|
+
|
94
|
+
result
|
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
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
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
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require "sentry/baggage"
|
5
|
+
|
6
|
+
module Sentry
|
7
|
+
class PropagationContext
|
8
|
+
SENTRY_TRACE_REGEXP = Regexp.new(
|
9
|
+
"^[ \t]*" + # whitespace
|
10
|
+
"([0-9a-f]{32})?" + # trace_id
|
11
|
+
"-?([0-9a-f]{16})?" + # span_id
|
12
|
+
"-?([01])?" + # sampled
|
13
|
+
"[ \t]*$" # whitespace
|
14
|
+
)
|
15
|
+
|
16
|
+
# An uuid that can be used to identify a trace.
|
17
|
+
# @return [String]
|
18
|
+
attr_reader :trace_id
|
19
|
+
# An uuid that can be used to identify the span.
|
20
|
+
# @return [String]
|
21
|
+
attr_reader :span_id
|
22
|
+
# Span parent's span_id.
|
23
|
+
# @return [String, nil]
|
24
|
+
attr_reader :parent_span_id
|
25
|
+
# The sampling decision of the parent transaction.
|
26
|
+
# @return [Boolean, nil]
|
27
|
+
attr_reader :parent_sampled
|
28
|
+
# Is there an incoming trace or not?
|
29
|
+
# @return [Boolean]
|
30
|
+
attr_reader :incoming_trace
|
31
|
+
# This is only for accessing the current baggage variable.
|
32
|
+
# Please use the #get_baggage method for interfacing outside this class.
|
33
|
+
# @return [Baggage, nil]
|
34
|
+
attr_reader :baggage
|
35
|
+
|
36
|
+
def initialize(scope, env = nil)
|
37
|
+
@scope = scope
|
38
|
+
@parent_span_id = nil
|
39
|
+
@parent_sampled = nil
|
40
|
+
@baggage = nil
|
41
|
+
@incoming_trace = false
|
42
|
+
|
43
|
+
if env
|
44
|
+
sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
|
45
|
+
baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME]
|
46
|
+
|
47
|
+
if sentry_trace_header
|
48
|
+
sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
|
49
|
+
|
50
|
+
if sentry_trace_data
|
51
|
+
@trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
|
52
|
+
|
53
|
+
@baggage = if baggage_header && !baggage_header.empty?
|
54
|
+
Baggage.from_incoming_header(baggage_header)
|
55
|
+
else
|
56
|
+
# If there's an incoming sentry-trace but no incoming baggage header,
|
57
|
+
# for instance in traces coming from older SDKs,
|
58
|
+
# baggage will be empty and frozen and won't be populated as head SDK.
|
59
|
+
Baggage.new({})
|
60
|
+
end
|
61
|
+
|
62
|
+
@baggage.freeze!
|
63
|
+
@incoming_trace = true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
@trace_id ||= SecureRandom.uuid.delete("-")
|
69
|
+
@span_id = SecureRandom.uuid.delete("-").slice(0, 16)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
|
73
|
+
#
|
74
|
+
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
|
75
|
+
# @return [Array, nil]
|
76
|
+
def self.extract_sentry_trace(sentry_trace)
|
77
|
+
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
|
78
|
+
return nil if match.nil?
|
79
|
+
|
80
|
+
trace_id, parent_span_id, sampled_flag = match[1..3]
|
81
|
+
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
|
82
|
+
|
83
|
+
[trace_id, parent_span_id, parent_sampled]
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the trace context that can be used to embed in an Event.
|
87
|
+
# @return [Hash]
|
88
|
+
def get_trace_context
|
89
|
+
{
|
90
|
+
trace_id: trace_id,
|
91
|
+
span_id: span_id,
|
92
|
+
parent_span_id: parent_span_id
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns the sentry-trace header from the propagation context.
|
97
|
+
# @return [String]
|
98
|
+
def get_traceparent
|
99
|
+
"#{trace_id}-#{span_id}"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns the Baggage from the propagation context or populates as head SDK if empty.
|
103
|
+
# @return [Baggage, nil]
|
104
|
+
def get_baggage
|
105
|
+
populate_head_baggage if @baggage.nil? || @baggage.mutable
|
106
|
+
@baggage
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns the Dynamic Sampling Context from the baggage.
|
110
|
+
# @return [String, nil]
|
111
|
+
def get_dynamic_sampling_context
|
112
|
+
get_baggage&.dynamic_sampling_context
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def populate_head_baggage
|
118
|
+
return unless Sentry.initialized?
|
119
|
+
|
120
|
+
configuration = Sentry.configuration
|
121
|
+
|
122
|
+
items = {
|
123
|
+
"trace_id" => trace_id,
|
124
|
+
"environment" => configuration.environment,
|
125
|
+
"release" => configuration.release,
|
126
|
+
"public_key" => configuration.dsn&.public_key,
|
127
|
+
"user_segment" => @scope.user && @scope.user["segment"]
|
128
|
+
}
|
129
|
+
|
130
|
+
items.compact!
|
131
|
+
@baggage = Baggage.new(items, mutable: false)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/sentry/puma.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
return unless defined?(Puma::Server)
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
module Puma
|
7
|
+
module Server
|
8
|
+
PUMA_4_AND_PRIOR = Gem::Version.new(::Puma::Const::PUMA_VERSION) < Gem::Version.new("5.0.0")
|
9
|
+
|
10
|
+
def lowlevel_error(e, env, status=500)
|
11
|
+
result =
|
12
|
+
if PUMA_4_AND_PRIOR
|
13
|
+
super(e, env)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
Sentry.capture_exception(e) do |scope|
|
20
|
+
scope.set_rack_env(env)
|
21
|
+
end
|
22
|
+
rescue
|
23
|
+
# if anything happens, we don't want to break the app
|
24
|
+
end
|
25
|
+
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Sentry.register_patch(:puma, Sentry::Puma::Server, Puma::Server)
|