sentry-ruby-core 5.20.1 → 5.22.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: 698becb3655b43c16988129ae4176690e6a8a2e67bee4004b8580d71348b843a
4
- data.tar.gz: 7a5e37ce71f2e3f483a3e193f2bfb0e283ae304ed4950068384806780614fc40
3
+ metadata.gz: d6ea6ea7aa1656256faf78dd295dddffdea94bac58ff263f49d3a67433a35fd6
4
+ data.tar.gz: 9f8b2d74f31fc7a313d4170bdbbc0707b61c52ae658102c6d353a09f84df81e9
5
5
  SHA512:
6
- metadata.gz: 2531811bb4f288dd34ac72b7ad8fdf31cee30ba8bed63bb48a90c527d63ce3df531480ec5c9d69bf03c44ff81ddb9026936c45977f30c35a68ce473c1dc0b999
7
- data.tar.gz: 2cfa9516dcbbddac0b7b8930b400150d2f93b7ff779c93fb237cdd5bfb5e04434a1cae2e8cedd85cade5bfe536807da1d6b1b9d5838a53267426bc5694c62ade
6
+ metadata.gz: 312560c682b07cee10690c180e7e2780daf616574dfcb792bebf1315feeb9a47ad428dbb7c7627439e781b8db85c9bc9166feb77dfa14974373df12982ae6324
7
+ data.tar.gz: 7f2d782aa3b58c473c0c06efbcb1e8ff8ff039a5d396d8066187d2ff863008145fb08a7ad97eedc69841b685d09da01a56c67938bb19032d65fab7ddbce05e7f
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
  git_source(:github) { |name| "https://github.com/#{name}.git" }
3
5
 
@@ -14,6 +16,7 @@ gem "puma"
14
16
 
15
17
  gem "timecop"
16
18
  gem "stackprof" unless RUBY_PLATFORM == "java"
19
+ gem "vernier", platforms: :ruby if RUBY_VERSION >= "3.2.1"
17
20
 
18
21
  gem "graphql", ">= 2.2.6" if RUBY_VERSION.to_f >= 2.7
19
22
 
@@ -25,5 +28,6 @@ gem "benchmark-memory"
25
28
  gem "yard", github: "lsegal/yard"
26
29
  gem "webrick"
27
30
  gem "faraday"
31
+ gem "excon"
28
32
 
29
33
  eval_gemfile File.expand_path("../Gemfile", __dir__)
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rake/clean"
2
4
  CLOBBER.include "pkg"
3
5
 
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "debug"
@@ -13,10 +13,10 @@ module Sentry
13
13
  ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
14
14
  (\d+)
15
15
  (?: :in\s('|`)([^']+)')?$
16
- /x.freeze
16
+ /x
17
17
 
18
18
  # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
19
- JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/.freeze
19
+ JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/
20
20
 
21
21
  # The file portion of the line (such as app/models/user.rb)
22
22
  attr_reader :file
@@ -6,7 +6,7 @@ module Sentry
6
6
  # A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation.
7
7
  class Baggage
8
8
  SENTRY_PREFIX = "sentry-"
9
- SENTRY_PREFIX_REGEX = /^sentry-/.freeze
9
+ SENTRY_PREFIX_REGEX = /^sentry-/
10
10
 
11
11
  # @return [Hash]
12
12
  attr_reader :items
@@ -3,8 +3,8 @@
3
3
  require "concurrent/utility/processor_counter"
4
4
 
5
5
  require "sentry/utils/exception_cause_chain"
6
- require 'sentry/utils/custom_inspection'
7
- require 'sentry/utils/env_helper'
6
+ require "sentry/utils/custom_inspection"
7
+ require "sentry/utils/env_helper"
8
8
  require "sentry/dsn"
9
9
  require "sentry/release_detector"
10
10
  require "sentry/transport/configuration"
@@ -291,6 +291,10 @@ module Sentry
291
291
  # @return [Symbol]
292
292
  attr_reader :instrumenter
293
293
 
294
+ # The profiler class
295
+ # @return [Class]
296
+ attr_reader :profiler_class
297
+
294
298
  # Take a float between 0.0 and 1.0 as the sample rate for capturing profiles.
295
299
  # Note that this rate is relative to traces_sample_rate / traces_sampler,
296
300
  # i.e. the profile is sampled by this rate after the transaction is sampled.
@@ -331,19 +335,19 @@ module Sentry
331
335
  ].freeze
