require 'forwardable' require 'honeybadger/version' require 'honeybadger/config' require 'honeybadger/context_manager' require 'honeybadger/notice' require 'honeybadger/plugin' require 'honeybadger/logging' require 'honeybadger/worker' module Honeybadger # The Honeybadger agent contains all the methods for interacting with the # Honeybadger service. It can be used to send notifications to multiple # projects in large apps. The global agent instance ({Agent.instance}) should # always be accessed through the {Honeybadger} singleton. # # === Context # # Context is global by default, meaning agents created via # +Honeybadger::Agent.new+ will share context (added via # +Honeybadger.context+ or {Honeybadger::Agent#context}) with other agents. # This also includes the Rack environment when using the # {Honeybadger::Rack::ErrorNotifier} middleware. To localize context for a # custom agent, use the +local_context: true+ option when initializing. # # @example # # # Standard usage: # OtherBadger = Honeybadger::Agent.new # # # With local context: # OtherBadger = Honeybadger::Agent.new(local_context: true) # # OtherBadger.configure do |config| # config.api_key = 'project api key' # end # # begin # # Risky operation # rescue => e # OtherBadger.notify(e) # end class Agent extend Forwardable include Logging::Helper # @api private def self.instance @instance end # @api private def self.instance=(instance) @instance = instance end def initialize(opts = {}) if opts.kind_of?(Config) @config = opts opts = {} end @context = opts.delete(:context) @context ||= ContextManager.new if opts.delete(:local_context) @config ||= Config.new(opts) init_worker end # Sends an exception to Honeybadger. Does not report ignored exceptions by # default. # # @example # # With an exception: # begin # fail 'oops' # rescue => exception # Honeybadger.notify(exception, context: { # my_data: 'value' # }) # => '-1dfb92ae-9b01-42e9-9c13-31205b70744a' # end # # # Custom notification: # Honeybadger.notify('Something went wrong.', { # error_class: 'MyClass', # context: {my_data: 'value'} # }) # => '06220c5a-b471-41e5-baeb-de247da45a56' # # @param [Exception, Hash, Object] exception_or_opts An Exception object, # or a Hash of options which is used to build the notice. All other types # of objects will be converted to a String and used as the :error_message. # @param [Hash] opts The options Hash when the first argument is an Exception. # # @option opts [String] :error_message The error message. # @option opts [String] :error_class ('Notice') The class name of the error. # @option opts [Array] :backtrace The backtrace of the error (optional). # @option opts [String] :fingerprint The grouping fingerprint of the exception (optional). # @option opts [Boolean] :force (false) Always report the exception when true, even when ignored (optional). # @option opts [String] :tags The comma-separated list of tags (optional). # @option opts [Hash] :context The context to associate with the exception (optional). # @option opts [String] :controller The controller name (such as a Rails controller) (optional). # @option opts [String] :action The action name (such as a Rails controller action) (optional). # @option opts [Hash] :parameters The HTTP request paramaters (optional). # @option opts [Hash] :session The HTTP request session (optional). # @option opts [String] :url The HTTP request URL (optional). # @option opts [Exception] :cause The cause for this error (optional). # # @return [String] UUID reference to the notice within Honeybadger. # @return [false] when ignored. def notify(exception_or_opts, opts = {}) return false if config.disabled? if exception_or_opts.is_a?(Exception) opts[:exception] = exception_or_opts elsif exception_or_opts.respond_to?(:to_hash) opts.merge!(exception_or_opts.to_hash) else opts[:error_message] = exception_or_opts.to_s end validate_notify_opts!(opts) opts[:rack_env] ||= context_manager.get_rack_env opts[:global_context] ||= context_manager.get_context notice = Notice.new(config, opts) config.before_notify_hooks.each do |hook| break if notice.halted? with_error_handling { hook.call(notice) } end unless notice.api_key =~ NOT_BLANK error { sprintf('Unable to send error report: API key is missing. id=%s', notice.id) } return false end if !opts[:force] && notice.ignore? debug { sprintf('ignore notice feature=notices id=%s', notice.id) } return false end if notice.halted? debug { 'halted notice feature=notices' } return false end info { sprintf('Reporting error id=%s', notice.id) } if opts[:sync] send_now(notice) else push(notice) end notice.id end # Perform a synchronous check_in. # # @example # Honeybadger.check_in('1MqIo1') # # @param [String] id The unique check in id (e.g. '1MqIo1') or the check in url. # # @return [Boolean] true if the check in was successful and false # otherwise. def check_in(id) # this is to allow check ins even if a url is passed check_in_id = id.to_s.strip.gsub(/\/$/, '').split('/').last response = backend.check_in(check_in_id) response.success? end # Save global context for the current request. # # @example # Honeybadger.context({my_data: 'my value'}) # # # Inside a Rails controller: # before_action do # Honeybadger.context({user_id: current_user.id}) # end # # # Explicit conversion # class User < ActiveRecord::Base # def to_honeybadger_context # { user_id: id, user_email: email } # end # end # # user = User.first # Honeybadger.context(user) # # # Clearing global context: # Honeybadger.context.clear! # # @param [Hash] context A Hash of data which will be sent to Honeybadger # when an error occurs. If the object responds to +#to_honeybadger_context+, # the return value of that method will be used (explicit conversion). Can # include any key/value, but a few keys have a special meaning in # Honeybadger. # # @option context [String] :user_id The user ID used by Honeybadger # to aggregate user data across occurrences on the error page (optional). # @option context [String] :user_email The user email address (optional). # @option context [String] :tags The comma-separated list of tags. # When present, tags will be applied to errors with this context # (optional). # # @return [self] so that method calls can be chained. def context(context = nil) context_manager.set_context(context) unless context.nil? self end # @api private # Used to clear context via `#context.clear!`. def clear! # :nodoc: context_manager.clear! end # Get global context for the current request. # # @example # Honeybadger.context({my_data: 'my value'}) # Honeybadger.get_context # => {my_data: 'my value'} # # @return [Hash, nil] def get_context context_manager.get_context end # Flushes all data from workers before returning. This is most useful in # tests when using the test backend, where normally the asynchronous nature # of this library could create race conditions. # # @example # # Without a block: # it "sends a notification to Honeybadger" do # expect { # Honeybadger.notify(StandardError.new('test backend')) # Honeybadger.flush # }.to change(Honeybadger::Backend::Test.notifications[:notices], :size).by(0) # end # # # With a block: # it "sends a notification to Honeybadger" do # expect { # Honeybadger.flush do # 49.times do # Honeybadger.notify(StandardError.new('test backend')) # end # end # }.to change(Honeybadger::Backend::Test.notifications[:notices], :size).by(49) # end # # @yield An optional block to execute (exceptions will propagate after # data is flushed). # # @return [Object, Boolean] value of block if block is given, otherwise true # on success or false if Honeybadger isn't running. def flush return true unless block_given? yield ensure worker.flush end # Stops the Honeybadger service. # # @example # Honeybadger.stop # => nil def stop(force = false) worker.send(force ? :shutdown! : :shutdown) true end # @api private attr_reader :config # Configure the Honeybadger agent via Ruby. # # @example # Honeybadger.configure do |config| # config.api_key = 'project api key' # config.exceptions.ignore += [CustomError] # end # # @!method configure # @yield [Config::Ruby] configuration object. def_delegator :config, :configure # Callback to ignore exceptions. # # See public API documentation for {Honeybadger::Notice} for available attributes. # # @example # # Ignoring based on error message: # Honeybadger.exception_filter do |notice| # notice[:error_message] =~ /sensitive data/ # end # # # Ignore an entire class of exceptions: # Honeybadger.exception_filter do |notice| # notice[:exception].class < MyError # end # # @!method exception_filter # @yieldreturn [Boolean] true (to ignore) or false (to send). def_delegator :config, :exception_filter # Callback to add a custom grouping strategy for exceptions. The return # value is hashed and sent to Honeybadger. Errors with the same fingerprint # will be grouped. # # See public API documentation for {Honeybadger::Notice} for available attributes. # # @example # Honeybadger.exception_fingerprint do |notice| # [notice[:error_class], notice[:component], notice[:backtrace].to_s].join(':') # end # # @!method exception_fingerprint # @yieldreturn [#to_s] The fingerprint of the error. def_delegator :config, :exception_fingerprint # Callback to filter backtrace lines. One use for this is to make # additional [PROJECT_ROOT] or [GEM_ROOT] substitutions, which are used by # Honeybadger when grouping errors and displaying application traces. # # @example # Honeybadger.backtrace_filter do |line| # line.gsub(/^\/my\/unknown\/bundle\/path/, "[GEM_ROOT]") # end # # @!method backtrace_filter # @yieldparam [String] line The backtrace line to modify. # @yieldreturn [String] The new (modified) backtrace line. def_delegator :config, :backtrace_filter # @api private def with_rack_env(rack_env, &block) context_manager.set_rack_env(rack_env) yield ensure context_manager.set_rack_env(nil) end # @api private attr_reader :worker # @api private # @!method init!(...) # @see Config#init! def_delegators :config, :init! # @api private # @!method backend # @see Config#backend def_delegators :config, :backend private def validate_notify_opts!(opts) return if opts.has_key?(:exception) return if opts.has_key?(:error_message) msg = sprintf('`Honeybadger.notify` was called with invalid arguments. You must pass either an Exception or options Hash containing the `:error_message` key. location=%s', caller[caller.size-1]) raise ArgumentError.new(msg) if config.dev? warn(msg) end def context_manager return @context if @context ContextManager.current end def push(object) worker.push(object) true end def send_now(object) worker.send_now(object) true end def init_worker @worker = Worker.new(config) end def with_error_handling yield rescue => ex error { "Rescued an error in a before notify hook: #{ex.message}" } end @instance = new(Config.new) end end