sentry-ruby 5.10.0 → 5.26.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -1
  3. data/Gemfile +12 -13
  4. data/README.md +26 -11
  5. data/Rakefile +9 -11
  6. data/bin/console +2 -0
  7. data/lib/sentry/attachment.rb +40 -0
  8. data/lib/sentry/background_worker.rb +11 -5
  9. data/lib/sentry/backpressure_monitor.rb +45 -0
  10. data/lib/sentry/backtrace.rb +12 -9
  11. data/lib/sentry/baggage.rb +7 -7
  12. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  13. data/lib/sentry/breadcrumb.rb +13 -6
  14. data/lib/sentry/check_in_event.rb +61 -0
  15. data/lib/sentry/client.rb +214 -25
  16. data/lib/sentry/configuration.rb +221 -38
  17. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  18. data/lib/sentry/cron/configuration.rb +23 -0
  19. data/lib/sentry/cron/monitor_check_ins.rb +77 -0
  20. data/lib/sentry/cron/monitor_config.rb +53 -0
  21. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  22. data/lib/sentry/dsn.rb +4 -4
  23. data/lib/sentry/envelope/item.rb +88 -0
  24. data/lib/sentry/envelope.rb +2 -68
  25. data/lib/sentry/error_event.rb +2 -2
  26. data/lib/sentry/event.rb +28 -47
  27. data/lib/sentry/excon/middleware.rb +77 -0
  28. data/lib/sentry/excon.rb +10 -0
  29. data/lib/sentry/faraday.rb +77 -0
  30. data/lib/sentry/graphql.rb +9 -0
  31. data/lib/sentry/hub.rb +138 -6
  32. data/lib/sentry/integrable.rb +10 -0
  33. data/lib/sentry/interface.rb +1 -0
  34. data/lib/sentry/interfaces/exception.rb +5 -3
  35. data/lib/sentry/interfaces/mechanism.rb +20 -0
  36. data/lib/sentry/interfaces/request.rb +8 -8
  37. data/lib/sentry/interfaces/single_exception.rb +13 -9
  38. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  39. data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
  40. data/lib/sentry/linecache.rb +3 -3
  41. data/lib/sentry/log_event.rb +206 -0
  42. data/lib/sentry/log_event_buffer.rb +75 -0
  43. data/lib/sentry/logger.rb +1 -1
  44. data/lib/sentry/metrics/aggregator.rb +248 -0
  45. data/lib/sentry/metrics/configuration.rb +47 -0
  46. data/lib/sentry/metrics/counter_metric.rb +25 -0
  47. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  48. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  49. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  50. data/lib/sentry/metrics/metric.rb +19 -0
  51. data/lib/sentry/metrics/set_metric.rb +28 -0
  52. data/lib/sentry/metrics/timing.rb +51 -0
  53. data/lib/sentry/metrics.rb +56 -0
  54. data/lib/sentry/net/http.rb +27 -44
  55. data/lib/sentry/profiler/helpers.rb +46 -0
  56. data/lib/sentry/profiler.rb +41 -60
  57. data/lib/sentry/propagation_context.rb +135 -0
  58. data/lib/sentry/puma.rb +12 -5
  59. data/lib/sentry/rack/capture_exceptions.rb +17 -8
  60. data/lib/sentry/rack.rb +2 -2
  61. data/lib/sentry/rake.rb +4 -15
  62. data/lib/sentry/redis.rb +10 -4
  63. data/lib/sentry/release_detector.rb +5 -5
  64. data/lib/sentry/rspec.rb +91 -0
  65. data/lib/sentry/scope.rb +75 -39
  66. data/lib/sentry/session.rb +2 -2
  67. data/lib/sentry/session_flusher.rb +15 -43
  68. data/lib/sentry/span.rb +92 -8
  69. data/lib/sentry/std_lib_logger.rb +50 -0
  70. data/lib/sentry/structured_logger.rb +138 -0
  71. data/lib/sentry/test_helper.rb +42 -13
  72. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  73. data/lib/sentry/transaction.rb +44 -43
  74. data/lib/sentry/transaction_event.rb +10 -6
  75. data/lib/sentry/transport/configuration.rb +73 -1
  76. data/lib/sentry/transport/http_transport.rb +71 -41
  77. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  78. data/lib/sentry/transport.rb +53 -49
  79. data/lib/sentry/utils/argument_checking_helper.rb +12 -0
  80. data/lib/sentry/utils/env_helper.rb +21 -0
  81. data/lib/sentry/utils/http_tracing.rb +74 -0
  82. data/lib/sentry/utils/logging_helper.rb +10 -7
  83. data/lib/sentry/utils/real_ip.rb +2 -2
  84. data/lib/sentry/utils/request_id.rb +1 -1
  85. data/lib/sentry/utils/uuid.rb +13 -0
  86. data/lib/sentry/vernier/output.rb +89 -0
  87. data/lib/sentry/vernier/profiler.rb +132 -0
  88. data/lib/sentry/version.rb +1 -1
  89. data/lib/sentry-ruby.rb +206 -35
  90. data/sentry-ruby-core.gemspec +3 -1
  91. data/sentry-ruby.gemspec +15 -6
  92. metadata +61 -11
