sentry-ruby-core 4.2.1 → 4.4.0.pre.beta.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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Sentry
4
4
  class TransactionEvent < Event
5
+ TYPE = "transaction"
6
+
5
7
  ATTRIBUTES = %i(
6
8
  event_id level timestamp start_timestamp
7
9
  release environment server_name modules
@@ -17,7 +19,7 @@ module Sentry
17
19
  end
18
20
 
19
21
  def type
20
- "transaction"
22
+ TYPE
21
23
  end
22
24
 
23
25
  def to_hash
@@ -6,12 +6,17 @@ module Sentry
6
6
  PROTOCOL_VERSION = '5'
7
7
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
8
8
 
9
+ include LoggingHelper
10
+
9
11
  attr_accessor :configuration
12
+ attr_reader :logger, :rate_limits
10
13
 
11
14
  def initialize(configuration)
12
15
  @configuration = configuration
16
+ @logger = configuration.logger
13
17
  @transport_configuration = configuration.transport
14
18
  @dsn = configuration.dsn
19
+ @rate_limits = {}
15
20
  end
16
21
 
17
22
  def send_data(data, options = {})
@@ -19,21 +24,57 @@ module Sentry
19
24
  end
20
25
 
21
26
  def send_event(event)
27
+ event_hash = event.to_hash
28
+ item_type = get_item_type(event_hash)
29
+
22
30
  unless configuration.sending_allowed?
23
- configuration.logger.debug(LOGGER_PROGNAME) { "Event not sent: #{configuration.error_messages}" }
31
+ log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}")
32
+
33
+ return
34
+ end
35
+
36
+ if is_rate_limited?(item_type)
37
+ log_info("Envelope [#{item_type}] not sent: rate limiting")
38
+
24
39
  return
25
40
  end
26
41
 
27
- encoded_data = prepare_encoded_event(event)
42
+ encoded_data = encode(event)
28
43
 
29
44
  return nil unless encoded_data
30
45
 
31
46
  send_data(encoded_data)
32
47
 
33
48
  event
34
- rescue => e
35
- failed_for_exception(e, event)
36
- nil
49
+ end
50
+
51
+ def is_rate_limited?(item_type)
52
+ # check category-specific limit
53
+ category_delay =
54
+ case item_type
55
+ when "transaction"
56
+ @rate_limits["transaction"]
57
+ else
58
+ @rate_limits["error"]
59
+ end
60
+
61
+ # check universal limit if not category limit
62
+ universal_delay = @rate_limits[nil]
63
+
64
+ delay =
65
+ if category_delay && universal_delay
66
+ if category_delay > universal_delay
67
+ category_delay
68
+ else
69
+ universal_delay
70
+ end
71
+ elsif category_delay
72
+ category_delay
73
+ else
74
+ universal_delay
75
+ end
76
+
77
+ !!delay && delay > Time.now
37
78
  end
38
79
 
39
80
  def generate_auth_header
@@ -48,38 +89,28 @@ module Sentry
48
89
  'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
49
90
  end
50
91
 
51
- def encode(event_hash)
52
- event_id = event_hash[:event_id] || event_hash['event_id']
53
- event_type = event_hash[:type] || event_hash['type']
92
+ def encode(event)
93
+ # Convert to hash
94
+ event_hash = event.to_hash
95
+
96
+ event_id = event_hash[:event_id] || event_hash["event_id"]
97
+ item_type = get_item_type(event_hash)
54
98
 
55
99
  envelope = <<~ENVELOPE
56
100
  {"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{Sentry.utc_now.iso8601}"}
57
- {"type":"#{event_type}","content_type":"application/json"}
101
+ {"type":"#{item_type}","content_type":"application/json"}
58
102
  #{JSON.generate(event_hash)}
59
103
  ENVELOPE
60
104
 
105
+ log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
106
+
61
107
  envelope
62
108
  end
63
109
 
64
110
  private
65
111
 
66
- def prepare_encoded_event(event)
67
- # Convert to hash
68
- event_hash = event.to_hash
69
-
70
- event_id = event_hash[:event_id] || event_hash["event_id"]
71
- event_type = event_hash[:type] || event_hash["type"]
72
- configuration.logger.info(LOGGER_PROGNAME) { "Sending #{event_type} #{event_id} to Sentry" }
73
- encode(event_hash)
74
- end
75
-
76
- def failed_for_exception(e, event)
77
- configuration.logger.warn(LOGGER_PROGNAME) { "Unable to record event with remote Sentry server (#{e.class} - #{e.message}):\n#{e.backtrace[0..10].join("\n")}" }
78
- log_not_sending(event)
79
- end
80
-
81
- def log_not_sending(event)
82
- configuration.logger.warn(LOGGER_PROGNAME) { "Failed to submit event. Unreported Event: #{Event.get_log_message(event.to_hash)}" }
112
+ def get_item_type(event_hash)
113
+ event_hash[:type] || event_hash["type"] || "event"
83
114
  end
