sentry-ruby 5.8.0 → 5.12.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 +14 -2
- data/README.md +8 -8
- data/Rakefile +8 -1
- data/lib/sentry/backtrace.rb +1 -1
- data/lib/sentry/baggage.rb +1 -12
- data/lib/sentry/check_in_event.rb +60 -0
- data/lib/sentry/client.rb +40 -2
- data/lib/sentry/configuration.rb +76 -7
- data/lib/sentry/cron/monitor_check_ins.rb +61 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/envelope.rb +2 -5
- data/lib/sentry/event.rb +6 -0
- data/lib/sentry/hub.rb +78 -3
- data/lib/sentry/integrable.rb +6 -0
- data/lib/sentry/interfaces/single_exception.rb +4 -3
- data/lib/sentry/net/http.rb +21 -22
- data/lib/sentry/profiler.rb +233 -0
- data/lib/sentry/propagation_context.rb +134 -0
- data/lib/sentry/puma.rb +25 -0
- data/lib/sentry/rack/capture_exceptions.rb +1 -4
- data/lib/sentry/redis.rb +6 -7
- data/lib/sentry/scope.rb +23 -3
- data/lib/sentry/span.rb +39 -2
- data/lib/sentry/test_helper.rb +18 -12
- data/lib/sentry/transaction.rb +24 -17
- data/lib/sentry/transaction_event.rb +30 -3
- data/lib/sentry/transport.rb +11 -3
- data/lib/sentry/utils/argument_checking_helper.rb +9 -3
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +69 -2
- metadata +9 -3
- data/CODE_OF_CONDUCT.md +0 -74
data/lib/sentry/hub.rb
CHANGED
@@ -88,8 +88,10 @@ module Sentry
|
|
88
88
|
}
|
89
89
|
|
90
90
|
sampling_context.merge!(custom_sampling_context)
|
91
|
-
|
92
91
|
transaction.set_initial_sample_decision(sampling_context: sampling_context)
|
92
|
+
|
93
|
+
transaction.start_profiler!
|
94
|
+
|
93
95
|
transaction
|
94
96
|
end
|
95
97
|
|
@@ -114,7 +116,11 @@ module Sentry
|
|
114
116
|
end
|
115
117
|
|
116
118
|
def capture_exception(exception, **options, &block)
|
117
|
-
|
119
|
+
if RUBY_PLATFORM == "java"
|
120
|
+
check_argument_type!(exception, ::Exception, ::Java::JavaLang::Throwable)
|
121
|
+
else
|
122
|
+
check_argument_type!(exception, ::Exception)
|
123
|
+
end
|
118
124
|
|
119
125
|
return if Sentry.exception_captured?(exception)
|
120
126
|
|
@@ -122,6 +128,7 @@ module Sentry
|
|
122
128
|
|
123
129
|
options[:hint] ||= {}
|
124
130
|
options[:hint][:exception] = exception
|
131
|
+
|
125
132
|
event = current_client.event_from_exception(exception, options[:hint])
|
126
133
|
|
127
134
|
return unless event
|
@@ -149,6 +156,30 @@ module Sentry
|
|
149
156
|
capture_event(event, **options, &block)
|
150
157
|
end
|
151
158
|
|
159
|
+
def capture_check_in(slug, status, **options, &block)
|
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, &block)
|
180
|
+
event.check_in_id
|
181
|
+
end
|
182
|
+
|
152
183
|
def capture_event(event, **options, &block)
|
153
184
|
check_argument_type!(event, Sentry::Event)
|
154
185
|
|
@@ -171,7 +202,7 @@ module Sentry
|
|
171
202
|
configuration.log_debug(event.to_json_compatible)
|
172
203
|
end
|
173
204
|
|
174
|
-
@last_event_id = event&.event_id
|
205
|
+
@last_event_id = event&.event_id if event.is_a?(Sentry::ErrorEvent)
|
175
206
|
event
|
176
207
|
end
|
177
208
|
|
@@ -222,6 +253,50 @@ module Sentry
|
|
222
253
|
end_session
|
223
254
|
end
|
224
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
|
+
|
225
300
|
private
|
226
301
|
|
227
302
|
def current_layer
|
data/lib/sentry/integrable.rb
CHANGED
@@ -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
|
@@ -11,7 +11,8 @@ module Sentry
|
|
11
11
|
OMISSION_MARK = "...".freeze
|
12
12
|
MAX_LOCAL_BYTES = 1024
|
13
13
|
|
14
|
-
attr_reader :type, :
|
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
|
@@ -22,7 +23,7 @@ module Sentry
|
|
22
23
|
exception.message || ""
|
23
24
|
end
|
24
25
|
|
25
|
-
@value = exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
|
26
|
+
@value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES))
|
26
27
|
|
27
28
|
@module = exception.class.to_s.split('::')[0...-1].join('::')
|
28
29
|
@thread_id = Thread.current.object_id
|
@@ -50,7 +51,7 @@ module Sentry
|
|
50
51
|
v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK
|
51
52
|
end
|
52
53
|
|
53
|
-
v
|
54
|
+
Utils::EncodingHelper.encode_to_utf_8(v)
|
54
55
|
rescue StandardError
|
55
56
|
PROBLEMATIC_LOCAL_VALUE_REPLACEMENT
|
56
57
|
end
|
data/lib/sentry/net/http.rb
CHANGED
@@ -30,15 +30,21 @@ module Sentry
|
|
30
30
|
return super if from_sentry_sdk?
|
31
31
|
|
32
32
|
Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span|
|
33
|
-
|
33
|
+
request_info = extract_request_info(req)
|
34
|
+
|
35
|
+
if propagate_trace?(request_info[:url], Sentry.configuration)
|
36
|
+
set_propagation_headers(req)
|
37
|
+
end
|
34
38
|
|
35
39
|
super.tap do |res|
|
36
|
-
record_sentry_breadcrumb(
|
40
|
+
record_sentry_breadcrumb(request_info, res)
|
37
41
|
|
38
42
|
if sentry_span
|
39
|
-
request_info = extract_request_info(req)
|
40
43
|
sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
|
41
|
-
sentry_span.set_data(
|
44
|
+
sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
|
45
|
+
sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
|
46
|
+
sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
|
47
|
+
sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, res.code.to_i)
|
42
48
|
end
|
43
49
|
end
|
44
50
|
end
|
@@ -46,23 +52,13 @@ module Sentry
|
|
46
52
|
|
47
53
|
private
|
48
54
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
client = Sentry.get_current_client
|
53
|
-
|
54
|
-
trace = client.generate_sentry_trace(sentry_span)
|
55
|
-
req[SENTRY_TRACE_HEADER_NAME] = trace if trace
|
56
|
-
|
57
|
-
baggage = client.generate_baggage(sentry_span)
|
58
|
-
req[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty?
|
55
|
+
def set_propagation_headers(req)
|
56
|
+
Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
|
59
57
|
end
|
60
58
|
|
61
|
-
def record_sentry_breadcrumb(
|
59
|
+
def record_sentry_breadcrumb(request_info, res)
|
62
60
|
return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
|
63
61
|
|
64
|
-
request_info = extract_request_info(req)
|
65
|
-
|
66
62
|
crumb = Sentry::Breadcrumb.new(
|
67
63
|
level: :info,
|
68
64
|
category: BREADCRUMB_CATEGORY,
|
@@ -87,17 +83,20 @@ module Sentry
|
|
87
83
|
result = { method: req.method, url: url }
|
88
84
|
|
89
85
|
if Sentry.configuration.send_default_pii
|
90
|
-
result[:
|
86
|
+
result[:query] = uri.query
|
91
87
|
result[:body] = req.body
|
92
88
|
end
|
93
89
|
|
94
90
|
result
|
95
91
|
end
|
92
|
+
|
93
|
+
def propagate_trace?(url, configuration)
|
94
|
+
url &&
|
95
|
+
configuration.propagate_traces &&
|
96
|
+
configuration.trace_propagation_targets.any? { |target| url.match?(target) }
|
97
|
+
end
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
99
101
|
|
100
|
-
Sentry.register_patch
|
101
|
-
patch = Sentry::Net::HTTP
|
102
|
-
Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
|
103
|
-
end
|
102
|
+
Sentry.register_patch(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]
|
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,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Puma
|
5
|
+
module Server
|
6
|
+
def lowlevel_error(e, env, status=500)
|
7
|
+
result = super
|
8
|
+
|
9
|
+
begin
|
10
|
+
Sentry.capture_exception(e) do |scope|
|
11
|
+
scope.set_rack_env(env)
|
12
|
+
end
|
13
|
+
rescue
|
14
|
+
# if anything happens, we don't want to break the app
|
15
|
+
end
|
16
|
+
|
17
|
+
result
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if defined?(Puma::Server)
|
24
|
+
Sentry.register_patch(Sentry::Puma::Server, Puma::Server)
|
25
|
+
end
|
@@ -62,11 +62,8 @@ module Sentry
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def start_transaction(env, scope)
|
65
|
-
sentry_trace = env["HTTP_SENTRY_TRACE"]
|
66
|
-
baggage = env["HTTP_BAGGAGE"]
|
67
|
-
|
68
65
|
options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
|
69
|
-
transaction = Sentry
|
66
|
+
transaction = Sentry.continue_trace(env, **options)
|
70
67
|
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
|
71
68
|
end
|
72
69
|
|
data/lib/sentry/redis.rb
CHANGED
@@ -19,7 +19,10 @@ module Sentry
|
|
19
19
|
|
20
20
|
if span
|
21
21
|
span.set_description(commands_description)
|
22
|
-
span.set_data(
|
22
|
+
span.set_data(Span::DataConventions::DB_SYSTEM, "redis")
|
23
|
+
span.set_data(Span::DataConventions::DB_NAME, db)
|
24
|
+
span.set_data(Span::DataConventions::SERVER_ADDRESS, host)
|
25
|
+
span.set_data(Span::DataConventions::SERVER_PORT, port)
|
23
26
|
end
|
24
27
|
end
|
25
28
|
end
|
@@ -30,6 +33,7 @@ module Sentry
|
|
30
33
|
attr_reader :commands, :host, :port, :db
|
31
34
|
|
32
35
|
def record_breadcrumb
|
36
|
+
return unless Sentry.initialized?
|
33
37
|
return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
|
34
38
|
|
35
39
|
Sentry.add_breadcrumb(
|
@@ -95,12 +99,7 @@ end
|
|
95
99
|
|
96
100
|
if defined?(::Redis::Client)
|
97
101
|
if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0")
|
98
|
-
Sentry.register_patch
|
99
|
-
patch = Sentry::Redis::OldClientPatch
|
100
|
-
unless Redis::Client.ancestors.include?(patch)
|
101
|
-
Redis::Client.prepend(patch)
|
102
|
-
end
|
103
|
-
end
|
102
|
+
Sentry.register_patch(Sentry::Redis::OldClientPatch, ::Redis::Client)
|
104
103
|
elsif defined?(RedisClient)
|
105
104
|
RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
|
106
105
|
end
|