332
336
 
333
337
  HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\
334
- "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`".freeze
338
+ "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`"
335
339
 
336
- LOG_PREFIX = "** [Sentry] ".freeze
337
- MODULE_SEPARATOR = "::".freeze
340
+ LOG_PREFIX = "** [Sentry] "
341
+ MODULE_SEPARATOR = "::"
338
342
  SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder]
339
343
 
340
344
  INSTRUMENTERS = [:sentry, :otel]
341
345
 
342
- PROPAGATION_TARGETS_MATCH_ALL = /.*/.freeze
346
+ PROPAGATION_TARGETS_MATCH_ALL = /.*/
343
347
 
344
348
  DEFAULT_PATCHES = %i[redis puma http].freeze
345
349
 
346
- APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/.freeze
350
+ APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/
347
351
 
348
352
  class << self
349
353
  # Post initialization callbacks are called at the end of initialization process
@@ -387,9 +391,9 @@ module Sentry
387
391
  self.auto_session_tracking = true
388
392
  self.enable_backpressure_handling = false
389
393
  self.trusted_proxies = []
390
- self.dsn = ENV['SENTRY_DSN']
394
+ self.dsn = ENV["SENTRY_DSN"]
391
395
 
392
- spotlight_env = ENV['SENTRY_SPOTLIGHT']
396
+ spotlight_env = ENV["SENTRY_SPOTLIGHT"]
393
397
  spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
394
398
  self.spotlight = spotlight_bool.nil? ? (spotlight_env || false) : spotlight_bool
395
399
  self.server_name = server_name_from_env
@@ -403,6 +407,8 @@ module Sentry
403
407
  self.traces_sampler = nil
404
408
  self.enable_tracing = nil
405
409
 
410
+ self.profiler_class = Sentry::Profiler
411
+
406
412
  @transport = Transport::Configuration.new
407
413
  @cron = Cron::Configuration.new
408
414
  @metrics = Metrics::Configuration.new
@@ -498,6 +504,18 @@ module Sentry
498
504
  @profiles_sample_rate = profiles_sample_rate
499
505
  end
500
506
 
507
+ def profiler_class=(profiler_class)
508
+ if profiler_class == Sentry::Vernier::Profiler
509
+ begin
510
+ require "vernier"
511
+ rescue LoadError
512
+ raise ArgumentError, "Please add the 'vernier' gem to your Gemfile to use the Vernier profiler with Sentry."
513
+ end
514
+ end
515
+
516
+ @profiler_class = profiler_class
517
+ end
518
+
501
519
  def sending_allowed?
502
520
  spotlight || sending_to_dsn_allowed?