84
115
  end
85
116
  end
@@ -1,12 +1,14 @@
1
1
  module Sentry
2
2
  class Transport
3
3
  class Configuration
4
- attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :http_adapter, :faraday_builder, :transport_class
4
+ attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :http_adapter, :faraday_builder,
5
+ :transport_class, :encoding
5
6
 
6
7
  def initialize
7
8
  @ssl_verification = true
8
9
  @open_timeout = 1
9
10
  @timeout = 2
11
+ @encoding = HTTPTransport::GZIP_ENCODING
10
12
  end
11
13
 
12
14
  def transport_class=(klass)
@@ -1,8 +1,16 @@
1
1
  require 'faraday'
2
+ require 'zlib'
2
3
 
3
4
  module Sentry
4
5
  class HTTPTransport < Transport
5
- CONTENT_TYPE = 'application/json'
6
+ GZIP_ENCODING = "gzip"
7
+ GZIP_THRESHOLD = 1024 * 30
8
+ CONTENT_TYPE = 'application/x-sentry-envelope'
9
+
10
+ DEFAULT_DELAY = 60
11
+ RETRY_AFTER_HEADER = "retry-after"
12
+ RATE_LIMIT_HEADER = "x-sentry-rate-limits"
13
+
6
14
  attr_reader :conn, :adapter
7
15
 
8
16
  def initialize(*args)
@@ -13,28 +21,106 @@ module Sentry
13
21
  end
14
22
 
15
23
  def send_data(data)
16
- conn.post @endpoint do |req|
24
+ encoding = ""
25
+
26
+ if should_compress?(data)
27
+ data = Zlib.gzip(data)
28
+ encoding = GZIP_ENCODING
29
+ end
30
+
31
+ response = conn.post @endpoint do |req|
17
32
  req.headers['Content-Type'] = CONTENT_TYPE
33
+ req.headers['Content-Encoding'] = encoding
18
34
  req.headers['X-Sentry-Auth'] = generate_auth_header
19
35
  req.body = data
20
36
  end
37
+
38
+ if has_rate_limited_header?(response.headers)
39
+ handle_rate_limited_response(response.headers)
40
+ end
21
41
  rescue Faraday::Error => e
22
42
  error_info = e.message
23
43
 
24
44
  if e.response
25
- error_info += "\nbody: #{e.response[:body]}"
26
- error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error']
45
+ if e.response[:status] == 429
46
+ handle_rate_limited_response(e.response[:headers])
47
+ else
48
+ error_info += "\nbody: #{e.response[:body]}"
49
+ error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error']
50
+ end
27
51
  end
28
52
 
29
- raise Sentry::Error, error_info
53
+ raise Sentry::ExternalError, error_info
30
54
  end
31
55
 
32
56
  private
33
57
 
58
+ def has_rate_limited_header?(headers)
59
+ headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
60
+ end
61
+
62
+ def handle_rate_limited_response(headers)
63
+ rate_limits =
64
+ if rate_limits = headers[RATE_LIMIT_HEADER]
65
+ parse_rate_limit_header(rate_limits)
66
+ elsif retry_after = headers[RETRY_AFTER_HEADER]
67
+ # although Sentry doesn't send a date string back
68
+ # based on HTTP specification, this could be a date string (instead of an integer)
69
+ retry_after = retry_after.to_i
70
+ retry_after = DEFAULT_DELAY if retry_after == 0
71
+
72
+ { nil => Time.now + retry_after }
73
+ else
74
+ { nil => Time.now + DEFAULT_DELAY }
75
+ end
76
+
77
+ rate_limits.each do |category, limit|
78
+ if current_limit = @rate_limits[category]
79
+ if current_limit < limit
80
+ @rate_limits[category] = limit
81
+ end
82
+ else
83
+ @rate_limits[category] = limit
84
+ end
85
+ end
86
+ end
87
+
88
+ def parse_rate_limit_header(rate_limit_header)
89
+ time = Time.now
90
+
91
+ result = {}
92
+
93
+ limits = rate_limit_header.split(",")
94
+ limits.each do |limit|
95
+ next if limit.nil? || limit.empty?
96
+
97
+ begin
98
+ retry_after, categories = limit.strip.split(":").first(2)
99
+ retry_after = time + retry_after.to_i
100
+ categories = categories.split(";")
101
+
102
+ if categories.empty?
103
+ result[nil] = retry_after
104
+ else
105
+ categories.each do |category|
106
+ result[category] = retry_after
107
+ end
108
+ end
109
+ rescue StandardError
110
+ end
111
+ end
112
+
113
+ result
114
+ end
115
+
116
+ def should_compress?(data)
117
+ @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
118
+ end
119
+
34
120
  def set_conn
