sentry-raven/lib/raven/client.rb
# frozen_string_literal: true
require 'base64'
require 'json'
require 'zlib'
require "raven/transports"
module Raven
# Encodes events and sends them to the Sentry server.
class Client
PROTOCOL_VERSION = '5'
USER_AGENT = "raven-ruby/#{Raven::VERSION}"
CONTENT_TYPE = 'application/json'
attr_accessor :configuration
def initialize(configuration)
@configuration = configuration
@processors = configuration.processors.map { |v| v.new(self) }
@state = ClientState.new
end
def send_event(event, hint = nil)
return false unless configuration.sending_allowed?(event)
event = configuration.before_send.call(event, hint) if configuration.before_send
if event.nil?
configuration.logger.info "Discarded event because before_send returned nil"
return
end
# Convert to hash
event = event.to_hash
unless @state.should_try?
failed_send(nil, event)
return
end
event_id = event[:event_id] || event['event_id']
configuration.logger.info "Sending event #{event_id} to Sentry"
content_type, encoded_data = encode(event)
begin
transport.send_event(generate_auth_header, encoded_data,
:content_type => content_type)
successful_send
rescue => e
failed_send(e, event)
return
end
event
end
def transport
@transport ||=
case configuration.scheme
when 'http', 'https'
Transports::HTTP.new(configuration)
when 'stdout'
Transports::Stdout.new(configuration)
when 'dummy'
Transports::Dummy.new(configuration)
else
fail "Unknown transport scheme '#{configuration.scheme}'"
end
end
private
def encode(event)
hash = @processors.reduce(event.to_hash) { |a, e| e.process(a) }
encoded = JSON.fast_generate(hash)
case configuration.encoding
when 'gzip'
['application/octet-stream', Base64.strict_encode64(Zlib::Deflate.deflate(encoded))]
else
['application/json', encoded]
end
end
def get_message_from_exception(event)
(
event &&
event[:exception] &&
event[:exception][:values] &&
event[:exception][:values][0] &&
event[:exception][:values][0][:type] &&
event[:exception][:values][0][:value] &&
"#{event[:exception][:values][0][:type]}: #{event[:exception][:values][0][:value]}"
)
end
def get_log_message(event)
(event && event[:message]) || (event && event['message']) || get_message_from_exception(event) || '<no message value>'
end
def generate_auth_header
now = Time.now.to_i.to_s
fields = {
'sentry_version' => PROTOCOL_VERSION,
'sentry_client' => USER_AGENT,
'sentry_timestamp' => now,
'sentry_key' => configuration.public_key
}
fields['sentry_secret'] = configuration.secret_key unless configuration.secret_key.nil?
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
end
def successful_send
@state.success
end
def failed_send(e, event)
if e # exception was raised
@state.failure
configuration.logger.warn "Unable to record event with remote Sentry server (#{e.class} - #{e.message}):\n#{e.backtrace[0..10].join("\n")}"
else
configuration.logger.warn "Not sending event due to previous failure(s)."
end
configuration.logger.warn("Failed to submit event: #{get_log_message(event)}")
# configuration.transport_failure_callback can be false & nil
configuration.transport_failure_callback.call(event, e) if configuration.transport_failure_callback # rubocop:disable Style/SafeNavigation
end
end
class ClientState
def initialize
reset
end
def should_try?
return true if @status == :online
interval = @retry_after || [@retry_number, 6].min**2
return true if Time.now - @last_check >= interval
false
end
def failure(retry_after = nil)
@status = :error
@retry_number += 1
@last_check = Time.now
@retry_after = retry_after
end
def success
reset
end
def reset
@status = :online
@retry_number = 0
@last_check = nil
@retry_after = nil
end
def failed?
@status == :error
end
end
end