503
521
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Cron
3
5
  module MonitorCheckIns
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class Envelope::Item
6
+ STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
7
+ MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000
8
+
9
+ SIZE_LIMITS = Hash.new(MAX_SERIALIZED_PAYLOAD_SIZE).update(
10
+ "profile" => 1024 * 1000 * 50
11
+ )
12
+
13
+ attr_reader :size_limit, :headers, :payload, :type, :data_category
14
+
15
+ # rate limits and client reports use the data_category rather than envelope item type
16
+ def self.data_category(type)
17
+ case type
18
+ when "session", "attachment", "transaction", "profile", "span" then type
19
+ when "sessions" then "session"
20
+ when "check_in" then "monitor"
21
+ when "statsd", "metric_meta" then "metric_bucket"
22
+ when "event" then "error"
23
+ when "client_report" then "internal"
24
+ else "default"
25
+ end
26
+ end
27
+
28
+ def initialize(headers, payload)
29
+ @headers = headers
30
+ @payload = payload
31
+ @type = headers[:type] || "event"
32
+ @data_category = self.class.data_category(type)
33
+ @size_limit = SIZE_LIMITS[type]
34
+ end
35
+
36
+ def to_s
37
+ [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
38
+ end
39
+
40
+ def serialize
41
+ result = to_s
42
+
43
+ if result.bytesize > size_limit
44
+ remove_breadcrumbs!
45
+ result = to_s
46
+ end
47
+
48
+ if result.bytesize > size_limit
49
+ reduce_stacktrace!
50
+ result = to_s
51
+ end
52
+
53
+ [result, result.bytesize > size_limit]
54
+ end
55
+
56
+ def size_breakdown
57
+ payload.map do |key, value|
58
+ "#{key}: #{JSON.generate(value).bytesize}"
59
+ end.join(", ")
60
+ end
61
+
62
+ private
63
+
64
+ def remove_breadcrumbs!
65
+ if payload.key?(:breadcrumbs)
66
+ payload.delete(:breadcrumbs)
67
+ elsif payload.key?("breadcrumbs")
68
+ payload.delete("breadcrumbs")
69
+ end
70
+ end
71
+
72
+ def reduce_stacktrace!
73
+ if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
74
+ exceptions.each do |exception|
75
+ # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
76
+ traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
77
+
78
+ if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
79
+ size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
80
+ traces.replace(
81
+ traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -3,91 +3,6 @@
3
3
  module Sentry
4
4
  # @api private
5
5
  class Envelope
6
- class Item
7
- STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
8
- MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000
9
-
10
- attr_accessor :headers, :payload
11
-
12
- def initialize(headers, payload)
13
- @headers = headers
14
- @payload = payload
15
- end
16
-
17
- def type
18
- @headers[:type] || "event"
19
- end
20
-
21
- # rate limits and client reports use the data_category rather than envelope item type
22
- def self.data_category(type)
23
- case type
24
- when "session", "attachment", "transaction", "profile", "span" then type
25
- when "sessions" then "session"
26
- when "check_in" then "monitor"
27
- when "statsd", "metric_meta" then "metric_bucket"
28
- when "event" then "error"
29
- when "client_report" then "internal"
30
- else "default"
31
- end
32
- end
33
-
34
- def data_category
35
- self.class.data_category(type)
36
- end
37
-
38
- def to_s
39
- [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
40
- end
41
-
42
- def serialize
43
- result = to_s
44
-
45
- if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
46
- remove_breadcrumbs!
47
- result = to_s
48
- end
49
-
50
- if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
51
- reduce_stacktrace!
52
- result = to_s
53
- end
54
-
55
- [result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE]
56
- end
57
-
58
- def size_breakdown
59
- payload.map do |key, value|
60
- "#{key}: #{JSON.generate(value).bytesize}"
61
- end.join(", ")
62
- end
63
-
64
- private
65
-
66
- def remove_breadcrumbs!
67
- if payload.key?(:breadcrumbs)
68
- payload.delete(:breadcrumbs)
69
- elsif payload.key?("breadcrumbs")
70
- payload.delete("breadcrumbs")
71
- end
72
- end
73
-
74
- def reduce_stacktrace!
75
- if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
76
- exceptions.each do |exception|
77
- # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
78
- traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
79
-
80
- if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
81
- size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
82
- traces.replace(
83
- traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
84
- )
85
- end
86
- end
87
- end
88
- end
89
- end
90
-
91
6
  attr_accessor :headers, :items
92
7
 
93
8
  def initialize(headers = {})
@@ -108,3 +23,5 @@ module Sentry
108
23
  end
109
24
  end
110
25
  end
26
+
27
+ require_relative "envelope/item"
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Excon
5
+ OP_NAME = "http.client"
6
+
7
+ class Middleware < ::Excon::Middleware::Base
8
+ def initialize(stack)
9
+ super
10
+ @instrumenter = Instrumenter.new
11
+ end
12
+
13
+ def request_call(datum)
14
+ @instrumenter.start_transaction(datum)
15
+ @stack.request_call(datum)
16
+ end
17
+
18
+ def response_call(datum)
19
+ @instrumenter.finish_transaction(datum)
20
+ @stack.response_call(datum)
21
+ end
22
+ end
23
+
24
+ class Instrumenter
25
+ SPAN_ORIGIN = "auto.http.excon"
26
+ BREADCRUMB_CATEGORY = "http"
27
+
28
+ include Utils::HttpTracing
29
+
30
+ def start_transaction(env)
31
+ return unless Sentry.initialized?
32
+
33
+ current_span = Sentry.get_current_scope&.span
34
+ @span = current_span&.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN)
35
+
36
+ request_info = extract_request_info(env)
37
+
38
+ if propagate_trace?(request_info[:url])
39
+ set_propagation_headers(env[:headers])
40
+ end
41
+ end
42
+
43
+ def finish_transaction(response)
44
+ return unless @span
45
+
46
+ response_status = response[:response][:status]
47
+ request_info = extract_request_info(response)
48
+
49
+ if record_sentry_breadcrumb?
50
+ record_sentry_breadcrumb(request_info, response_status)
51
+ end
52
+
53
+ set_span_info(@span, request_info, response_status)
54
+ ensure
55
+ @span&.finish
56
+ end
57
+
58
+ private
59
+
60
+ def extract_request_info(env)
61
+ url = env[:scheme] + "://" + env[:hostname] + env[:path]
62
+ result = { method: env[:method].to_s.upcase, url: url }
63
+
64
+ if Sentry.configuration.send_default_pii
65
+ result[:query] = env[:query]
66
+
67
+ # Handle excon 1.0.0+
68
+ result[:query] = build_nested_query(result[:query]) unless result[:query].is_a?(String)
69
+
70
+ result[:body] = env[:body]
71
+ end
72
+
73
+ result
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sentry.register_patch(:excon) do
4
+ if defined?(::Excon)
5
+ require "sentry/excon/middleware"
6
+ if Excon.defaults[:middlewares]
7
+ Excon.defaults[:middlewares] << Sentry::Excon::Middleware unless Excon.defaults[:middlewares].include?(Sentry::Excon::Middleware)
8
+ end
9
+ end
10
+ end
@@ -7,8 +7,8 @@ module Sentry
7
7
  include CustomInspection
8
8
 
9
9
  SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace]
10
- PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]".freeze
11
- OMISSION_MARK = "...".freeze
10
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]"
11
+ OMISSION_MARK = "..."
12
12
  MAX_LOCAL_BYTES = 1024
