sentry-ruby-core 4.2.2 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Sentry
2
- VERSION = "4.2.2"
2
+ VERSION = "4.4.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.2
4
+ version: 4.4.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-18 00:00:00.000000000 Z
11
+ date: 2021-05-03 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
@@ -122,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
126
  - !ruby/object:Gem::Version
123
127
  version: '0'
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