sentry-ruby 5.7.0 → 5.9.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 +9 -3
- 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 +85 -20
- data/lib/sentry/envelope.rb +1 -4
- data/lib/sentry/hub.rb +4 -1
- data/lib/sentry/net/http.rb +1 -4
- data/lib/sentry/profiler.rb +222 -0
- data/lib/sentry/puma.rb +25 -0
- data/lib/sentry/redis.rb +22 -8
- data/lib/sentry/scope.rb +26 -3
- data/lib/sentry/span.rb +4 -0
- data/lib/sentry/test_helper.rb +1 -1
- data/lib/sentry/transaction.rb +29 -0
- data/lib/sentry/transaction_event.rb +35 -0
- data/lib/sentry/transport.rb +7 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +32 -4
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a114a391fe058601b40369376152bee8ba9b1b96ab94710344283cfe1a4490b
|
|
4
|
+
data.tar.gz: 7a17648c7a5d06f22d2d643f6ff1cb25d3fed2348609e372fc5762043ebd538a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dab19469a8e68201380e2be405332bb7c8caef7bf46fd4dae70f4dc3beaac2902ce51b2b2c902b39ca048e1bc5ff47f85b1e7d3a824b5f20ff002dcbdca2c354
|
|
7
|
+
data.tar.gz: 6bf0cba7b42dd9bfd5078e4353ad7f92d932b959a87c748cb30e505ffd8d205899c819ea4f72e5f3099f0b861139864062b870219678abdeda0a0629604963d3
|
data/.rspec
CHANGED
data/Gemfile
CHANGED
|
@@ -7,14 +7,19 @@ rack_version = ENV["RACK_VERSION"]
|
|
|
7
7
|
rack_version = "3.0.0" if rack_version.nil?
|
|
8
8
|
gem "rack", "~> #{Gem::Version.new(rack_version)}" unless rack_version == "0"
|
|
9
9
|
|
|
10
|
+
redis_rb_version = ENV.fetch("REDIS_RB_VERSION", "5.0")
|
|
11
|
+
gem "redis", "~> #{redis_rb_version}"
|
|
12
|
+
|
|
13
|
+
gem "puma"
|
|
14
|
+
|
|
10
15
|
gem "rake", "~> 12.0"
|
|
11
16
|
gem "rspec", "~> 3.0"
|
|
12
17
|
gem "rspec-retry"
|
|
13
|
-
gem "fakeredis"
|
|
14
18
|
gem "timecop"
|
|
15
|
-
gem
|
|
19
|
+
gem "simplecov"
|
|
16
20
|
gem "simplecov-cobertura", "~> 1.4"
|
|
17
21
|
gem "rexml"
|
|
22
|
+
gem "stackprof" unless RUBY_PLATFORM == "java"
|
|
18
23
|
|
|
19
24
|
gem "object_tracer"
|
|
20
25
|
gem "debug", github: "ruby/debug", platform: :ruby if RUBY_VERSION.to_f >= 2.6
|
|
@@ -25,4 +30,5 @@ gem "benchmark_driver"
|
|
|
25
30
|
gem "benchmark-ipsa"
|
|
26
31
|
gem "benchmark-memory"
|
|
27
32
|
|
|
28
|
-
gem "yard",
|
|
33
|
+
gem "yard", github: "lsegal/yard"
|
|
34
|
+
gem "webrick"
|
data/Rakefile
CHANGED
|
@@ -8,6 +8,13 @@ require "rspec/core/rake_task"
|
|
|
8
8
|
|
|
9
9
|
RSpec::Core::RakeTask.new(:spec).tap do |task|
|
|
10
10
|
task.rspec_opts = "--order rand"
|
|
11
|
+
task.exclude_pattern = "spec/isolated/**/*_spec.rb"
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
task :
|
|
14
|
+
task :isolated_specs do
|
|
15
|
+
Dir["spec/isolated/*"].each do |file|
|
|
16
|
+
sh "bundle exec rspec #{file}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
task :default => [:spec, :isolated_specs]
|
data/lib/sentry/backtrace.rb
CHANGED
data/lib/sentry/baggage.rb
CHANGED
|
@@ -8,17 +8,6 @@ module Sentry
|
|
|
8
8
|
SENTRY_PREFIX = 'sentry-'
|
|
9
9
|
SENTRY_PREFIX_REGEX = /^sentry-/.freeze
|
|
10
10
|
|
|
11
|
-
DSC_KEYS = %w(
|
|
12
|
-
trace_id
|
|
13
|
-
public_key
|
|
14
|
-
sample_rate
|
|
15
|
-
release
|
|
16
|
-
environment
|
|
17
|
-
transaction
|
|
18
|
-
user_id
|
|
19
|
-
user_segment
|
|
20
|
-
).freeze
|
|
21
|
-
|
|
22
11
|
# @return [Hash]
|
|
23
12
|
attr_reader :items
|
|
24
13
|
|
|
@@ -68,7 +57,7 @@ module Sentry
|
|
|
68
57
|
# hash to be used in the trace envelope header.
|
|
69
58
|
# @return [Hash]
|
|
70
59
|
def dynamic_sampling_context
|
|
71
|
-
@items
|
|
60
|
+
@items
|
|
72
61
|
end
|
|
73
62
|
|
|
74
63
|
# Serialize the Baggage object back to a string.
|
data/lib/sentry/client.rb
CHANGED
|
@@ -76,7 +76,10 @@ module Sentry
|
|
|
76
76
|
# @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
|
|
77
77
|
# @return [Event, nil]
|
|
78
78
|
def event_from_exception(exception, hint = {})
|
|
79
|
-
return unless @configuration.sending_allowed?
|
|
79
|
+
return unless @configuration.sending_allowed?
|
|
80
|
+
|
|
81
|
+
ignore_exclusions = hint.delete(:ignore_exclusions) { false }
|
|
82
|
+
return if !ignore_exclusions && !@configuration.exception_class_allowed?(exception)
|
|
80
83
|
|
|
81
84
|
integration_meta = Sentry.integrations[hint[:integration]]
|
|
82
85
|
|
|
@@ -122,6 +125,16 @@ module Sentry
|
|
|
122
125
|
end
|
|
123
126
|
end
|
|
124
127
|
|
|
128
|
+
if event_type == TransactionEvent::TYPE && configuration.before_send_transaction
|
|
129
|
+
event = configuration.before_send_transaction.call(event, hint)
|
|
130
|
+
|
|
131
|
+
if event.nil?
|
|
132
|
+
log_info("Discarded event because before_send_transaction returned nil")
|
|
133
|
+
transport.record_lost_event(:before_send, 'transaction')
|
|
134
|
+
return
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
125
138
|
transport.send_event(event)
|
|
126
139
|
|
|
127
140
|
event
|
data/lib/sentry/configuration.rb
CHANGED
|
@@ -72,6 +72,19 @@ module Sentry
|
|
|
72
72
|
# @return [Proc]
|
|
73
73
|
attr_reader :before_send
|
|
74
74
|
|
|
75
|
+
# Optional Proc, called before sending an event to the server
|
|
76
|
+
# @example
|
|
77
|
+
# config.before_send_transaction = lambda do |event, hint|
|
|
78
|
+
# # skip unimportant transactions or strip sensitive data
|
|
79
|
+
# if event.transaction == "/healthcheck/route"
|
|
80
|
+
# nil
|
|
81
|
+
# else
|
|
82
|
+
# event
|
|
83
|
+
# end
|
|
84
|
+
# end
|
|
85
|
+
# @return [Proc]
|
|
86
|
+
attr_reader :before_send_transaction
|
|
87
|
+
|
|
75
88
|
# An array of breadcrumbs loggers to be used. Available options are:
|
|
76
89
|
# - :sentry_logger
|
|
77
90
|
# - :http_logger
|
|
@@ -84,10 +97,6 @@ module Sentry
|
|
|
84
97
|
# @return [Array<Symbol>]
|
|
85
98
|
attr_reader :breadcrumbs_logger
|
|
86
99
|
|
|
87
|
-
# Whether to capture local variables from the raised exception's frame. Default is false.
|
|
88
|
-
# @return [Boolean]
|
|
89
|
-
attr_accessor :capture_exception_frame_locals
|
|
90
|
-
|
|
91
100
|
# Max number of breadcrumbs a breadcrumb buffer can hold
|
|
92
101
|
# @return [Integer]
|
|
93
102
|
attr_accessor :max_breadcrumbs
|
|
@@ -127,6 +136,22 @@ module Sentry
|
|
|
127
136
|
attr_accessor :inspect_exception_causes_for_exclusion
|
|
128
137
|
alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion
|
|
129
138
|
|
|
139
|
+
# Whether to capture local variables from the raised exception's frame. Default is false.
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
attr_accessor :include_local_variables
|
|
142
|
+
|
|
143
|
+
# @deprecated Use {#include_local_variables} instead.
|
|
144
|
+
alias_method :capture_exception_frame_locals, :include_local_variables
|
|
145
|
+
|
|
146
|
+
# @deprecated Use {#include_local_variables=} instead.
|
|
147
|
+
def capture_exception_frame_locals=(value)
|
|
148
|
+
log_warn <<~MSG
|
|
149
|
+
`capture_exception_frame_locals` is now deprecated in favor of `include_local_variables`.
|
|
150
|
+
MSG
|
|
151
|
+
|
|
152
|
+
self.include_local_variables = value
|
|
153
|
+
end
|
|
154
|
+
|
|
130
155
|
# You may provide your own LineCache for matching paths with source files.
|
|
131
156
|
# This may be useful if you need to get source code from places other than the disk.
|
|
132
157
|
# @see LineCache
|
|
@@ -202,6 +227,11 @@ module Sentry
|
|
|
202
227
|
# @return [Proc]
|
|
203
228
|
attr_accessor :traces_sampler
|
|
204
229
|
|
|
230
|
+
# Easier way to use performance tracing
|
|
231
|
+
# If set to true, will set traces_sample_rate to 1.0
|
|
232
|
+
# @return [Boolean, nil]
|
|
233
|
+
attr_reader :enable_tracing
|
|
234
|
+
|
|
205
235
|
# Send diagnostic client reports about dropped events, true by default
|
|
206
236
|
# tries to attach to an existing envelope max once every 30s
|
|
207
237
|
# @return [Boolean]
|
|
@@ -215,6 +245,12 @@ module Sentry
|
|
|
215
245
|
# @return [Symbol]
|
|
216
246
|
attr_reader :instrumenter
|
|
217
247
|
|
|
248
|
+
# Take a float between 0.0 and 1.0 as the sample rate for capturing profiles.
|
|
249
|
+
# Note that this rate is relative to traces_sample_rate / traces_sampler,
|
|
250
|
+
# i.e. the profile is sampled by this rate after the transaction is sampled.
|
|
251
|
+
# @return [Float, nil]
|
|
252
|
+
attr_reader :profiles_sample_rate
|
|
253
|
+
|
|
218
254
|
# these are not config options
|
|
219
255
|
# @!visibility private
|
|
220
256
|
attr_reader :errors, :gem_specs
|
|
@@ -243,9 +279,18 @@ module Sentry
|
|
|
243
279
|
|
|
244
280
|
INSTRUMENTERS = [:sentry, :otel]
|
|
245
281
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
282
|
+
class << self
|
|
283
|
+
# Post initialization callbacks are called at the end of initialization process
|
|
284
|
+
# allowing extending the configuration of sentry-ruby by multiple extensions
|
|
285
|
+
def post_initialization_callbacks
|
|
286
|
+
@post_initialization_callbacks ||= []
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# allow extensions to add their hooks to the Configuration class
|
|
290
|
+
def add_post_initialization_callback(&block)
|
|
291
|
+
post_initialization_callbacks << block
|
|
292
|
+
end
|
|
293
|
+
end
|
|
249
294
|
|
|
250
295
|
def initialize
|
|
251
296
|
self.app_dirs_pattern = nil
|
|
@@ -255,7 +300,7 @@ module Sentry
|
|
|
255
300
|
self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
|
|
256
301
|
self.breadcrumbs_logger = []
|
|
257
302
|
self.context_lines = 3
|
|
258
|
-
self.
|
|
303
|
+
self.include_local_variables = false
|
|
259
304
|
self.environment = environment_from_env
|
|
260
305
|
self.enabled_environments = []
|
|
261
306
|
self.exclude_loggers = []
|
|
@@ -278,9 +323,11 @@ module Sentry
|
|
|
278
323
|
self.instrumenter = :sentry
|
|
279
324
|
|
|
280
325
|
self.before_send = nil
|
|
326
|
+
self.before_send_transaction = nil
|
|
281
327
|
self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
|
|
282
328
|
self.traces_sample_rate = nil
|
|
283
329
|
self.traces_sampler = nil
|
|
330
|
+
self.enable_tracing = nil
|
|
284
331
|
|
|
285
332
|
@transport = Transport::Configuration.new
|
|
286
333
|
@gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
|
|
@@ -329,6 +376,12 @@ module Sentry
|
|
|
329
376
|
@before_send = value
|
|
330
377
|
end
|
|
331
378
|
|
|
379
|
+
def before_send_transaction=(value)
|
|
380
|
+
check_callable!("before_send_transaction", value)
|
|
381
|
+
|
|
382
|
+
@before_send_transaction = value
|
|
383
|
+
end
|
|
384
|
+
|
|
332
385
|
def before_breadcrumb=(value)
|
|
333
386
|
check_callable!("before_breadcrumb", value)
|
|
334
387
|
|
|
@@ -343,6 +396,16 @@ module Sentry
|
|
|
343
396
|
@instrumenter = INSTRUMENTERS.include?(instrumenter) ? instrumenter : :sentry
|
|
344
397
|
end
|
|
345
398
|
|
|
399
|
+
def enable_tracing=(enable_tracing)
|
|
400
|
+
@enable_tracing = enable_tracing
|
|
401
|
+
@traces_sample_rate ||= 1.0 if enable_tracing
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def profiles_sample_rate=(profiles_sample_rate)
|
|
405
|
+
log_info("Please make sure to include the 'stackprof' gem in your Gemfile to use Profiling with Sentry.") unless defined?(StackProf)
|
|
406
|
+
@profiles_sample_rate = profiles_sample_rate
|
|
407
|
+
end
|
|
408
|
+
|
|
346
409
|
def sending_allowed?
|
|
347
410
|
@errors = []
|
|
348
411
|
|
|
@@ -373,7 +436,20 @@ module Sentry
|
|
|
373
436
|
end
|
|
374
437
|
|
|
375
438
|
def tracing_enabled?
|
|
376
|
-
!!((@traces_sample_rate &&
|
|
439
|
+
valid_sampler = !!((@traces_sample_rate &&
|
|
440
|
+
@traces_sample_rate >= 0.0 &&
|
|
441
|
+
@traces_sample_rate <= 1.0) ||
|
|
442
|
+
@traces_sampler)
|
|
443
|
+
|
|
444
|
+
(@enable_tracing != false) && valid_sampler && sending_allowed?
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def profiling_enabled?
|
|
448
|
+
valid_sampler = !!(@profiles_sample_rate &&
|
|
449
|
+
@profiles_sample_rate >= 0.0 &&
|
|
450
|
+
@profiles_sample_rate <= 1.0)
|
|
451
|
+
|
|
452
|
+
tracing_enabled? && valid_sampler && sending_allowed?
|
|
377
453
|
end
|
|
378
454
|
|
|
379
455
|
# @return [String, nil]
|
|
@@ -498,16 +574,5 @@ module Sentry
|
|
|
498
574
|
instance_eval(&hook)
|
|
499
575
|
end
|
|
500
576
|
end
|
|
501
|
-
|
|
502
|
-
# allow extensions to add their hooks to the Configuration class
|
|
503
|
-
def self.add_post_initialization_callback(&block)
|
|
504
|
-
self.post_initialization_callbacks << block
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
protected
|
|
508
|
-
|
|
509
|
-
def self.post_initialization_callbacks
|
|
510
|
-
@@post_initialization_callbacks
|
|
511
|
-
end
|
|
512
577
|
end
|
|
513
578
|
end
|
data/lib/sentry/envelope.rb
CHANGED
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
|
|
|
@@ -122,6 +124,7 @@ module Sentry
|
|
|
122
124
|
|
|
123
125
|
options[:hint] ||= {}
|
|
124
126
|
options[:hint][:exception] = exception
|
|
127
|
+
|
|
125
128
|
event = current_client.event_from_exception(exception, options[:hint])
|
|
126
129
|
|
|
127
130
|
return unless event
|
data/lib/sentry/net/http.rb
CHANGED
|
@@ -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
|
@@ -54,7 +54,8 @@ module Sentry
|
|
|
54
54
|
def parsed_commands
|
|
55
55
|
commands.map do |statement|
|
|
56
56
|
command, key, *arguments = statement
|
|
57
|
-
command_set = { command: command.to_s.upcase
|
|
57
|
+
command_set = { command: command.to_s.upcase }
|
|
58
|
+
command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key)
|
|
58
59
|
|
|
59
60
|
if Sentry.configuration.send_default_pii
|
|
60
61
|
command_set[:arguments] = arguments
|
|
@@ -70,19 +71,32 @@ module Sentry
|
|
|
70
71
|
"#{host}:#{port}/#{db}"
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
module
|
|
74
|
+
module OldClientPatch
|
|
74
75
|
def logging(commands, &block)
|
|
75
|
-
Sentry::Redis.new(commands, host, port, db).instrument
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
Sentry::Redis.new(commands, host, port, db).instrument { super }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
module GlobalRedisInstrumentation
|
|
81
|
+
def call(command, redis_config)
|
|
82
|
+
Sentry::Redis
|
|
83
|
+
.new([command], redis_config.host, redis_config.port, redis_config.db)
|
|
84
|
+
.instrument { super }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def call_pipelined(commands, redis_config)
|
|
88
|
+
Sentry::Redis
|
|
89
|
+
.new(commands, redis_config.host, redis_config.port, redis_config.db)
|
|
90
|
+
.instrument { super }
|
|
78
91
|
end
|
|
79
92
|
end
|
|
80
93
|
end
|
|
81
94
|
end
|
|
82
95
|
|
|
83
96
|
if defined?(::Redis::Client)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0")
|
|
98
|
+
Sentry.register_patch(Sentry::Redis::OldClientPatch, ::Redis::Client)
|
|
99
|
+
elsif defined?(RedisClient)
|
|
100
|
+
RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
|
|
87
101
|
end
|
|
88
102
|
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
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
|
|
|
@@ -54,6 +59,10 @@ module Sentry
|
|
|
54
59
|
# @return [Hash]
|
|
55
60
|
attr_reader :contexts
|
|
56
61
|
|
|
62
|
+
# The Profiler instance for this transaction.
|
|
63
|
+
# @return [Profiler]
|
|
64
|
+
attr_reader :profiler
|
|
65
|
+
|
|
57
66
|
def initialize(
|
|
58
67
|
hub:,
|
|
59
68
|
name: nil,
|
|
@@ -78,6 +87,8 @@ module Sentry
|
|
|
78
87
|
@dsn = hub.configuration.dsn
|
|
79
88
|
@effective_sample_rate = nil
|
|
80
89
|
@contexts = {}
|
|
90
|
+
@measurements = {}
|
|
91
|
+
@profiler = Profiler.new(@configuration)
|
|
81
92
|
init_span_recorder
|
|
82
93
|
end
|
|
83
94
|
|
|
@@ -163,6 +174,15 @@ module Sentry
|
|
|
163
174
|
copy
|
|
164
175
|
end
|
|
165
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
|
+
|
|
166
186
|
# Sets initial sampling decision of the transaction.
|
|
167
187
|
# @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
|
|
168
188
|
# @return [void]
|
|
@@ -240,6 +260,8 @@ module Sentry
|
|
|
240
260
|
@name = UNLABELD_NAME
|
|
241
261
|
end
|
|
242
262
|
|
|
263
|
+
@profiler.stop
|
|
264
|
+
|
|
243
265
|
if @sampled
|
|
244
266
|
event = hub.current_client.event_from_transaction(self)
|
|
245
267
|
hub.capture_event(event)
|
|
@@ -274,6 +296,13 @@ module Sentry
|
|
|
274
296
|
@contexts[key] = value
|
|
275
297
|
end
|
|
276
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
|
+
|
|
277
306
|
protected
|
|
278
307
|
|
|
279
308
|
def init_span_recorder(limit = 1000)
|
|
@@ -11,9 +11,15 @@ module Sentry
|
|
|
11
11
|
# @return [Hash, nil]
|
|
12
12
|
attr_accessor :dynamic_sampling_context
|
|
13
13
|
|
|
14
|
+
# @return [Hash]
|
|
15
|
+
attr_accessor :measurements
|
|
16
|
+
|
|
14
17
|
# @return [Float, nil]
|
|
15
18
|
attr_reader :start_timestamp
|
|
16
19
|
|
|
20
|
+
# @return [Hash, nil]
|
|
21
|
+
attr_accessor :profile
|
|
22
|
+
|
|
17
23
|
def initialize(transaction:, **options)
|
|
18
24
|
super(**options)
|
|
19
25
|
|
|
@@ -25,9 +31,12 @@ module Sentry
|
|
|
25
31
|
self.start_timestamp = transaction.start_timestamp
|
|
26
32
|
self.tags = transaction.tags
|
|
27
33
|
self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
|
|
34
|
+
self.measurements = transaction.measurements
|
|
28
35
|
|
|
29
36
|
finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
|
|
30
37
|
self.spans = finished_spans.map(&:to_hash)
|
|
38
|
+
|
|
39
|
+
populate_profile(transaction)
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
# Sets the event's start_timestamp.
|
|
@@ -42,7 +51,33 @@ module Sentry
|
|
|
42
51
|
data = super
|
|
43
52
|
data[:spans] = @spans.map(&:to_hash) if @spans
|
|
44
53
|
data[:start_timestamp] = @start_timestamp
|
|
54
|
+
data[:measurements] = @measurements
|
|
45
55
|
data
|
|
46
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def populate_profile(transaction)
|
|
61
|
+
profile_hash = transaction.profiler.to_hash
|
|
62
|
+
return if profile_hash.empty?
|
|
63
|
+
|
|
64
|
+
profile_hash.merge!(
|
|
65
|
+
environment: environment,
|
|
66
|
+
release: release,
|
|
67
|
+
timestamp: Time.at(start_timestamp).iso8601,
|
|
68
|
+
device: { architecture: Scope.os_context[:machine] },
|
|
69
|
+
os: { name: Scope.os_context[:name], version: Scope.os_context[:version] },
|
|
70
|
+
runtime: Scope.runtime_context,
|
|
71
|
+
transaction: {
|
|
72
|
+
id: event_id,
|
|
73
|
+
name: transaction.name,
|
|
74
|
+
trace_id: transaction.trace_id,
|
|
75
|
+
# TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
|
|
76
|
+
active_thead_id: '0'
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.profile = profile_hash
|
|
81
|
+
end
|
|
47
82
|
end
|
|
48
83
|
end
|
data/lib/sentry/transport.rb
CHANGED
|
@@ -154,6 +154,13 @@ module Sentry
|
|
|
154
154
|
event_payload
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
+
if event.is_a?(TransactionEvent) && event.profile
|
|
158
|
+
envelope.add_item(
|
|
159
|
+
{ type: 'profile', content_type: 'application/json' },
|
|
160
|
+
event.profile
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
157
164
|
client_report_headers, client_report_payload = fetch_pending_client_report
|
|
158
165
|
envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
|
|
159
166
|
|
data/lib/sentry/version.rb
CHANGED
data/lib/sentry-ruby.rb
CHANGED
|
@@ -73,8 +73,18 @@ module Sentry
|
|
|
73
73
|
##### Patch Registration #####
|
|
74
74
|
|
|
75
75
|
# @!visibility private
|
|
76
|
-
def register_patch(&block)
|
|
77
|
-
|
|
76
|
+
def register_patch(patch = nil, target = nil, &block)
|
|
77
|
+
if patch && block
|
|
78
|
+
raise ArgumentError.new("Please provide either a patch and its target OR a block, but not both")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if block
|
|
82
|
+
registered_patches << block
|
|
83
|
+
else
|
|
84
|
+
registered_patches << proc do
|
|
85
|
+
target.send(:prepend, patch) unless target.ancestors.include?(patch)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
# @!visibility private
|
|
@@ -212,7 +222,7 @@ module Sentry
|
|
|
212
222
|
nil
|
|
213
223
|
end
|
|
214
224
|
|
|
215
|
-
if config.
|
|
225
|
+
if config.include_local_variables
|
|
216
226
|
exception_locals_tp.enable
|
|
217
227
|
end
|
|
218
228
|
|
|
@@ -234,7 +244,7 @@ module Sentry
|
|
|
234
244
|
@session_flusher = nil
|
|
235
245
|
end
|
|
236
246
|
|
|
237
|
-
if configuration&.
|
|
247
|
+
if configuration&.include_local_variables
|
|
238
248
|
exception_locals_tp.disable
|
|
239
249
|
end
|
|
240
250
|
|
|
@@ -462,6 +472,23 @@ module Sentry
|
|
|
462
472
|
!!exc.instance_variable_get(CAPTURED_SIGNATURE)
|
|
463
473
|
end
|
|
464
474
|
|
|
475
|
+
# Add a global event processor [Proc].
|
|
476
|
+
# These run before scope event processors.
|
|
477
|
+
#
|
|
478
|
+
# @yieldparam event [Event]
|
|
479
|
+
# @yieldparam hint [Hash, nil]
|
|
480
|
+
# @return [void]
|
|
481
|
+
#
|
|
482
|
+
# @example
|
|
483
|
+
# Sentry.add_global_event_processor do |event, hint|
|
|
484
|
+
# event.tags = { foo: 42 }
|
|
485
|
+
# event
|
|
486
|
+
# end
|
|
487
|
+
#
|
|
488
|
+
def add_global_event_processor(&block)
|
|
489
|
+
Scope.add_global_event_processor(&block)
|
|
490
|
+
end
|
|
491
|
+
|
|
465
492
|
##### Helpers #####
|
|
466
493
|
|
|
467
494
|
# @!visibility private
|
|
@@ -492,3 +519,4 @@ end
|
|
|
492
519
|
# patches
|
|
493
520
|
require "sentry/net/http"
|
|
494
521
|
require "sentry/redis"
|
|
522
|
+
require "sentry/puma"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sentry-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.9.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:
|
|
11
|
+
date: 2023-04-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -78,6 +78,8 @@ files:
|
|
|
78
78
|
- lib/sentry/linecache.rb
|
|
79
79
|
- lib/sentry/logger.rb
|
|
80
80
|
- lib/sentry/net/http.rb
|
|
81
|
+
- lib/sentry/profiler.rb
|
|
82
|
+
- lib/sentry/puma.rb
|
|
81
83
|
- lib/sentry/rack.rb
|
|
82
84
|
- lib/sentry/rack/capture_exceptions.rb
|
|
83
85
|
- lib/sentry/rake.rb
|