require 'uri' module Raven class Configuration # rubocop:disable Metrics/ClassLength # Directories to be recognized as part of your app. e.g. if you # have an `engines` dir at the root of your project, you may want # to set this to something like /(app|config|engines|lib)/ attr_accessor :app_dirs_pattern # Provide an object that responds to `call` to send events asynchronously. # E.g.: lambda { |event| Thread.new { Raven.send_event(event) } } attr_reader :async alias async? async # Number of lines of code context to capture, or nil for none attr_accessor :context_lines # RACK_ENV by default. attr_reader :current_environment # Encoding type for event bodies. Must be :json or :gzip. attr_reader :encoding # Whitelist of environments that will send notifications to Sentry. Array of Strings. attr_accessor :environments # Logger 'progname's to exclude from breadcrumbs attr_accessor :exclude_loggers # Array of exception classes that should never be sent. See IGNORE_DEFAULT. # You should probably append to this rather than overwrite it. attr_accessor :excluded_exceptions # Boolean to check nested exceptions when deciding if to exclude. Defaults to false attr_accessor :inspect_exception_causes_for_exclusion alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion # DSN component - set automatically if DSN provided attr_accessor :host # The Faraday adapter to be used. Will default to Net::HTTP when not set. attr_accessor :http_adapter # A Proc yeilding the faraday builder allowing for further configuration # of the faraday adapter attr_accessor :faraday_builder # You may provide your own LineCache for matching paths with source files. # This may be useful if you need to get source code from places other than # the disk. See Raven::LineCache for the required interface you must implement. attr_accessor :linecache # Logger used by Raven. In Rails, this is the Rails logger, otherwise # Raven provides its own Raven::Logger. attr_accessor :logger # Timeout waiting for the Sentry server connection to open in seconds attr_accessor :open_timeout # DSN component - set automatically if DSN provided attr_accessor :path # DSN component - set automatically if DSN provided attr_accessor :port # Processors to run on data before sending upstream. See DEFAULT_PROCESSORS. # You should probably append to this rather than overwrite it. attr_accessor :processors # Project ID number to send to the Sentry server # If you provide a DSN, this will be set automatically. attr_accessor :project_id # Project directory root for in_app detection. Could be Rails root, etc. # Set automatically for Rails. attr_reader :project_root # Proxy information to pass to the HTTP adapter (via Faraday) attr_accessor :proxy # Public key for authentication with the Sentry server # If you provide a DSN, this will be set automatically. attr_accessor :public_key # Turns on ActiveSupport breadcrumbs integration attr_accessor :rails_activesupport_breadcrumbs # Rails catches exceptions in the ActionDispatch::ShowExceptions or # ActionDispatch::DebugExceptions middlewares, depending on the environment. # When `rails_report_rescued_exceptions` is true (it is by default), Raven # will report exceptions even when they are rescued by these middlewares. attr_accessor :rails_report_rescued_exceptions # Release tag to be passed with every event sent to Sentry. # We automatically try to set this to a git SHA or Capistrano release. attr_accessor :release # The sampling factor to apply to events. A value of 0.0 will not send # any events, and a value of 1.0 will send 100% of events. attr_accessor :sample_rate # Boolean - sanitize values that look like credit card numbers attr_accessor :sanitize_credit_cards # By default, Sentry censors Hash values when their keys match things like # "secret", "password", etc. Provide an array of Strings that, when matched in # a hash key, will be censored and not sent to Sentry. attr_accessor :sanitize_fields # If you're sure you want to override the default sanitization values, you can # add to them to an array of Strings here, e.g. %w(authorization password) attr_accessor :sanitize_fields_excluded # Sanitize additional HTTP headers - only Authorization is removed by default. attr_accessor :sanitize_http_headers # DSN component - set automatically if DSN provided. # Otherwise, can be one of "http", "https", or "dummy" attr_accessor :scheme # Secret key for authentication with the Sentry server # If you provide a DSN, this will be set automatically. # # This is deprecated and not necessary for newer Sentry installations any more. attr_accessor :secret_key # Include module versions in reports - boolean. attr_accessor :send_modules # Simple server string - set this to the DSN found on your Sentry settings. attr_reader :server attr_accessor :server_name # Provide a configurable callback to determine event capture. # Note that the object passed into the block will be a String (messages) or # an exception. # e.g. lambda { |exc_or_msg| exc_or_msg.some_attr == false } attr_reader :should_capture # Silences ready message when true. attr_accessor :silence_ready # SSL settings passed directly to Faraday's ssl option attr_accessor :ssl # The path to the SSL certificate file attr_accessor :ssl_ca_file # Should the SSL certificate of the server be verified? attr_accessor :ssl_verification # Default tags for events. Hash. attr_accessor :tags # Timeout when waiting for the server to return data in seconds. attr_accessor :timeout # Optional Proc, called when the Sentry server cannot be contacted for any reason # E.g. lambda { |event| Thread.new { MyJobProcessor.send_email(event) } } attr_reader :transport_failure_callback # Optional Proc, called before sending an event to the server/ # E.g.: lambda { |event, hint| event } # E.g.: lambda { |event, hint| nil } # E.g.: lambda { |event, hint| # event[:message] = 'a' # event # } attr_reader :before_send # Errors object - an Array that contains error messages. See # attr_reader :errors IGNORE_DEFAULT = [ 'AbstractController::ActionNotFound', 'ActionController::InvalidAuthenticityToken', 'ActionController::RoutingError', 'ActionController::UnknownAction', 'ActiveRecord::RecordNotFound', 'CGI::Session::CookieStore::TamperedWithCookie', 'Mongoid::Errors::DocumentNotFound', 'Sinatra::NotFound', 'ActiveJob::DeserializationError' ].freeze # Note the order - we have to remove circular references and bad characters # before passing to other processors. DEFAULT_PROCESSORS = [ Raven::Processor::RemoveCircularReferences, Raven::Processor::UTF8Conversion, Raven::Processor::SanitizeData, Raven::Processor::Cookies, Raven::Processor::PostData, Raven::Processor::HTTPHeaders ].freeze HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\ "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`".freeze LOG_PREFIX = "** [Raven] ".freeze MODULE_SEPARATOR = "::".freeze def initialize self.async = false self.context_lines = 3 self.current_environment = current_environment_from_env self.encoding = 'gzip' self.environments = [] self.exclude_loggers = [] self.excluded_exceptions = IGNORE_DEFAULT.dup self.inspect_exception_causes_for_exclusion = false self.linecache = ::Raven::LineCache.new self.logger = ::Raven::Logger.new(STDOUT) self.open_timeout = 1 self.processors = DEFAULT_PROCESSORS.dup self.project_root = detect_project_root self.rails_activesupport_breadcrumbs = false self.rails_report_rescued_exceptions = true self.release = detect_release self.sample_rate = 1.0 self.sanitize_credit_cards = true self.sanitize_fields = [] self.sanitize_fields_excluded = [] self.sanitize_http_headers = [] self.send_modules = true self.server = ENV['SENTRY_DSN'] self.server_name = server_name_from_env self.should_capture = false self.ssl_verification = true self.tags = {} self.timeout = 2 self.transport_failure_callback = false self.before_send = false end def server=(value) return if value.nil? uri = URI.parse(value) uri_path = uri.path.split('/') if uri.user # DSN-style string self.project_id = uri_path.pop self.public_key = uri.user self.secret_key = !(uri.password.nil? || uri.password.empty?) ? uri.password : nil end self.scheme = uri.scheme self.host = uri.host self.port = uri.port if uri.port self.path = uri_path.join('/') # For anyone who wants to read the base server string @server = "#{scheme}://#{host}" @server << ":#{port}" unless port == { 'http' => 80, 'https' => 443 }[scheme] @server << path end alias dsn= server= def encoding=(encoding) raise(Error, 'Unsupported encoding') unless %w(gzip json).include? encoding @encoding = encoding end def async=(value) unless value == false || value.respond_to?(:call) raise(ArgumentError, "async must be callable (or false to disable)") end @async = value end def transport_failure_callback=(value) unless value == false || value.respond_to?(:call) raise(ArgumentError, "transport_failure_callback must be callable (or false to disable)") end @transport_failure_callback = value end def should_capture=(value) unless value == false || value.respond_to?(:call) raise ArgumentError, "should_capture must be callable (or false to disable)" end @should_capture = value end def before_send=(value) unless value == false || value.respond_to?(:call) raise ArgumentError, "before_send must be callable (or false to disable)" end @before_send = value end # Allows config options to be read like a hash # # @param [Symbol] option Key for a given attribute def [](option) public_send(option) end def current_environment=(environment) @current_environment = environment.to_s end def capture_allowed?(message_or_exc = nil) @errors = [] valid? && capture_in_current_environment? && capture_allowed_by_callback?(message_or_exc) && sample_allowed? end # If we cannot capture, we cannot send. alias sending_allowed? capture_allowed? def error_messages @errors = [errors[0]] + errors[1..-1].map(&:downcase) # fix case of all but first errors.join(", ") end def project_root=(root_dir) @project_root = root_dir Backtrace::Line.instance_variable_set(:@in_app_pattern, nil) # blow away cache end def exception_class_allowed?(exc) if exc.is_a?(Raven::Error) # Try to prevent error reporting loops logger.debug "Refusing to capture Raven error: #{exc.inspect}" false elsif excluded_exception?(exc) logger.debug "User excluded error: #{exc.inspect}" false else true end end private def detect_project_root if defined? Rails.root # we are in a Rails application Rails.root.to_s else Dir.pwd end end def detect_release detect_release_from_git || detect_release_from_capistrano || detect_release_from_heroku rescue => ex logger.error "Error detecting release: #{ex.message}" end def excluded_exception?(incoming_exception) excluded_exceptions.any? do |excluded_exception| matches_exception?(get_exception_class(excluded_exception), incoming_exception) end end def get_exception_class(x) x.is_a?(Module) ? x : qualified_const_get(x) end def matches_exception?(excluded_exception_class, incoming_exception) if inspect_exception_causes_for_exclusion? Raven::Utils::ExceptionCauseChain.exception_to_array(incoming_exception).any? { |cause| excluded_exception_class === cause } else excluded_exception_class === incoming_exception end end # In Ruby <2.0 const_get can't lookup "SomeModule::SomeClass" in one go def qualified_const_get(x) x = x.to_s if !x.match(/::/) Object.const_get(x) else x.split(MODULE_SEPARATOR).reject(&:empty?).inject(Object) { |a, e| a.const_get(e) } end rescue NameError # There's no way to safely ask if a constant exist for an unknown string nil end def detect_release_from_heroku return unless running_on_heroku? return if ENV['CI'] logger.warn(HEROKU_DYNO_METADATA_MESSAGE) && return unless ENV['HEROKU_SLUG_COMMIT'] ENV['HEROKU_SLUG_COMMIT'] end def running_on_heroku? File.directory?("/etc/heroku") end def detect_release_from_capistrano revision_file = File.join(project_root, 'REVISION') revision_log = File.join(project_root, '..', 'revisions.log') if File.exist?(revision_file) File.read(revision_file).strip elsif File.exist?(revision_log) File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1') end end def detect_release_from_git Raven.sys_command("git rev-parse --short HEAD") if File.directory?(".git") end def capture_in_current_environment? return true unless environments.any? && !environments.include?(current_environment) @errors << "Not configured to send/capture in environment '#{current_environment}'" false end def capture_allowed_by_callback?(message_or_exc) return true if !should_capture || message_or_exc.nil? || should_capture.call(*[message_or_exc]) @errors << "should_capture returned false" false end def valid? return true if %w(server host path public_key project_id).all? { |k| public_send(k) } if server %w(server host path public_key project_id).map do |key| @errors << "No #{key} specified" unless public_send(key) end else @errors << "DSN not set" end false end def sample_allowed? return true if sample_rate == 1.0 if Random::DEFAULT.rand >= sample_rate @errors << "Excluded by random sample" false else true end end # Try to resolve the hostname to an FQDN, but fall back to whatever # the load name is. def resolve_hostname Socket.gethostname || Socket.gethostbyname(hostname).first rescue server_name end def current_environment_from_env ENV['SENTRY_CURRENT_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default' end def server_name_from_env if running_on_heroku? ENV['DYNO'] else resolve_hostname end end end end