1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
# frozen_string_literal: true
module ErrorTracking
class SentryClient
include SentryClient::Event
include SentryClient::Projects
include SentryClient::Issue
include SentryClient::Repo
include SentryClient::IssueLink
Error = Class.new(StandardError)
MissingKeysError = Class.new(StandardError)
InvalidFieldValueError = Class.new(StandardError)
ResponseInvalidSizeError = Class.new(StandardError)
RESPONSE_SIZE_LIMIT = 1.megabyte
private_constant :RESPONSE_SIZE_LIMIT
# The bytes size of a JSON payload is different from what DeepSize
# calculates which is Ruby's object size.
#
# This factor accounts for the difference.
#
# See https://gitlab.com/gitlab-org/gitlab/-/issues/393029#note_1289914133
RESPONSE_MEMORY_SIZE_LIMIT = RESPONSE_SIZE_LIMIT * 5
attr_accessor :url, :token
def initialize(api_url, token)
@url = api_url
@token = token
end
private
def validate_size(response)
bytesize = response.body.bytesize
if bytesize > RESPONSE_SIZE_LIMIT
limit = ActiveSupport::NumberHelper.number_to_human_size(RESPONSE_SIZE_LIMIT)
message = "Sentry API response is too big. Limit is #{limit}. Got #{bytesize} bytes."
raise ResponseInvalidSizeError, message
end
parsed = response.parsed_response
return if Gitlab::Utils::DeepSize.new(parsed, max_size: RESPONSE_MEMORY_SIZE_LIMIT).valid?
limit = ActiveSupport::NumberHelper.number_to_human_size(RESPONSE_MEMORY_SIZE_LIMIT)
message = "Sentry API response memory footprint is too big. Limit is #{limit}."
raise ResponseInvalidSizeError, message
end
def api_urls
@api_urls ||= SentryClient::ApiUrls.new(@url)
end
def handle_mapping_exceptions
yield
rescue KeyError => e
Gitlab::ErrorTracking.track_exception(e)
raise MissingKeysError, "Sentry API response is missing keys. #{e.message}"
end
def request_params
{
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{@token}"
},
follow_redirects: false
}
end
def http_get(url, params = {})
http_request do
Gitlab::HTTP.get(url, **request_params.merge(params))
end
end
def http_put(url, params = {})
http_request do
Gitlab::HTTP.put(url, **request_params.merge(body: params.to_json))
end
end
def http_post(url, params = {})
http_request do
Gitlab::HTTP.post(url, **request_params.merge(body: params.to_json))
end
end
def http_request(&block)
response = handle_request_exceptions(&block)
handle_response(response)
end
def handle_request_exceptions
yield
rescue Gitlab::HTTP::Error => e
Gitlab::ErrorTracking.track_exception(e)
raise_error 'Error when connecting to Sentry'
rescue Net::OpenTimeout
raise_error 'Connection to Sentry timed out'
rescue SocketError
raise_error 'Received SocketError when trying to connect to Sentry'
rescue OpenSSL::SSL::SSLError
raise_error 'Sentry returned invalid SSL data'
rescue Errno::ECONNREFUSED
raise_error 'Connection refused'
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e)
raise_error "Sentry request failed due to #{e.class}"
end
def handle_response(response)
raise_error "Sentry response status code: #{response.code}" unless response.code.between?(200, 204)
validate_size(response)
{ body: response.parsed_response, headers: response.headers }
end
def raise_error(message)
raise SentryClient::Error, message
end
def ensure_numeric!(field, value)
return value if /\A\d+\z/.match?(value)
raise_invalid_field_value!(field, "#{value.inspect} is not numeric")
end
def raise_invalid_field_value!(field, message)
raise InvalidFieldValueError, %(Sentry API response contains invalid value for field "#{field}": #{message})
end
end
end
|