@@ -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", "log" 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,74 +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 * 200
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
- def to_s
22
- [JSON.generate(@headers), JSON.generate(@payload)].join("\n")
23
- end
24
-
25
- def serialize
26
- result = to_s
27
-
28
- if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
29
- remove_breadcrumbs!
30
- result = to_s
31
- end
32
-
33
- if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
34
- reduce_stacktrace!
35
- result = to_s
36
- end
37
-
38
- [result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE]
39
- end
40
-
41
- def size_breakdown
42
- payload.map do |key, value|
43
- "#{key}: #{JSON.generate(value).bytesize}"
44
- end.join(", ")
45
- end
46
-
47
- private
48
-
49
- def remove_breadcrumbs!
50
- if payload.key?(:breadcrumbs)
51
- payload.delete(:breadcrumbs)
52
- elsif payload.key?("breadcrumbs")
53
- payload.delete("breadcrumbs")
54
- end
55
- end
56
-
57
- def reduce_stacktrace!
58
- if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
59
- exceptions.each do |exception|
60
- # 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
61
- traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
62
-
63
- if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
64
- size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
65
- traces.replace(
66
- traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
67
- )
68
- end
69
- end
70
- end
71
- end
72
- end
73
-
74
6
  attr_accessor :headers, :items
75
7
 
76
8
  def initialize(headers = {})
@@ -91,3 +23,5 @@ module Sentry
91
23
  end
92
24
  end
93
25
  end
26
+
27
+ require_relative "envelope/item"
@@ -27,12 +27,12 @@ module Sentry
27
27
  end
28
28
 
29
29
  # @!visibility private
30
- def add_exception_interface(exception)
30
+ def add_exception_interface(exception, mechanism:)
31
31
  if exception.respond_to?(:sentry_context)
32
32
  @extra.merge!(exception.sentry_context)
33
33
  end
34
34
 
35
- @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
35
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder, mechanism: mechanism)
36
36
  end
37
37
  end
38
38
  end
data/lib/sentry/event.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'socket'
4
- require 'securerandom'
5
- require 'sentry/interface'
6
- require 'sentry/backtrace'
7
- require 'sentry/utils/real_ip'
8
- require 'sentry/utils/request_id'
9
- require 'sentry/utils/custom_inspection'
3
+ require "socket"
4
+ require "securerandom"
5
+ require "sentry/interface"
6
+ require "sentry/backtrace"
7
+ require "sentry/utils/real_ip"
8
+ require "sentry/utils/request_id"
9
+ require "sentry/utils/custom_inspection"
10
+ require "sentry/utils/uuid"
10
11
 
11
12
  module Sentry
12
13
  # This is an abstract class that defines the shared attributes of an event.
@@ -14,16 +15,16 @@ module Sentry
14
15
  class Event
15
16
  TYPE = "event"
16
17
  # These are readable attributes.
17
- SERIALIZEABLE_ATTRIBUTES = %i(
18
+ SERIALIZEABLE_ATTRIBUTES = %i[
18
19
  event_id level timestamp
19
20
  release environment server_name modules
20
21
  message user tags contexts extra
21
22
  fingerprint breadcrumbs transaction transaction_info
22
23
  platform sdk type
23
- )
24
+ ]
24
25
 
25
26
  # These are writable attributes.
26
- WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
27
+ WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i[type timestamp level]
27
28
 
28
29
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
29
30
 
@@ -37,12 +38,20 @@ module Sentry
37
38
  # @return [RequestInterface]
38
39
  attr_reader :request
39
40
 
41
+ # Dynamic Sampling Context (DSC) that gets attached
42
+ # as the trace envelope header in the transport.
43
+ # @return [Hash, nil]
44
+ attr_accessor :dynamic_sampling_context
45
+
46
+ # @return [Array<Attachment>]
47
+ attr_accessor :attachments
48
+
40
49
  # @param configuration [Configuration]
41
50
  # @param integration_meta [Hash, nil]
42
51
  # @param message [String, nil]
43
52
  def initialize(configuration:, integration_meta: nil, message: nil)
44
53
  # Set some simple default values
45
- @event_id = SecureRandom.uuid.delete("-")
54
+ @event_id = Utils.uuid
46
55
  @timestamp = Sentry.utc_now.iso8601
47
56
  @platform = :ruby
48
57
  @type = self.class::TYPE
@@ -52,8 +61,10 @@ module Sentry
52
61
  @extra = {}
53
62
  @contexts = {}
54
63
  @tags = {}
64
+ @attachments = []
55
65
 
56
66
  @fingerprint = []
67
+ @dynamic_sampling_context = nil
57
68
 
58
69
  # configuration data that's directly used by events
59
70
  @server_name = configuration.server_name
@@ -70,34 +81,6 @@ module Sentry
70
81
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
71
82
  end
