module Raygun # client for the Raygun REST APIv1 # as per http://raygun.io/raygun-providers/rest-json-api?v=1 class Client ENV_IP_ADDRESS_KEYS = %w(action_dispatch.remote_ip raygun.remote_ip REMOTE_ADDR) NO_API_KEY_MESSAGE = "[RAYGUN] Just a note, you've got no API Key configured, which means we can't report exceptions. Specify your Raygun API key using Raygun#setup (find yours at https://app.raygun.io)" include HTTParty base_uri "https://api.raygun.io/" def initialize @api_key = require_api_key @headers = { "X-ApiKey" => @api_key } enable_http_proxy if Raygun.configuration.proxy_settings[:address] end def require_api_key Raygun.configuration.api_key || print_api_key_warning end def track_exception(exception_instance, env = {}) create_entry(build_payload_hash(exception_instance, env)) end private def enable_http_proxy self.class.http_proxy(Raygun.configuration.proxy_settings[:address], Raygun.configuration.proxy_settings[:port] || "80", Raygun.configuration.proxy_settings[:username], Raygun.configuration.proxy_settings[:password]) end def client_details { name: Raygun::CLIENT_NAME, version: Raygun::VERSION, clientUrl: Raygun::CLIENT_URL } end def error_details(exception) { className: exception.class.to_s, message: exception.message.to_s.encode('UTF-16', :undef => :replace, :invalid => :replace).encode('UTF-8'), stackTrace: (exception.backtrace || []).map { |line| stack_trace_for(line) } } end def stack_trace_for(line) # see http://www.ruby-doc.org/core-2.0/Exception.html#method-i-backtrace file_name, line_number, method = line.split(":") { lineNumber: line_number, fileName: file_name, methodName: method ? method.gsub(/^in `(.*?)'$/, "\\1") : "(none)" } end def hostname Socket.gethostname end def version Raygun.configuration.version end def user_information(env) env["raygun.affected_user"] end def affected_user_present?(env) !!env["raygun.affected_user"] end def rack_env ENV["RACK_ENV"] end def rails_env ENV["RAILS_ENV"] end def request_information(env) return {} if env.nil? || env.empty? { hostName: env["SERVER_NAME"], url: env["PATH_INFO"], httpMethod: env["REQUEST_METHOD"], iPAddress: "#{ip_address_from(env)}", queryString: Rack::Utils.parse_nested_query(env["QUERY_STRING"]), headers: headers(env), form: form_params(env), rawData: raw_data(env) } end def headers(rack_env) rack_env.select { |k, v| k.to_s.start_with?("HTTP_") }.inject({}) do |hsh, (k, v)| hsh[normalize_raygun_header_key(k)] = v hsh end end def normalize_raygun_header_key(key) key.sub(/^HTTP_/, '') .sub(/_/, ' ') .split.map(&:capitalize).join(' ') .sub(/ /, '-') end def form_params(env) params = action_dispatch_params(env) || rack_params(env) || {} filter_params(params, env["action_dispatch.parameter_filter"]) end def action_dispatch_params(env) env["action_dispatch.request.parameters"] end def rack_params(env) request = Rack::Request.new(env) request.params if env["rack.input"] end def raw_data(rack_env) request = Rack::Request.new(rack_env) unless request.form_data? form_params(rack_env) end end def filter_custom_data(env) params = env.delete(:custom_data) || {} filter_params(params, env["action_dispatch.parameter_filter"]) end # see http://raygun.io/raygun-providers/rest-json-api?v=1 def build_payload_hash(exception_instance, env = {}) custom_data = filter_custom_data(env) || {} tags = env.delete(:tags) || [] tags << rails_env || rack_env grouping_key = env.delete(:grouping_key) error_details = { machineName: hostname, version: version, client: client_details, error: error_details(exception_instance), userCustomData: Raygun.configuration.custom_data.merge(custom_data), tags: Raygun.configuration.tags.concat(tags).compact.uniq, request: request_information(env) } error_details.merge!(groupingKey: grouping_key) if grouping_key error_details.merge!(user: user_information(env)) if affected_user_present?(env) { occurredOn: Time.now.utc.iso8601, details: error_details } end def create_entry(payload_hash) self.class.post("/entries", verify_peer: true, verify: true, headers: @headers, body: JSON.generate(payload_hash)) end def filter_params(params_hash, extra_filter_keys = nil) if Raygun.configuration.filter_parameters.is_a?(Proc) filter_params_with_proc(params_hash, Raygun.configuration.filter_parameters) else filter_keys = (Array(extra_filter_keys) + Raygun.configuration.filter_parameters).map(&:to_s) filter_params_with_array(params_hash, filter_keys) end end def filter_params_with_proc(params_hash, proc) proc.call(params_hash) end def filter_params_with_array(params_hash, filter_keys) # Recursive filtering of (nested) hashes (params_hash || {}).inject({}) do |result, (k, v)| result[k] = case v when Hash filter_params_with_array(v, filter_keys) else filter_keys.any? { |fk| /#{fk}/i === k.to_s } ? "[FILTERED]" : v end result end end def ip_address_from(env_hash) ENV_IP_ADDRESS_KEYS.each do |key_to_try| return env_hash[key_to_try] unless env_hash[key_to_try].nil? || env_hash[key_to_try] == "" end "(Not Available)" end def print_api_key_warning $stderr.puts(NO_API_KEY_MESSAGE) end end end