13
13
 
14
14
  attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Sentry
6
+ class Profiler
7
+ module Helpers
8
+ def in_app?(abs_path)
9
+ abs_path.match?(@in_app_pattern)
10
+ end
11
+
12
+ # copied from stacktrace.rb since I don't want to touch existing code
13
+ # TODO-neel-profiler try to fetch this from stackprof once we patch
14
+ # the native extension
15
+ def compute_filename(abs_path, in_app)
16
+ return nil if abs_path.nil?
17
+
18
+ under_project_root = @project_root && abs_path.start_with?(@project_root)
19
+
20
+ prefix =
21
+ if under_project_root && in_app
22
+ @project_root
23
+ else
24
+ longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
25
+
26
+ if under_project_root
27
+ longest_load_path || @project_root
28
+ else
29
+ longest_load_path
30
+ end
31
+ end
32
+
33
+ prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
34
+ end
35
+
36
+ def split_module(name)
37
+ # last module plus class/instance method
38
+ i = name.rindex("::")
39
+ function = i ? name[(i + 2)..-1] : name
40
+ mod = i ? name[0...i] : nil
41
+
42
+ [function, mod]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
+ require_relative "profiler/helpers"
4
5
 
5
6
  module Sentry
6
7
  class Profiler
8
+ include Profiler::Helpers
9
+
7
10
  VERSION = "1"
8
11
  PLATFORM = "ruby"
9
12
  # 101 Hz in microseconds
@@ -44,6 +47,10 @@ module Sentry
44
47
  log("Stopped")
45
48
  end
46
49
 
50
+ def active_thread_id
51
+ "0"
52
+ end
53
+
47
54
  # Sets initial sampling decision of the profile.
48
55
  # @return [void]
49
56
  def set_initial_sample_decision(transaction_sampled)
@@ -188,43 +195,6 @@ module Sentry
188
195
  Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" }
189
196
  end
190
197
 
191
- def in_app?(abs_path)
192
- abs_path.match?(@in_app_pattern)
193
- end
194
-
195
- # copied from stacktrace.rb since I don't want to touch existing code
196
- # TODO-neel-profiler try to fetch this from stackprof once we patch
197
- # the native extension
198
- def compute_filename(abs_path, in_app)
199
- return nil if abs_path.nil?
200
-
201
- under_project_root = @project_root && abs_path.start_with?(@project_root)
202
-
203
- prefix =
204
- if under_project_root && in_app
205
- @project_root
206
- else
207
- longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
208
-
209
- if under_project_root
210
- longest_load_path || @project_root
211
- else
212
- longest_load_path
213
- end
214
- end
215
-
216
- prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
217
- end
218
-
219
- def split_module(name)
220
- # last module plus class/instance method
221
- i = name.rindex("::")
222
- function = i ? name[(i + 2)..-1] : name
223
- mod = i ? name[0...i] : nil
224
-
225
- [function, mod]
226
- end
227
-
228
198
  def record_lost_event(reason)