72
83
 
73
- class << self
74
- # @!visibility private
75
- def get_log_message(event_hash)
76
- message = event_hash[:message] || event_hash['message']
77
-
78
- return message unless message.nil? || message.empty?
79
-
80
- message = get_message_from_exception(event_hash)
81
-
82
- return message unless message.nil? || message.empty?
83
-
84
- message = event_hash[:transaction] || event_hash["transaction"]
85
-
86
- return message unless message.nil? || message.empty?
87
-
88
- '<no message value>'
89
- end
90
-
91
- # @!visibility private
92
- def get_message_from_exception(event_hash)
93
- if exception = event_hash.dig(:exception, :values, 0)
94
- "#{exception[:type]}: #{exception[:value]}"
95
- elsif exception = event_hash.dig("exception", "values", 0)
96
- "#{exception["type"]}: #{exception["value"]}"
97
- end
98
- end
99
- end
100
-
101
84
  # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
102
85
  # @return [Configuration]
103
86
  def configuration
@@ -126,9 +109,7 @@ module Sentry
126
109
  unless request || env.empty?
127
110
  add_request_interface(env)
128
111
 
129
- if @send_default_pii
130
- user[:ip_address] = calculate_real_ip_from_rack(env)
131
- end
112
+ user[:ip_address] ||= calculate_real_ip_from_rack(env) if @send_default_pii
132
113
 
133
114
  if request_id = Utils::RequestId.read_from(env)
134
115
  tags[:request_id] = request_id
@@ -167,11 +148,11 @@ module Sentry
167
148
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
168
149
  def calculate_real_ip_from_rack(env)
169
150
  Utils::RealIp.new(
170
- :remote_addr => env["REMOTE_ADDR"],
171
- :client_ip => env["HTTP_CLIENT_IP"],
172
- :real_ip => env["HTTP_X_REAL_IP"],
173
- :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
174
- :trusted_proxies => @trusted_proxies
151
+ remote_addr: env["REMOTE_ADDR"],
152
+ client_ip: env["HTTP_CLIENT_IP"],
153
+ real_ip: env["HTTP_X_REAL_IP"],
154
+ forwarded_for: env["HTTP_X_FORWARDED_FOR"],
155
+ trusted_proxies: @trusted_proxies
175
156
  ).calculate_ip
176
157
  end
177
158
  end
@@ -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
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Faraday
5
+ OP_NAME = "http.client"
6
+
7
+ module Connection
8
+ # Since there's no way to preconfigure Faraday connections and add our instrumentation
9
+ # by default, we need to extend the connection constructor and do it there
10
+ #
11
+ # @see https://lostisland.github.io/faraday/#/customization/index?id=configuration
12
+ def initialize(url = nil, options = nil)
13
+ super
14
+
15
+ # Ensure that we attach instrumentation only if the adapter is not net/http
16
+ # because if is is, then the net/http instrumentation will take care of it
17
+ if builder.adapter.name != "Faraday::Adapter::NetHttp"
18
+ # Make sure that it's going to be the first middleware so that it can capture
19
+ # the entire request processing involving other middlewares
20
+ builder.insert(0, ::Faraday::Request::Instrumentation, name: OP_NAME, instrumenter: Instrumenter.new)
21
+ end
22
+ end
23
+ end
24
+
25
+ class Instrumenter
26
+ SPAN_ORIGIN = "auto.http.faraday"
27
+ BREADCRUMB_CATEGORY = "http"
28
+
29
+ include Utils::HttpTracing
30
+
31
+ def instrument(op_name, env, &block)
32
+ return block.call unless Sentry.initialized?
33
+
34
+ Sentry.with_child_span(op: op_name, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span|
35
+ request_info = extract_request_info(env)
36
+
37
+ if propagate_trace?(request_info[:url])
38
+ set_propagation_headers(env[:request_headers])
39
+ end
40
+
41
+ res = block.call
42
+ response_status = res.status
43
+
44
+ if record_sentry_breadcrumb?
45
+ record_sentry_breadcrumb(request_info, response_status)
46
+ end
47
+
48
+ if sentry_span
49
+ set_span_info(sentry_span, request_info, response_status)
50
+ end
51
+
52
+ res
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def extract_request_info(env)
59
+ url = env[:url].scheme + "://" + env[:url].host + env[:url].path
60
+ result = { method: env[:method].to_s.upcase, url: url }
61
+
62
+ if Sentry.configuration.send_default_pii
63
+ result[:query] = env[:url].query
64
+ result[:body] = env[:body]
65
+ end
66
+
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ Sentry.register_patch(:faraday) do
74
+ if defined?(::Faraday)
75
+ ::Faraday::Connection.prepend(Sentry::Faraday::Connection)
76
+ end
77
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sentry.register_patch(:graphql) do |config|
4
+ if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with)
5
+ ::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true)
6
+ else
7
+ config.logger.warn(Sentry::LOGGER_PROGNAME) { "You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile." }
8
+ end
9
+ end