sentry-ruby 5.5.0 → 5.10.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/.rspec +0 -1
- data/Gemfile +14 -5
- data/README.md +3 -0
- data/Rakefile +8 -1
- data/lib/sentry/backtrace.rb +1 -1
- data/lib/sentry/baggage.rb +1 -12
- data/lib/sentry/client.rb +14 -1
- data/lib/sentry/configuration.rb +128 -26
- data/lib/sentry/envelope.rb +1 -4
- data/lib/sentry/hub.rb +31 -3
- data/lib/sentry/interfaces/request.rb +2 -14
- data/lib/sentry/interfaces/single_exception.rb +9 -1
- data/lib/sentry/net/http.rb +20 -37
- data/lib/sentry/profiler.rb +222 -0
- data/lib/sentry/puma.rb +25 -0
- data/lib/sentry/rack/capture_exceptions.rb +1 -1
- data/lib/sentry/redis.rb +36 -23
- data/lib/sentry/scope.rb +26 -3
- data/lib/sentry/span.rb +14 -11
- data/lib/sentry/test_helper.rb +1 -1
- data/lib/sentry/transaction.rb +73 -15
- data/lib/sentry/transaction_event.rb +36 -0
- data/lib/sentry/transport.rb +7 -0
- data/lib/sentry/utils/argument_checking_helper.rb +3 -3
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +35 -20
- metadata +5 -3
- data/CODE_OF_CONDUCT.md +0 -74
@@ -0,0 +1,222 @@
|
|
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
|
+
|
13
|
+
attr_reader :sampled, :started, :event_id
|
14
|
+
|
15
|
+
def initialize(configuration)
|
16
|
+
@event_id = SecureRandom.uuid.delete('-')
|
17
|
+
@started = false
|
18
|
+
@sampled = nil
|
19
|
+
|
20
|
+
@profiling_enabled = defined?(StackProf) && 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 || Backtrace::APP_DIRS_PATTERN
|
24
|
+
@in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def start
|
28
|
+
return unless @sampled
|
29
|
+
|
30
|
+
@started = StackProf.start(interval: DEFAULT_INTERVAL,
|
31
|
+
mode: :wall,
|
32
|
+
raw: true,
|
33
|
+
aggregate: false)
|
34
|
+
|
35
|
+
@started ? log('Started') : log('Not started since running elsewhere')
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop
|
39
|
+
return unless @sampled
|
40
|
+
return unless @started
|
41
|
+
|
42
|
+
StackProf.stop
|
43
|
+
log('Stopped')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets initial sampling decision of the profile.
|
47
|
+
# @return [void]
|
48
|
+
def set_initial_sample_decision(transaction_sampled)
|
49
|
+
unless @profiling_enabled
|
50
|
+
@sampled = false
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
unless transaction_sampled
|
55
|
+
@sampled = false
|
56
|
+
log('Discarding profile because transaction not sampled')
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
case @profiles_sample_rate
|
61
|
+
when 0.0
|
62
|
+
@sampled = false
|
63
|
+
log('Discarding profile because sample_rate is 0')
|
64
|
+
return
|
65
|
+
when 1.0
|
66
|
+
@sampled = true
|
67
|
+
return
|
68
|
+
else
|
69
|
+
@sampled = Random.rand < @profiles_sample_rate
|
70
|
+
end
|
71
|
+
|
72
|
+
log('Discarding profile due to sampling decision') unless @sampled
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_hash
|
76
|
+
return {} unless @sampled
|
77
|
+
return {} unless @started
|
78
|
+
|
79
|
+
results = StackProf.results
|
80
|
+
return {} unless results
|
81
|
+
return {} if results.empty?
|
82
|
+
return {} if results[:samples] == 0
|
83
|
+
return {} unless results[:raw]
|
84
|
+
|
85
|
+
frame_map = {}
|
86
|
+
|
87
|
+
frames = results[:frames].to_enum.with_index.map do |frame, idx|
|
88
|
+
frame_id, frame_data = frame
|
89
|
+
|
90
|
+
# need to map over stackprof frame ids to ours
|
91
|
+
frame_map[frame_id] = idx
|
92
|
+
|
93
|
+
file_path = frame_data[:file]
|
94
|
+
in_app = in_app?(file_path)
|
95
|
+
filename = compute_filename(file_path, in_app)
|
96
|
+
function, mod = split_module(frame_data[:name])
|
97
|
+
|
98
|
+
frame_hash = {
|
99
|
+
abs_path: file_path,
|
100
|
+
function: function,
|
101
|
+
filename: filename,
|
102
|
+
in_app: in_app
|
103
|
+
}
|
104
|
+
|
105
|
+
frame_hash[:module] = mod if mod
|
106
|
+
frame_hash[:lineno] = frame_data[:line] if frame_data[:line]
|
107
|
+
|
108
|
+
frame_hash
|
109
|
+
end
|
110
|
+
|
111
|
+
idx = 0
|
112
|
+
stacks = []
|
113
|
+
num_seen = []
|
114
|
+
|
115
|
+
# extract stacks from raw
|
116
|
+
# raw is a single array of [.., len_stack, *stack_frames(len_stack), num_stack_seen , ..]
|
117
|
+
while (len = results[:raw][idx])
|
118
|
+
idx += 1
|
119
|
+
|
120
|
+
# our call graph is reversed
|
121
|
+
stack = results[:raw].slice(idx, len).map { |id| frame_map[id] }.compact.reverse
|
122
|
+
stacks << stack
|
123
|
+
|
124
|
+
num_seen << results[:raw][idx + len]
|
125
|
+
idx += len + 1
|
126
|
+
|
127
|
+
log('Unknown frame in stack') if stack.size != len
|
128
|
+
end
|
129
|
+
|
130
|
+
idx = 0
|
131
|
+
elapsed_since_start_ns = 0
|
132
|
+
samples = []
|
133
|
+
|
134
|
+
num_seen.each_with_index do |n, i|
|
135
|
+
n.times do
|
136
|
+
# stackprof deltas are in microseconds
|
137
|
+
delta = results[:raw_timestamp_deltas][idx]
|
138
|
+
elapsed_since_start_ns += (delta * MICRO_TO_NANO_SECONDS).to_i
|
139
|
+
idx += 1
|
140
|
+
|
141
|
+
# Not sure why but some deltas are very small like 0/1 values,
|
142
|
+
# they pollute our flamegraph so just ignore them for now.
|
143
|
+
# Open issue at https://github.com/tmm1/stackprof/issues/201
|
144
|
+
next if delta < 10
|
145
|
+
|
146
|
+
samples << {
|
147
|
+
stack_id: i,
|
148
|
+
# TODO-neel-profiler we need to patch rb_profile_frames and write our own C extension to enable threading info.
|
149
|
+
# Till then, on multi-threaded servers like puma, we will get frames from other active threads when the one
|
150
|
+
# we're profiling is idle/sleeping/waiting for IO etc.
|
151
|
+
# https://bugs.ruby-lang.org/issues/10602
|
152
|
+
thread_id: '0',
|
153
|
+
elapsed_since_start_ns: elapsed_since_start_ns.to_s
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
log('Some samples thrown away') if samples.size != results[:samples]
|
159
|
+
|
160
|
+
if samples.size <= 2
|
161
|
+
log('Not enough samples, discarding profiler')
|
162
|
+
return {}
|
163
|
+
end
|
164
|
+
|
165
|
+
profile = {
|
166
|
+
frames: frames,
|
167
|
+
stacks: stacks,
|
168
|
+
samples: samples
|
169
|
+
}
|
170
|
+
|
171
|
+
{
|
172
|
+
event_id: @event_id,
|
173
|
+
platform: PLATFORM,
|
174
|
+
version: VERSION,
|
175
|
+
profile: profile
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def log(message)
|
182
|
+
Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
|
183
|
+
end
|
184
|
+
|
185
|
+
def in_app?(abs_path)
|
186
|
+
abs_path.match?(@in_app_pattern)
|
187
|
+
end
|
188
|
+
|
189
|
+
# copied from stacktrace.rb since I don't want to touch existing code
|
190
|
+
# TODO-neel-profiler try to fetch this from stackprof once we patch
|
191
|
+
# the native extension
|
192
|
+
def compute_filename(abs_path, in_app)
|
193
|
+
return nil if abs_path.nil?
|
194
|
+
|
195
|
+
under_project_root = @project_root && abs_path.start_with?(@project_root)
|
196
|
+
|
197
|
+
prefix =
|
198
|
+
if under_project_root && in_app
|
199
|
+
@project_root
|
200
|
+
else
|
201
|
+
longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
|
202
|
+
|
203
|
+
if under_project_root
|
204
|
+
longest_load_path || @project_root
|
205
|
+
else
|
206
|
+
longest_load_path
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
|
211
|
+
end
|
212
|
+
|
213
|
+
def split_module(name)
|
214
|
+
# last module plus class/instance method
|
215
|
+
i = name.rindex('::')
|
216
|
+
function = i ? name[(i + 2)..-1] : name
|
217
|
+
mod = i ? name[0...i] : nil
|
218
|
+
|
219
|
+
[function, mod]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
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
|
data/lib/sentry/redis.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Sentry
|
4
4
|
# @api private
|
5
5
|
class Redis
|
6
|
-
OP_NAME = "db.redis
|
6
|
+
OP_NAME = "db.redis"
|
7
7
|
LOGGER_NAME = :redis_logger
|
8
8
|
|
9
9
|
def initialize(commands, host, port, db)
|
@@ -13,9 +13,14 @@ module Sentry
|
|
13
13
|
def instrument
|
14
14
|
return yield unless Sentry.initialized?
|
15
15
|
|
16
|
-
|
16
|
+
Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |span|
|
17
17
|
yield.tap do
|
18
18
|
record_breadcrumb
|
19
|
+
|
20
|
+
if span
|
21
|
+
span.set_description(commands_description)
|
22
|
+
span.set_data(:server, server_description)
|
23
|
+
end
|
19
24
|
end
|
20
25
|
end
|
21
26
|
end
|
@@ -24,19 +29,8 @@ module Sentry
|
|
24
29
|
|
25
30
|
attr_reader :commands, :host, :port, :db
|
26
31
|
|
27
|
-
def record_span
|
28
|
-
return yield unless (transaction = Sentry.get_current_scope.get_transaction) && transaction.sampled
|
29
|
-
|
30
|
-
sentry_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
|
31
|
-
|
32
|
-
yield.tap do
|
33
|
-
sentry_span.set_description(commands_description)
|
34
|
-
sentry_span.set_data(:server, server_description)
|
35
|
-
sentry_span.set_timestamp(Sentry.utc_now.to_f)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
32
|
def record_breadcrumb
|
33
|
+
return unless Sentry.initialized?
|
40
34
|
return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
|
41
35
|
|
42
36
|
Sentry.add_breadcrumb(
|
@@ -61,10 +55,16 @@ module Sentry
|
|
61
55
|
def parsed_commands
|
62
56
|
commands.map do |statement|
|
63
57
|
command, key, *arguments = statement
|
58
|
+
command_set = { command: command.to_s.upcase }
|
59
|
+
command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key)
|
64
60
|
|
65
|
-
|
66
|
-
command_set[:arguments] = arguments
|
61
|
+
if Sentry.configuration.send_default_pii
|
62
|
+
command_set[:arguments] = arguments
|
63
|
+
.select { |a| Utils::EncodingHelper.valid_utf_8?(a) }
|
64
|
+
.join(" ")
|
67
65
|
end
|
66
|
+
|
67
|
+
command_set
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
@@ -72,19 +72,32 @@ module Sentry
|
|
72
72
|
"#{host}:#{port}/#{db}"
|
73
73
|
end
|
74
74
|
|
75
|
-
module
|
75
|
+
module OldClientPatch
|
76
76
|
def logging(commands, &block)
|
77
|
-
Sentry::Redis.new(commands, host, port, db).instrument
|
78
|
-
|
79
|
-
|
77
|
+
Sentry::Redis.new(commands, host, port, db).instrument { super }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module GlobalRedisInstrumentation
|
82
|
+
def call(command, redis_config)
|
83
|
+
Sentry::Redis
|
84
|
+
.new([command], redis_config.host, redis_config.port, redis_config.db)
|
85
|
+
.instrument { super }
|
86
|
+
end
|
87
|
+
|
88
|
+
def call_pipelined(commands, redis_config)
|
89
|
+
Sentry::Redis
|
90
|
+
.new(commands, redis_config.host, redis_config.port, redis_config.db)
|
91
|
+
.instrument { super }
|
80
92
|
end
|
81
93
|
end
|
82
94
|
end
|
83
95
|
end
|
84
96
|
|
85
97
|
if defined?(::Redis::Client)
|
86
|
-
|
87
|
-
|
88
|
-
|
98
|
+
if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0")
|
99
|
+
Sentry.register_patch(Sentry::Redis::OldClientPatch, ::Redis::Client)
|
100
|
+
elsif defined?(RedisClient)
|
101
|
+
RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
|
89
102
|
end
|
90
103
|
end
|
data/lib/sentry/scope.rb
CHANGED
@@ -58,8 +58,10 @@ module Sentry
|
|
58
58
|
event.breadcrumbs = breadcrumbs
|
59
59
|
event.rack_env = rack_env if rack_env
|
60
60
|
|
61
|
-
|
62
|
-
|
61
|
+
all_event_processors = self.class.global_event_processors + @event_processors
|
62
|
+
|
63
|
+
unless all_event_processors.empty?
|
64
|
+
all_event_processors.each do |processor_block|
|
63
65
|
event = processor_block.call(event, hint)
|
64
66
|
end
|
65
67
|
end
|
@@ -190,6 +192,10 @@ module Sentry
|
|
190
192
|
# @return [Hash]
|
191
193
|
def set_contexts(contexts_hash)
|
192
194
|
check_argument_type!(contexts_hash, Hash)
|
195
|
+
contexts_hash.values.each do |val|
|
196
|
+
check_argument_type!(val, Hash)
|
197
|
+
end
|
198
|
+
|
193
199
|
@contexts.merge!(contexts_hash) do |key, old, new|
|
194
200
|
old.merge(new)
|
195
201
|
end
|
@@ -303,7 +309,8 @@ module Sentry
|
|
303
309
|
name: uname[:sysname] || RbConfig::CONFIG["host_os"],
|
304
310
|
version: uname[:version],
|
305
311
|
build: uname[:release],
|
306
|
-
kernel_version: uname[:version]
|
312
|
+
kernel_version: uname[:version],
|
313
|
+
machine: uname[:machine]
|
307
314
|
}
|
308
315
|
end
|
309
316
|
end
|
@@ -315,6 +322,22 @@ module Sentry
|
|
315
322
|
version: RUBY_DESCRIPTION || Sentry.sys_command("ruby -v")
|
316
323
|
}
|
317
324
|
end
|
325
|
+
|
326
|
+
# Returns the global event processors array.
|
327
|
+
# @return [Array<Proc>]
|
328
|
+
def global_event_processors
|
329
|
+
@global_event_processors ||= []
|
330
|
+
end
|
331
|
+
|
332
|
+
# Adds a new global event processor [Proc].
|
333
|
+
# Sometimes we need a global event processor without needing to configure scope.
|
334
|
+
# These run before scope event processors.
|
335
|
+
#
|
336
|
+
# @param block [Proc]
|
337
|
+
# @return [void]
|
338
|
+
def add_global_event_processor(&block)
|
339
|
+
global_event_processors << block
|
340
|
+
end
|
318
341
|
end
|
319
342
|
|
320
343
|
end
|
data/lib/sentry/span.rb
CHANGED
@@ -60,25 +60,28 @@ module Sentry
|
|
60
60
|
# The Transaction object the Span belongs to.
|
61
61
|
# Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction.
|
62
62
|
# @return [Transaction]
|
63
|
-
|
63
|
+
attr_reader :transaction
|
64
64
|
|
65
65
|
def initialize(
|
66
|
+
transaction:,
|
66
67
|
description: nil,
|
67
68
|
op: nil,
|
68
69
|
status: nil,
|
69
70
|
trace_id: nil,
|
71
|
+
span_id: nil,
|
70
72
|
parent_span_id: nil,
|
71
73
|
sampled: nil,
|
72
74
|
start_timestamp: nil,
|
73
75
|
timestamp: nil
|
74
76
|
)
|
75
77
|
@trace_id = trace_id || SecureRandom.uuid.delete("-")
|
76
|
-
@span_id = SecureRandom.hex(8)
|
78
|
+
@span_id = span_id || SecureRandom.hex(8)
|
77
79
|
@parent_span_id = parent_span_id
|
78
80
|
@sampled = sampled
|
79
81
|
@start_timestamp = start_timestamp || Sentry.utc_now.to_f
|
80
82
|
@timestamp = timestamp
|
81
83
|
@description = description
|
84
|
+
@transaction = transaction
|
82
85
|
@op = op
|
83
86
|
@status = status
|
84
87
|
@data = {}
|
@@ -87,11 +90,8 @@ module Sentry
|
|
87
90
|
|
88
91
|
# Finishes the span by adding a timestamp.
|
89
92
|
# @return [self]
|
90
|
-
def finish
|
91
|
-
|
92
|
-
return if @timestamp
|
93
|
-
|
94
|
-
@timestamp = Sentry.utc_now.to_f
|
93
|
+
def finish(end_timestamp: nil)
|
94
|
+
@timestamp = end_timestamp || @timestamp || Sentry.utc_now.to_f
|
95
95
|
self
|
96
96
|
end
|
97
97
|
|
@@ -105,10 +105,10 @@ module Sentry
|
|
105
105
|
end
|
106
106
|
|
107
107
|
# Generates a W3C Baggage header string for distributed tracing
|
108
|
-
# from the incoming baggage stored on the
|
108
|
+
# from the incoming baggage stored on the transaction.
|
109
109
|
# @return [String, nil]
|
110
110
|
def to_baggage
|
111
|
-
transaction
|
111
|
+
transaction.get_baggage&.serialize
|
112
112
|
end
|
113
113
|
|
114
114
|
# @return [Hash]
|
@@ -143,9 +143,8 @@ module Sentry
|
|
143
143
|
# Starts a child span with given attributes.
|
144
144
|
# @param attributes [Hash] the attributes for the child span.
|
145
145
|
def start_child(**attributes)
|
146
|
-
attributes = attributes.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
|
146
|
+
attributes = attributes.dup.merge(transaction: @transaction, trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
|
147
147
|
new_span = Span.new(**attributes)
|
148
|
-
new_span.transaction = transaction
|
149
148
|
new_span.span_recorder = span_recorder
|
150
149
|
|
151
150
|
if span_recorder
|
@@ -170,6 +169,10 @@ module Sentry
|
|
170
169
|
yield(child_span)
|
171
170
|
|
172
171
|
child_span.finish
|
172
|
+
rescue
|
173
|
+
child_span.set_http_status(500)
|
174
|
+
child_span.finish
|
175
|
+
raise
|
173
176
|
end
|
174
177
|
|
175
178
|
def deep_dup
|
data/lib/sentry/test_helper.rb
CHANGED
@@ -25,7 +25,7 @@ module Sentry
|
|
25
25
|
copied_config.background_worker_threads = 0
|
26
26
|
|
27
27
|
# user can overwrite some of the configs, with a few exceptions like:
|
28
|
-
# -
|
28
|
+
# - include_local_variables
|
29
29
|
# - auto_session_tracking
|
30
30
|
block&.call(copied_config)
|
31
31
|
|
data/lib/sentry/transaction.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "sentry/baggage"
|
4
|
+
require "sentry/profiler"
|
4
5
|
|
5
6
|
module Sentry
|
6
7
|
class Transaction < Span
|
@@ -37,6 +38,10 @@ module Sentry
|
|
37
38
|
# @return [Baggage, nil]
|
38
39
|
attr_reader :baggage
|
39
40
|
|
41
|
+
# The measurements added to the transaction.
|
42
|
+
# @return [Hash]
|
43
|
+
attr_reader :measurements
|
44
|
+
|
40
45
|
# @deprecated Use Sentry.get_current_hub instead.
|
41
46
|
attr_reader :hub
|
42
47
|
|
@@ -50,6 +55,14 @@ module Sentry
|
|
50
55
|
# @return [Float, nil]
|
51
56
|
attr_reader :effective_sample_rate
|
52
57
|
|
58
|
+
# Additional contexts stored directly on the transaction object.
|
59
|
+
# @return [Hash]
|
60
|
+
attr_reader :contexts
|
61
|
+
|
62
|
+
# The Profiler instance for this transaction.
|
63
|
+
# @return [Profiler]
|
64
|
+
attr_reader :profiler
|
65
|
+
|
53
66
|
def initialize(
|
54
67
|
hub:,
|
55
68
|
name: nil,
|
@@ -58,12 +71,10 @@ module Sentry
|
|
58
71
|
baggage: nil,
|
59
72
|
**options
|
60
73
|
)
|
61
|
-
super(**options)
|
74
|
+
super(transaction: self, **options)
|
62
75
|
|
63
|
-
|
64
|
-
@source = SOURCES.include?(source) ? source.to_sym : :custom
|
76
|
+
set_name(name, source: source)
|
65
77
|
@parent_sampled = parent_sampled
|
66
|
-
@transaction = self
|
67
78
|
@hub = hub
|
68
79
|
@baggage = baggage
|
69
80
|
@configuration = hub.configuration # to be removed
|
@@ -75,6 +86,9 @@ module Sentry
|
|
75
86
|
@environment = hub.configuration.environment
|
76
87
|
@dsn = hub.configuration.dsn
|
77
88
|
@effective_sample_rate = nil
|
89
|
+
@contexts = {}
|
90
|
+
@measurements = {}
|
91
|
+
@profiler = Profiler.new(@configuration)
|
78
92
|
init_span_recorder
|
79
93
|
end
|
80
94
|
|
@@ -92,16 +106,10 @@ module Sentry
|
|
92
106
|
return unless hub.configuration.tracing_enabled?
|
93
107
|
return unless sentry_trace
|
94
108
|
|
95
|
-
|
96
|
-
return
|
97
|
-
trace_id, parent_span_id, sampled_flag = match[1..3]
|
109
|
+
sentry_trace_data = extract_sentry_trace(sentry_trace)
|
110
|
+
return unless sentry_trace_data
|
98
111
|
|
99
|
-
parent_sampled =
|
100
|
-
if sampled_flag.nil?
|
101
|
-
nil
|
102
|
-
else
|
103
|
-
sampled_flag != "0"
|
104
|
-
end
|
112
|
+
trace_id, parent_span_id, parent_sampled = sentry_trace_data
|
105
113
|
|
106
114
|
baggage = if baggage && !baggage.empty?
|
107
115
|
Baggage.from_incoming_header(baggage)
|
@@ -124,6 +132,20 @@ module Sentry
|
|
124
132
|
)
|
125
133
|
end
|
126
134
|
|
135
|
+
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
|
136
|
+
#
|
137
|
+
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
|
138
|
+
# @return [Array, nil]
|
139
|
+
def self.extract_sentry_trace(sentry_trace)
|
140
|
+
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
|
141
|
+
return nil if match.nil?
|
142
|
+
|
143
|
+
trace_id, parent_span_id, sampled_flag = match[1..3]
|
144
|
+
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
|
145
|
+
|
146
|
+
[trace_id, parent_span_id, parent_sampled]
|
147
|
+
end
|
148
|
+
|
127
149
|
# @return [Hash]
|
128
150
|
def to_hash
|
129
151
|
hash = super
|
@@ -152,6 +174,15 @@ module Sentry
|
|
152
174
|
copy
|
153
175
|
end
|
154
176
|
|
177
|
+
# Sets a custom measurement on the transaction.
|
178
|
+
# @param name [String] name of the measurement
|
179
|
+
# @param value [Float] value of the measurement
|
180
|
+
# @param unit [String] unit of the measurement
|
181
|
+
# @return [void]
|
182
|
+
def set_measurement(name, value, unit = "")
|
183
|
+
@measurements[name] = { value: value, unit: unit }
|
184
|
+
end
|
185
|
+
|
155
186
|
# Sets initial sampling decision of the transaction.
|
156
187
|
# @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
|
157
188
|
# @return [void]
|
@@ -211,7 +242,7 @@ module Sentry
|
|
211
242
|
# Finishes the transaction's recording and send it to Sentry.
|
212
243
|
# @param hub [Hub] the hub that'll send this transaction. (Deprecated)
|
213
244
|
# @return [TransactionEvent]
|
214
|
-
def finish(hub: nil)
|
245
|
+
def finish(hub: nil, end_timestamp: nil)
|
215
246
|
if hub
|
216
247
|
log_warn(
|
217
248
|
<<~MSG
|
@@ -223,12 +254,14 @@ module Sentry
|
|
223
254
|
|
224
255
|
hub ||= @hub
|
225
256
|
|
226
|
-
super()
|
257
|
+
super(end_timestamp: end_timestamp)
|
227
258
|
|
228
259
|
if @name.nil?
|
229
260
|
@name = UNLABELD_NAME
|
230
261
|
end
|
231
262
|
|
263
|
+
@profiler.stop
|
264
|
+
|
232
265
|
if @sampled
|
233
266
|
event = hub.current_client.event_from_transaction(self)
|
234
267
|
hub.capture_event(event)
|
@@ -245,6 +278,31 @@ module Sentry
|
|
245
278
|
@baggage
|
246
279
|
end
|
247
280
|
|
281
|
+
# Set the transaction name directly.
|
282
|
+
# Considered internal api since it bypasses the usual scope logic.
|
283
|
+
# @param name [String]
|
284
|
+
# @param source [Symbol]
|
285
|
+
# @return [void]
|
286
|
+
def set_name(name, source: :custom)
|
287
|
+
@name = name
|
288
|
+
@source = SOURCES.include?(source) ? source.to_sym : :custom
|
289
|
+
end
|
290
|
+
|
291
|
+
# Set contexts directly on the transaction.
|
292
|
+
# @param key [String, Symbol]
|
293
|
+
# @param value [Object]
|
294
|
+
# @return [void]
|
295
|
+
def set_context(key, value)
|
296
|
+
@contexts[key] = value
|
297
|
+
end
|
298
|
+
|
299
|
+
# Start the profiler.
|
300
|
+
# @return [void]
|
301
|
+
def start_profiler!
|
302
|
+
profiler.set_initial_sample_decision(sampled)
|
303
|
+
profiler.start
|
304
|
+
end
|
305
|
+
|
248
306
|
protected
|
249
307
|
|
250
308
|
def init_span_recorder(limit = 1000)
|