sentry-ruby-core 5.7.0 → 5.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b278a33d0084cedcfbe91b489c53c56dee24543e5a319d764aff94cc41c579a
4
- data.tar.gz: 923f906cd698d18f2fcb419bfeb2d1262c4165e6a8ffe2484de7027a90e69644
3
+ metadata.gz: 34b14c30aa0416847e72f2810e3155ca61866438cdc35dcc22ebdb9c15af6bcb
4
+ data.tar.gz: 7a17648c7a5d06f22d2d643f6ff1cb25d3fed2348609e372fc5762043ebd538a
5
5
  SHA512:
6
- metadata.gz: 47f71f658582bb3b81918b96780e118c4bc161c78f9d0c3e060a80fd6e75258824fc6be690d57899445fdc03f9cbf5c3693ad7754fcc9d96ba2d1480b0222444
7
- data.tar.gz: 14b28401b32d9ce9ee7ac0351de736d0d9f11221010e61091e4976b71a58b1fef61ca4e282ec31a714e040148197c5fab46f0a0e39e475ea1f6dd0196b5f104d
6
+ metadata.gz: 9db3e0be215c6fa6082602d1f992fce864c3a2cdd5874e0f409f41e61cca2f15493ec33c497647f64e149098d114c552bf29f36da1d5818002c4be0ea922b21e
7
+ data.tar.gz: 6bf0cba7b42dd9bfd5078e4353ad7f92d932b959a87c748cb30e505ffd8d205899c819ea4f72e5f3099f0b861139864062b870219678abdeda0a0629604963d3
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
1
  --format documentation
2
2
  --color
3
- --require spec_helper
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 'simplecov'
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", "~> 0.9.27"
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 :default => :spec
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]
@@ -76,7 +76,7 @@ module Sentry
76
76
  end
77
77
  end
78
78
 
79
- APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test)/.freeze
79
+ APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/.freeze
80
80
 
81
81
  # holder for an Array of Backtrace::Line instances
82
82
  attr_reader :lines
@@ -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.select { |k, _v| DSC_KEYS.include?(k) }
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? && @configuration.exception_class_allowed?(exception)
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
@@ -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
- # Post initialization callbacks are called at the end of initialization process
247
- # allowing extending the configuration of sentry-ruby by multiple extensions
248
- @@post_initialization_callbacks = []
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.capture_exception_frame_locals = false
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 && @traces_sample_rate >= 0.0 && @traces_sample_rate <= 1.0) || @traces_sampler) && sending_allowed?
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
@@ -19,10 +19,7 @@ module Sentry
19
19
  end
20
20
 
21
21
  def to_s
22
- <<~ITEM
23
- #{JSON.generate(@headers)}
24
- #{JSON.generate(@payload)}
25
- ITEM
22
+ [JSON.generate(@headers), JSON.generate(@payload)].join("\n")
26
23
  end
27
24
 
28
25
  def serialize
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
@@ -97,7 +97,4 @@ module Sentry
97
97
  end
98
98
  end
99
99
 
100
- Sentry.register_patch do
101
- patch = Sentry::Net::HTTP
102
- Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
103
- end
100
+ Sentry.register_patch(Sentry::Net::HTTP, Net::HTTP)
@@ -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
@@ -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, key: key }
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 Client
74
+ module OldClientPatch
74
75
  def logging(commands, &block)
75
- Sentry::Redis.new(commands, host, port, db).instrument do
76
- super
77
- end
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
- Sentry.register_patch do
85
- patch = Sentry::Redis::Client
86
- Redis::Client.prepend(patch) unless Redis::Client.ancestors.include?(patch)
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
- unless @event_processors.empty?
62
- @event_processors.each do |processor_block|
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
@@ -169,6 +169,10 @@ module Sentry
169
169
  yield(child_span)
170
170
 
171
171
  child_span.finish
172
+ rescue
173
+ child_span.set_http_status(500)
174
+ child_span.finish
175
+ raise
172
176
  end
173
177
 
174
178
  def deep_dup
@@ -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
- # - capture_exception_frame_locals
28
+ # - include_local_variables
29
29
  # - auto_session_tracking
30
30
  block&.call(copied_config)
31
31
 
@@ -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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.7.0"
4
+ VERSION = "5.9.0"
5
5
  end
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
- registered_patches << block
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.capture_exception_frame_locals
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&.capture_exception_frame_locals
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-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.7.0
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: 2022-12-01 00:00:00.000000000 Z
11
+ date: 2023-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sentry-ruby
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 5.7.0
19
+ version: 5.9.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 5.7.0
26
+ version: 5.9.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: concurrent-ruby
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +86,8 @@ files:
86
86
  - lib/sentry/linecache.rb
87
87
  - lib/sentry/logger.rb
88
88
  - lib/sentry/net/http.rb
89
+ - lib/sentry/profiler.rb
90
+ - lib/sentry/puma.rb
89
91
  - lib/sentry/rack.rb
90
92
  - lib/sentry/rack/capture_exceptions.rb
91
93
  - lib/sentry/rake.rb