35
121
  server = @dsn.server
36
122
 
37
- configuration.logger.debug(LOGGER_PROGNAME) { "Sentry HTTP Transport connecting to #{server}" }
123
+ log_debug("Sentry HTTP Transport connecting to #{server}")
38
124
 
39
125
  Faraday.new(server, :ssl => ssl_configuration, :proxy => @transport_configuration.proxy) do |builder|
40
126
  @transport_configuration.faraday_builder&.call(builder)
@@ -0,0 +1,24 @@
1
+ module Sentry
2
+ module LoggingHelper
3
+ def log_error(message, exception, debug: false)
4
+ message = "#{message}: #{exception.message}"
5
+ message += "\n#{exception.backtrace.join("\n")}" if debug
6
+
7
+ logger.error(LOGGER_PROGNAME) do
8
+ message
9
+ end
10
+ end
11
+
12
+ def log_info(message)
13
+ logger.info(LOGGER_PROGNAME) { message }
14
+ end
15
+
16
+ def log_debug(message)
17
+ logger.debug(LOGGER_PROGNAME) { message }
18
+ end
19
+
20
+ def log_warn(message)
21
+ logger.warn(LOGGER_PROGNAME) { message }
22
+ end
23
+ end
24
+ end
@@ -29,7 +29,7 @@ module Sentry
29
29
  @client_ip = client_ip
30
30
  @real_ip = real_ip
31
31
  @forwarded_for = forwarded_for
32
- @trusted_proxies = (LOCAL_ADDRESSES + Array(trusted_proxies)).map { |proxy| IPAddr.new(proxy) }.uniq
32
+ @trusted_proxies = (LOCAL_ADDRESSES + Array(trusted_proxies)).map { |proxy| IPAddr.new(proxy.to_s) }.uniq
33
33
  end
34
34
 
35
35
  def calculate_ip
@@ -1,3 +1,3 @@
1
1
  module Sentry
2
- VERSION = "4.2.1"
2
+ VERSION = "4.4.0-beta.0"
3
3
  end
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: 4.2.1
4
+ version: 4.4.0.pre.beta.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-09 00:00:00.000000000 Z
11
+ date: 2021-04-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -71,6 +71,7 @@ files:
71
71
  - lib/sentry/core_ext/object/duplicable.rb
72
72
  - lib/sentry/dsn.rb
73
73
  - lib/sentry/event.rb
74
+ - lib/sentry/exceptions.rb
74
75
  - lib/sentry/hub.rb
75
76
  - lib/sentry/integrable.rb
76
77
  - lib/sentry/interface.rb
@@ -78,9 +79,11 @@ files:
78
79
  - lib/sentry/interfaces/request.rb
79
80
  - lib/sentry/interfaces/single_exception.rb
80
81
  - lib/sentry/interfaces/stacktrace.rb
82
+ - lib/sentry/interfaces/stacktrace_builder.rb
81
83
  - lib/sentry/interfaces/threads.rb
82
84
  - lib/sentry/linecache.rb
83
85
  - lib/sentry/logger.rb
86
+ - lib/sentry/net/http.rb
84
87
  - lib/sentry/rack.rb
85
88
  - lib/sentry/rack/capture_exceptions.rb
86
89
  - lib/sentry/rack/deprecations.rb
@@ -95,6 +98,7 @@ files:
95
98
  - lib/sentry/transport/http_transport.rb
96
99
  - lib/sentry/utils/argument_checking_helper.rb
97
100
  - lib/sentry/utils/exception_cause_chain.rb
101
+ - lib/sentry/utils/logging_helper.rb
98
102
  - lib/sentry/utils/real_ip.rb
99
103
  - lib/sentry/utils/request_id.rb
100
104
  - lib/sentry/version.rb
@@ -118,11 +122,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
118
122
  version: '2.4'
119
123
  required_rubygems_version: !ruby/object:Gem::Requirement
120
124
  requirements:
121
- - - ">="
125
+ - - ">"
122
126
  - !ruby/object:Gem::Version
123
- version: '0'
127
+ version: 1.3.1
124
128
  requirements: []
125
- rubygems_version: 3.0.3
129
+ rubygems_version: 3.0.3.1
126
130
  signing_key:
127
131
  specification_version: 4
128
132
  summary: A gem that provides a client interface for the Sentry error logger