229
199
  Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
230
200
  end
@@ -54,7 +54,7 @@ module Sentry
54
54
  end
55
55
 
56
56
  def transaction_op
57
- "http.server".freeze
57
+ "http.server"
58
58
  end
59
59
 
60
60
  def capture_exception(exception, env)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts|
4
+ match do |sentry_events|
5
+ @expected_exception = expected_exception(**opts)
6
+ @context = context(**opts)
7
+ @tags = tags(**opts)
8
+
9
+ @expected_event = expected_event(event_message)
10
+ @matched_event = find_matched_event(event_message, sentry_events)
11
+
12
+ return false unless @matched_event
13
+
14
+ [verify_context(), verify_tags()].all?
15
+ end
16
+
17
+ chain :with_context do |context|
18
+ @context = context
19
+ end
20
+
21
+ chain :with_tags do |tags|
22
+ @tags = tags
23
+ end
24
+
25
+ failure_message do |sentry_events|
26
+ info = ["Failed to find event matching:\n"]
27
+ info << " message: #{@expected_event.message.inspect}"
28
+ info << " exception: #{@expected_exception.inspect}"
29
+ info << " context: #{@context.inspect}"
30
+ info << " tags: #{@tags.inspect}"
31
+ info << "\n"
32
+ info << "Captured events:\n"
33
+ info << dump_events(sentry_events)
34
+ info.join("\n")
35
+ end
36
+
37
+ def expected_event(event_message)
38
+ if @expected_exception
39
+ Sentry.get_current_client.event_from_exception(@expected_exception)
40
+ else
41
+ Sentry.get_current_client.event_from_message(event_message)
42
+ end
43
+ end
44
+
45
+ def expected_exception(**opts)
46
+ opts[:exception].new(opts[:message]) if opts[:exception]
47
+ end
48
+
49
+ def context(**opts)
50
+ opts.fetch(:context, @context || {})
51
+ end
52
+
53
+ def tags(**opts)
54
+ opts.fetch(:tags, @tags || {})
55
+ end
56
+
57
+ def find_matched_event(event_message, sentry_events)
58
+ @matched_event ||= sentry_events
59
+ .find { |event|
60
+ if @expected_exception
61
+ # Is it OK that we only compare the first exception?
62
+ event_exception = event.exception.values.first
63
+ expected_event_exception = @expected_event.exception.values.first
64
+
65
+ event_exception.type == expected_event_exception.type && event_exception.value == expected_event_exception.value
66
+ else
67
+ event.message == @expected_event.message
68
+ end
69
+ }
70
+ end
71
+
72
+ def dump_events(sentry_events)
73
+ sentry_events.map(&Kernel.method(:Hash)).map do |hash|
74
+ hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) }
75
+ end.map do |hash|
76
+ JSON.pretty_generate(hash)
77
+ end.join("\n\n")
78
+ end
79
+
80
+ def verify_context
81
+ return true if @context.empty?
82
+
83
+ @matched_event.contexts.any? { |key, value| value == @context[key] }
84
+ end
85
+
86
+ def verify_tags
87
+ return true if @tags.empty?
88
+
89
+ @tags.all? { |key, value| @matched_event.tags.include?(key) && @matched_event.tags[key] == value }
90
+ end
91
+ end
@@ -10,6 +10,7 @@ module Sentry
10
10
  @pending_aggregates = {}
11
11
  @release = configuration.release
12
12
  @environment = configuration.environment
13
+ @mutex = Mutex.new
13
14
 
14
15
  log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release
15
16
  end
@@ -18,7 +19,6 @@ module Sentry
18
19
  return if @pending_aggregates.empty?
19
20
 
20
21
  @client.capture_envelope(pending_envelope)
21
- @pending_aggregates = {}
22
22
  end
23
23
 
24
24
  alias_method :run, :flush
@@ -42,11 +42,15 @@ module Sentry
42
42
  end
43
43
 
44
44
  def pending_envelope
45
- envelope = Envelope.new
45
+ aggregates = @mutex.synchronize do
46
+ aggregates = @pending_aggregates.values
47
+ @pending_aggregates = {}
48
+ aggregates
49
+ end
46
50
 
51
+ envelope = Envelope.new
47
52
  header = { type: "sessions" }
