sentry-ruby-core 5.20.1 → 5.22.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 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