48
- payload = { attrs: attrs, aggregates: @pending_aggregates.values }
49
-
53
+ payload = { attrs: attrs, aggregates: aggregates }
50
54
  envelope.add_item(header, payload)
51
55
  envelope
52
56
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module TestHelper
3
5
  DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
@@ -9,7 +9,7 @@ module Sentry
9
9
  # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
10
10
  SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
11
11
 
12
- UNLABELD_NAME = "<unlabeled transaction>".freeze
12
+ UNLABELD_NAME = "<unlabeled transaction>"
13
13
  MESSAGE_PREFIX = "[Tracing]"
14
14
 
15
15
  # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
@@ -85,7 +85,7 @@ module Sentry
85
85
  @effective_sample_rate = nil
86
86
  @contexts = {}
87
87
  @measurements = {}
88
- @profiler = Profiler.new(@configuration)
88
+ @profiler = @configuration.profiler_class.new(@configuration)
89
89
  init_span_recorder
90
90
  end
91
91
 
@@ -74,8 +74,7 @@ module Sentry
74
74
  id: event_id,
75
75
  name: transaction.name,
76
76
  trace_id: transaction.trace_id,
77
- # TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
78
- active_thead_id: "0"
77
+ active_thread_id: transaction.profiler.active_thread_id.to_s
79
78
  }
80
79
  )
81
80
 
@@ -19,7 +19,7 @@ module Sentry
19
19
  crumb = Sentry::Breadcrumb.new(
20
20
  level: :info,
21
21
  category: self.class::BREADCRUMB_CATEGORY,
22
- type: :info,
22
+ type: "info",
23
23
  data: { status: response_status, **request_info }
24
24
  )
25
25
 
@@ -36,6 +36,25 @@ module Sentry
36
36
  Sentry.configuration.propagate_traces &&
37
37
  Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
38
38
  end
39
+
40
+ # Kindly borrowed from Rack::Utils
41
+ def build_nested_query(value, prefix = nil)
42
+ case value
43
+ when Array
44
+ value.map { |v|
45
+ build_nested_query(v, "#{prefix}[]")
46
+ }.join("&")
47
+ when Hash
48
+ value.map { |k, v|
49
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
50
+ }.delete_if(&:empty?).join("&")
51
+ when nil
52
+ URI.encode_www_form_component(prefix)
53
+ else
54
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
55
+ "#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
56
+ end
57
+ end
39
58
  end
40
59
  end
41
60
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+
6
+ module Sentry
7
+ module Vernier
8
+ class Output
9
+ include Profiler::Helpers
10
+
11
+ attr_reader :profile
12
+
13
+ def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:)
14
+ @profile = profile
15
+ @project_root = project_root
16
+ @in_app_pattern = in_app_pattern
17
+ @app_dirs_pattern = app_dirs_pattern
18
+ end
19
+
20
+ def to_h
21
+ @to_h ||= {
22
+ frames: frames,
23
+ stacks: stacks,
24
+ samples: samples,
25
+ thread_metadata: thread_metadata
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def thread_metadata
32
+ profile.threads.map { |thread_id, thread_info|
33
+ [thread_id, { name: thread_info[:name] }]
34
+ }.to_h
35
+ end
36
+
37
+ def samples
38
+ profile.threads.flat_map { |thread_id, thread_info|
39
+ started_at = thread_info[:started_at]
40
+ samples, timestamps = thread_info.values_at(:samples, :timestamps)
41
+
42
+ samples.zip(timestamps).map { |stack_id, timestamp|
43
+ elapsed_since_start_ns = timestamp - started_at
44
+
45
+ next if elapsed_since_start_ns < 0
46
+
47
+ {
48
+ thread_id: thread_id.to_s,
49
+ stack_id: stack_id,
50
+ elapsed_since_start_ns: elapsed_since_start_ns.to_s
51
+ }
52
+ }.compact
53
+ }
54
+ end
55
+
56
+ def frames
57
+ funcs = stack_table_hash[:frame_table].fetch(:func)
58
+ lines = stack_table_hash[:func_table].fetch(:first_line)
59
+
60
+ funcs.map do |idx|
61
+ function, mod = split_module(stack_table_hash[:func_table][:name][idx])
62
+
63
+ abs_path = stack_table_hash[:func_table][:filename][idx]
64
+ in_app = in_app?(abs_path)
65
+ filename = compute_filename(abs_path, in_app)
66
+
67
+ {
68
+ function: function,
69
+ module: mod,
70
+ filename: filename,
71
+ abs_path: abs_path,
72
+ lineno: (lineno = lines[idx]) > 0 ? lineno : nil,
73
+ in_app: in_app
74
+ }.compact
75
+ end
76
+ end
77
+
78
+ def stacks
79
+ profile._stack_table.stack_count.times.map do |stack_id|
80
+ profile.stack(stack_id).frames.map(&:idx)
81
+ end
82
+ end
83
+
84
+ def stack_table_hash
85
+ @stack_table_hash ||= profile._stack_table.to_h
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "../profiler/helpers"
5
+ require_relative "output"
6
+
7
+ module Sentry
8
+ module Vernier
9
+ class Profiler
10
+ EMPTY_RESULT = {}.freeze
11
+
12
+ attr_reader :started, :event_id, :result
13
+
14
+ def initialize(configuration)
15
+ @event_id = SecureRandom.uuid.delete("-")
16
+
17
+ @started = false
18
+ @sampled = nil
19
+
20
+ @profiling_enabled = defined?(Vernier) && 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
24
+ @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
25
+ end
26
+
27
+ def set_initial_sample_decision(transaction_sampled)
28
+ unless @profiling_enabled
29
+ @sampled = false
30
+ return
31
+ end
32
+
33
+ unless transaction_sampled
34
+ @sampled = false
35
+ log("Discarding profile because transaction not sampled")
36
+ return
37
+ end
38
+
39
+ case @profiles_sample_rate
40
+ when 0.0
41
+ @sampled = false
42
+ log("Discarding profile because sample_rate is 0")
43
+ return
44
+ when 1.0
45
+ @sampled = true
46
+ return
47
+ else
48
+ @sampled = Random.rand < @profiles_sample_rate
49
+ end
50
+
51
+ log("Discarding profile due to sampling decision") unless @sampled
52
+ end
53
+
54
+ def start
55
+ return unless @sampled
56
+ return if @started
57
+
58
+ @started = ::Vernier.start_profile
59
+
60
+ log("Started")
61
+
62
+ @started
63
+ rescue RuntimeError => e
64
+ # TODO: once Vernier raises something more dedicated, we should catch that instead
65
+ if e.message.include?("Profile already started")
66
+ log("Not started since running elsewhere")
67
+ else
68
+ log("Failed to start: #{e.message}")
69
+ end
70
+ end
71
+
72
+ def stop
73
+ return unless @sampled
74
+ return unless @started
75
+
76
+ @result = ::Vernier.stop_profile
77
+
78
+ log("Stopped")
79
+ rescue RuntimeError => e
80
+ if e.message.include?("Profile not started")
81
+ log("Not stopped since not started")
82
+ else
83
+ log("Failed to stop Vernier: #{e.message}")
84
+ end
85
+ end
86
+
87
+ def active_thread_id
88
+ Thread.current.object_id
89
+ end
90
+
91
+ def to_hash
92
+ return EMPTY_RESULT unless @started
93
+
94
+ unless @sampled
95
+ record_lost_event(:sample_rate)
96
+ return EMPTY_RESULT
97
+ end
98
+
99
+ { **profile_meta, profile: output.to_h }
100
+ end
101
+
102
+ private
103
+
104
+ def log(message)
105
+ Sentry.logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" }
106
+ end
107
+
108
+ def record_lost_event(reason)
109
+ Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
110
+ end
111
+
112
+ def profile_meta
113
+ {
114
+ event_id: @event_id,
115
+ version: "1",
116
+ platform: "ruby"
117
+ }
118
+ end
119
+
120
+ def output
121
+ @output ||= Output.new(
122
+ result,
123
+ project_root: @project_root,
124
+ app_dirs_pattern: @app_dirs_pattern,
125
+ in_app_pattern: @in_app_pattern
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.20.1"
4
+ VERSION = "5.22.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -25,6 +25,7 @@ require "sentry/session_flusher"
25
25
  require "sentry/backpressure_monitor"
26
26
  require "sentry/cron/monitor_check_ins"
27
27
  require "sentry/metrics"
28
+ require "sentry/vernier/profiler"
28
29
 
29
30
  [
30
31
  "sentry/rake",
@@ -41,14 +42,16 @@ module Sentry
41
42
 
42
43
  CAPTURED_SIGNATURE = :@__sentry_captured
43
44
 
44
- LOGGER_PROGNAME = "sentry".freeze
45
+ LOGGER_PROGNAME = "sentry"
45
46
 
46
- SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze
47
+ SENTRY_TRACE_HEADER_NAME = "sentry-trace"
47
48
 
48
- BAGGAGE_HEADER_NAME = "baggage".freeze
49
+ BAGGAGE_HEADER_NAME = "baggage"
49
50
 
50
51
  THREAD_LOCAL = :sentry_hub
51
52
 
53
+ MUTEX = Mutex.new
54
+
52
55
  class << self
53
56
  # @!visibility private
54
57
  def exception_locals_tp
@@ -274,8 +277,10 @@ module Sentry
274
277
 
275
278
  @background_worker.shutdown
276
279
 
277
- @main_hub = nil
278
- Thread.current.thread_variable_set(THREAD_LOCAL, nil)
280
+ MUTEX.synchronize do
281
+ @main_hub = nil
282
+ Thread.current.thread_variable_set(THREAD_LOCAL, nil)
283
+ end
279
284
  end
280
285
 
281
286
  # Returns true if the SDK is initialized.
@@ -302,7 +307,7 @@ module Sentry
302
307
  #
303
308
  # @return [Hub]
304
309
  def get_main_hub
305
- @main_hub
310
+ MUTEX.synchronize { @main_hub }
306
311
  end
307
312
 
308
313
  # Takes an instance of Sentry::Breadcrumb and stores it to the current active scope.
@@ -609,3 +614,4 @@ require "sentry/redis"
609
614
  require "sentry/puma"
610
615
  require "sentry/graphql"
611
616
  require "sentry/faraday"
617
+ require "sentry/excon"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "lib/sentry/version"
2
4
 
3
5
  Gem::Specification.new do |spec|
data/sentry-ruby.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "lib/sentry/version"
2
4
 
3
5
  Gem::Specification.new do |spec|
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.20.1
4
+ version: 5.22.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: 2024-09-27 00:00:00.000000000 Z
11
+ date: 2024-12-04 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.20.1
19
+ version: 5.22.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.20.1
26
+ version: 5.22.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: concurrent-ruby
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -77,9 +77,12 @@ files:
77
77
  - lib/sentry/cron/monitor_schedule.rb
78
78
  - lib/sentry/dsn.rb
79
79
  - lib/sentry/envelope.rb
80
+ - lib/sentry/envelope/item.rb
80
81
  - lib/sentry/error_event.rb
81
82
  - lib/sentry/event.rb
82
83
  - lib/sentry/exceptions.rb
84
+ - lib/sentry/excon.rb
85
+ - lib/sentry/excon/middleware.rb
83
86
  - lib/sentry/faraday.rb
84
87
  - lib/sentry/graphql.rb
85
88
  - lib/sentry/hub.rb
@@ -106,6 +109,7 @@ files:
106
109
  - lib/sentry/metrics/timing.rb
107
110
  - lib/sentry/net/http.rb
108
111
  - lib/sentry/profiler.rb
112
+ - lib/sentry/profiler/helpers.rb
109
113
  - lib/sentry/propagation_context.rb
110
114
  - lib/sentry/puma.rb
111
115
  - lib/sentry/rack.rb
@@ -113,6 +117,7 @@ files:
113
117
  - lib/sentry/rake.rb
114
118
  - lib/sentry/redis.rb
115
119
  - lib/sentry/release_detector.rb
120
+ - lib/sentry/rspec.rb
116
121
  - lib/sentry/scope.rb
117
122
  - lib/sentry/session.rb
118
123
  - lib/sentry/session_flusher.rb
@@ -135,6 +140,8 @@ files:
135
140
  - lib/sentry/utils/logging_helper.rb
136
141
  - lib/sentry/utils/real_ip.rb
137
142
  - lib/sentry/utils/request_id.rb
143
+ - lib/sentry/vernier/output.rb
144
+ - lib/sentry/vernier/profiler.rb
138
145
  - lib/sentry/version.rb
139
146
  - sentry-ruby-core.gemspec
140
147
  - sentry-ruby.gemspec
@@ -160,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
167
  - !ruby/object:Gem::Version
161
168
  version: '0'
162
169
  requirements: []
163
- rubygems_version: 3.5.16
170
+ rubygems_version: 3.5.22
164
171
  signing_key:
165
172
  specification_version: 4
166
173
  summary: A gem that provides a client interface for the Sentry error logger