airbrake-ruby 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/airbrake-ruby.rb +515 -0
- data/lib/airbrake-ruby/async_sender.rb +80 -0
- data/lib/airbrake-ruby/backtrace.rb +196 -0
- data/lib/airbrake-ruby/benchmark.rb +39 -0
- data/lib/airbrake-ruby/code_hunk.rb +51 -0
- data/lib/airbrake-ruby/config.rb +229 -0
- data/lib/airbrake-ruby/config/validator.rb +91 -0
- data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
- data/lib/airbrake-ruby/file_cache.rb +54 -0
- data/lib/airbrake-ruby/filter_chain.rb +95 -0
- data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
- data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
- data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +125 -0
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
- data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
- data/lib/airbrake-ruby/hash_keyable.rb +37 -0
- data/lib/airbrake-ruby/ignorable.rb +44 -0
- data/lib/airbrake-ruby/inspectable.rb +39 -0
- data/lib/airbrake-ruby/loggable.rb +34 -0
- data/lib/airbrake-ruby/monotonic_time.rb +43 -0
- data/lib/airbrake-ruby/nested_exception.rb +38 -0
- data/lib/airbrake-ruby/notice.rb +162 -0
- data/lib/airbrake-ruby/notice_notifier.rb +134 -0
- data/lib/airbrake-ruby/performance_breakdown.rb +46 -0
- data/lib/airbrake-ruby/performance_notifier.rb +155 -0
- data/lib/airbrake-ruby/promise.rb +109 -0
- data/lib/airbrake-ruby/query.rb +54 -0
- data/lib/airbrake-ruby/request.rb +46 -0
- data/lib/airbrake-ruby/response.rb +74 -0
- data/lib/airbrake-ruby/stashable.rb +15 -0
- data/lib/airbrake-ruby/stat.rb +73 -0
- data/lib/airbrake-ruby/sync_sender.rb +113 -0
- data/lib/airbrake-ruby/tdigest.rb +393 -0
- data/lib/airbrake-ruby/thread_pool.rb +128 -0
- data/lib/airbrake-ruby/time_truncate.rb +17 -0
- data/lib/airbrake-ruby/timed_trace.rb +58 -0
- data/lib/airbrake-ruby/truncator.rb +115 -0
- data/lib/airbrake-ruby/version.rb +6 -0
- data/spec/airbrake_spec.rb +324 -0
- data/spec/async_sender_spec.rb +72 -0
- data/spec/backtrace_spec.rb +427 -0
- data/spec/benchmark_spec.rb +33 -0
- data/spec/code_hunk_spec.rb +115 -0
- data/spec/config/validator_spec.rb +184 -0
- data/spec/config_spec.rb +154 -0
- data/spec/deploy_notifier_spec.rb +48 -0
- data/spec/file_cache_spec.rb +34 -0
- data/spec/filter_chain_spec.rb +92 -0
- data/spec/filters/context_filter_spec.rb +23 -0
- data/spec/filters/dependency_filter_spec.rb +12 -0
- data/spec/filters/exception_attributes_filter_spec.rb +50 -0
- data/spec/filters/gem_root_filter_spec.rb +41 -0
- data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
- data/spec/filters/git_repository_filter.rb +61 -0
- data/spec/filters/git_revision_filter_spec.rb +126 -0
- data/spec/filters/keys_blacklist_spec.rb +225 -0
- data/spec/filters/keys_whitelist_spec.rb +194 -0
- data/spec/filters/root_directory_filter_spec.rb +39 -0
- data/spec/filters/sql_filter_spec.rb +262 -0
- data/spec/filters/system_exit_filter_spec.rb +14 -0
- data/spec/filters/thread_filter_spec.rb +277 -0
- data/spec/fixtures/notroot.txt +7 -0
- data/spec/fixtures/project_root/code.rb +221 -0
- data/spec/fixtures/project_root/empty_file.rb +0 -0
- data/spec/fixtures/project_root/long_line.txt +1 -0
- data/spec/fixtures/project_root/short_file.rb +3 -0
- data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
- data/spec/helpers.rb +9 -0
- data/spec/ignorable_spec.rb +14 -0
- data/spec/inspectable_spec.rb +45 -0
- data/spec/monotonic_time_spec.rb +12 -0
- data/spec/nested_exception_spec.rb +73 -0
- data/spec/notice_notifier/options_spec.rb +259 -0
- data/spec/notice_notifier_spec.rb +356 -0
- data/spec/notice_spec.rb +296 -0
- data/spec/performance_breakdown_spec.rb +12 -0
- data/spec/performance_notifier_spec.rb +491 -0
- data/spec/promise_spec.rb +197 -0
- data/spec/query_spec.rb +11 -0
- data/spec/request_spec.rb +11 -0
- data/spec/response_spec.rb +88 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/stashable_spec.rb +23 -0
- data/spec/stat_spec.rb +47 -0
- data/spec/sync_sender_spec.rb +133 -0
- data/spec/tdigest_spec.rb +230 -0
- data/spec/thread_pool_spec.rb +158 -0
- data/spec/time_truncate_spec.rb +13 -0
- data/spec/timed_trace_spec.rb +125 -0
- data/spec/truncator_spec.rb +238 -0
- metadata +216 -0
| @@ -0,0 +1,134 @@ | |
| 1 | 
            +
            module Airbrake
         | 
| 2 | 
            +
              # NoticeNotifier is reponsible for sending notices to Airbrake. It supports
         | 
| 3 | 
            +
              # synchronous and asynchronous delivery.
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # @see Airbrake::Config The list of options
         | 
| 6 | 
            +
              # @since v1.0.0
         | 
| 7 | 
            +
              # @api public
         | 
| 8 | 
            +
              class NoticeNotifier
         | 
| 9 | 
            +
                # @return [Array<Class>] filters to be executed first
         | 
| 10 | 
            +
                DEFAULT_FILTERS = [
         | 
| 11 | 
            +
                  Airbrake::Filters::SystemExitFilter,
         | 
| 12 | 
            +
                  Airbrake::Filters::GemRootFilter
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  # Optional filters (must be included by users):
         | 
| 15 | 
            +
                  # Airbrake::Filters::ThreadFilter
         | 
| 16 | 
            +
                ].freeze
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                include Inspectable
         | 
| 19 | 
            +
                include Loggable
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def initialize
         | 
| 22 | 
            +
                  @config = Airbrake::Config.instance
         | 
| 23 | 
            +
                  @context = {}
         | 
| 24 | 
            +
                  @filter_chain = FilterChain.new
         | 
| 25 | 
            +
                  @async_sender = AsyncSender.new
         | 
| 26 | 
            +
                  @sync_sender = SyncSender.new
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  DEFAULT_FILTERS.each { |filter| add_filter(filter.new) }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  add_filter(Airbrake::Filters::ContextFilter.new(@context))
         | 
| 31 | 
            +
                  add_filter(Airbrake::Filters::ExceptionAttributesFilter.new)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                # @see Airbrake.notify
         | 
| 35 | 
            +
                def notify(exception, params = {}, &block)
         | 
| 36 | 
            +
                  send_notice(exception, params, default_sender, &block)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                # @see Airbrake.notify_sync
         | 
| 40 | 
            +
                def notify_sync(exception, params = {}, &block)
         | 
| 41 | 
            +
                  send_notice(exception, params, @sync_sender, &block).value
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                # @see Airbrake.add_filte
         | 
| 45 | 
            +
                def add_filter(filter = nil, &block)
         | 
| 46 | 
            +
                  @filter_chain.add_filter(block_given? ? block : filter)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                # @see Airbrake.delete_filter
         | 
| 50 | 
            +
                def delete_filter(filter_class)
         | 
| 51 | 
            +
                  @filter_chain.delete_filter(filter_class)
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                # @see Airbrake.build_notice
         | 
| 55 | 
            +
                def build_notice(exception, params = {})
         | 
| 56 | 
            +
                  if @async_sender.closed?
         | 
| 57 | 
            +
                    raise Airbrake::Error,
         | 
| 58 | 
            +
                          "attempted to build #{exception} with closed Airbrake instance"
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  if exception.is_a?(Airbrake::Notice)
         | 
| 62 | 
            +
                    exception[:params].merge!(params)
         | 
| 63 | 
            +
                    exception
         | 
| 64 | 
            +
                  else
         | 
| 65 | 
            +
                    Notice.new(convert_to_exception(exception), params.dup)
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                # @see Airbrake.close
         | 
| 70 | 
            +
                def close
         | 
| 71 | 
            +
                  @async_sender.close
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                # @see Airbrake.configured?
         | 
| 75 | 
            +
                def configured?
         | 
| 76 | 
            +
                  @config.valid?
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                # @see Airbrake.merge_context
         | 
| 80 | 
            +
                def merge_context(context)
         | 
| 81 | 
            +
                  @context.merge!(context)
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                private
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def convert_to_exception(ex)
         | 
| 87 | 
            +
                  if ex.is_a?(Exception) || Backtrace.java_exception?(ex)
         | 
| 88 | 
            +
                    # Manually created exceptions don't have backtraces, so we create a fake
         | 
| 89 | 
            +
                    # one, whose first frame points to the place where Airbrake was called
         | 
| 90 | 
            +
                    # (normally via `notify`).
         | 
| 91 | 
            +
                    ex.set_backtrace(clean_backtrace) unless ex.backtrace
         | 
| 92 | 
            +
                    return ex
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  e = RuntimeError.new(ex.to_s)
         | 
| 96 | 
            +
                  e.set_backtrace(clean_backtrace)
         | 
| 97 | 
            +
                  e
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def send_notice(exception, params, sender)
         | 
| 101 | 
            +
                  promise = @config.check_configuration
         | 
| 102 | 
            +
                  return promise if promise.rejected?
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  notice = build_notice(exception, params)
         | 
| 105 | 
            +
                  yield notice if block_given?
         | 
| 106 | 
            +
                  @filter_chain.refine(notice)
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  promise = Airbrake::Promise.new
         | 
| 109 | 
            +
                  return promise.reject("#{notice} was marked as ignored") if notice.ignored?
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  sender.send(notice, promise)
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def default_sender
         | 
| 115 | 
            +
                  return @async_sender if @async_sender.has_workers?
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  logger.warn(
         | 
| 118 | 
            +
                    "#{LOG_LABEL} falling back to sync delivery because there are no " \
         | 
| 119 | 
            +
                    "running async workers"
         | 
| 120 | 
            +
                  )
         | 
| 121 | 
            +
                  @sync_sender
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def clean_backtrace
         | 
| 125 | 
            +
                  caller_copy = Kernel.caller
         | 
| 126 | 
            +
                  clean_bt = caller_copy.drop_while { |frame| frame.include?('/lib/airbrake') }
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  # If true, then it's likely an internal library error. In this case return
         | 
| 129 | 
            +
                  # at least some backtrace to simplify debugging.
         | 
| 130 | 
            +
                  return caller_copy if clean_bt.empty?
         | 
| 131 | 
            +
                  clean_bt
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
              end
         | 
| 134 | 
            +
            end
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            module Airbrake
         | 
| 2 | 
            +
              # PerformanceBreakdown holds data that shows how much time a request spent
         | 
| 3 | 
            +
              # doing certaing subtasks such as (DB querying, view rendering, etc).
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # @see Airbrake.notify_breakdown
         | 
| 6 | 
            +
              # @api public
         | 
| 7 | 
            +
              # @since v4.2.0
         | 
| 8 | 
            +
              # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
         | 
| 9 | 
            +
              PerformanceBreakdown = Struct.new(
         | 
| 10 | 
            +
                :method, :route, :response_type, :groups, :start_time, :end_time
         | 
| 11 | 
            +
              ) do
         | 
| 12 | 
            +
                include HashKeyable
         | 
| 13 | 
            +
                include Ignorable
         | 
| 14 | 
            +
                include Stashable
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(
         | 
| 17 | 
            +
                  method:,
         | 
| 18 | 
            +
                  route:,
         | 
| 19 | 
            +
                  response_type:,
         | 
| 20 | 
            +
                  groups:,
         | 
| 21 | 
            +
                  start_time:,
         | 
| 22 | 
            +
                  end_time: Time.now
         | 
| 23 | 
            +
                )
         | 
| 24 | 
            +
                  @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
         | 
| 25 | 
            +
                  super(method, route, response_type, groups, start_time, end_time)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def destination
         | 
| 29 | 
            +
                  'routes-breakdowns'
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def cargo
         | 
| 33 | 
            +
                  'routes'
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def to_h
         | 
| 37 | 
            +
                  {
         | 
| 38 | 
            +
                    'method' => method,
         | 
| 39 | 
            +
                    'route' => route,
         | 
| 40 | 
            +
                    'responseType' => response_type,
         | 
| 41 | 
            +
                    'time' => @start_time_utc
         | 
| 42 | 
            +
                  }.delete_if { |_key, val| val.nil? }
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
              # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
         | 
| 46 | 
            +
            end
         | 
| @@ -0,0 +1,155 @@ | |
| 1 | 
            +
            module Airbrake
         | 
| 2 | 
            +
              # QueryNotifier aggregates information about SQL queries and periodically sends
         | 
| 3 | 
            +
              # collected data to Airbrake.
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # @api public
         | 
| 6 | 
            +
              # @since v3.2.0
         | 
| 7 | 
            +
              class PerformanceNotifier
         | 
| 8 | 
            +
                include Inspectable
         | 
| 9 | 
            +
                include Loggable
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize
         | 
| 12 | 
            +
                  @config = Airbrake::Config.instance
         | 
| 13 | 
            +
                  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
         | 
| 14 | 
            +
                  @sender = AsyncSender.new(:put)
         | 
| 15 | 
            +
                  @payload = {}
         | 
| 16 | 
            +
                  @schedule_flush = nil
         | 
| 17 | 
            +
                  @mutex = Mutex.new
         | 
| 18 | 
            +
                  @filter_chain = FilterChain.new
         | 
| 19 | 
            +
                  @waiting = false
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # @param [Hash] resource
         | 
| 23 | 
            +
                # @see Airbrake.notify_query
         | 
| 24 | 
            +
                # @see Airbrake.notify_request
         | 
| 25 | 
            +
                def notify(resource)
         | 
| 26 | 
            +
                  promise = @config.check_configuration
         | 
| 27 | 
            +
                  return promise if promise.rejected?
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  promise = @config.check_performance_options(resource)
         | 
| 30 | 
            +
                  return promise if promise.rejected?
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  @filter_chain.refine(resource)
         | 
| 33 | 
            +
                  return if resource.ignored?
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  @mutex.synchronize do
         | 
| 36 | 
            +
                    update_payload(resource)
         | 
| 37 | 
            +
                    @flush_period > 0 ? schedule_flush : send(@payload, promise)
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  promise.resolve(:success)
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                # @see Airbrake.add_performance_filter
         | 
| 44 | 
            +
                def add_filter(filter = nil, &block)
         | 
| 45 | 
            +
                  @filter_chain.add_filter(block_given? ? block : filter)
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # @see Airbrake.delete_performance_filter
         | 
| 49 | 
            +
                def delete_filter(filter_class)
         | 
| 50 | 
            +
                  @filter_chain.delete_filter(filter_class)
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def close
         | 
| 54 | 
            +
                  @mutex.synchronize do
         | 
| 55 | 
            +
                    @schedule_flush.kill if @schedule_flush
         | 
| 56 | 
            +
                    @sender.close
         | 
| 57 | 
            +
                    logger.debug("#{LOG_LABEL} performance notifier closed")
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                private
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def update_payload(resource)
         | 
| 64 | 
            +
                  @payload[resource] ||= { total: Airbrake::Stat.new }
         | 
| 65 | 
            +
                  @payload[resource][:total].increment(resource.start_time, resource.end_time)
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  resource.groups.each do |name, ms|
         | 
| 68 | 
            +
                    @payload[resource][name] ||= Airbrake::Stat.new
         | 
| 69 | 
            +
                    @payload[resource][name].increment_ms(ms)
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def schedule_flush
         | 
| 74 | 
            +
                  return if @payload.empty?
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  if @schedule_flush && @schedule_flush.status == 'sleep' && @waiting
         | 
| 77 | 
            +
                    begin
         | 
| 78 | 
            +
                      @schedule_flush.run
         | 
| 79 | 
            +
                    rescue ThreadError => exception
         | 
| 80 | 
            +
                      logger.error("#{LOG_LABEL}: error occurred while flushing: #{exception}")
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  @schedule_flush ||= spawn_timer
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def spawn_timer
         | 
| 88 | 
            +
                  Thread.new do
         | 
| 89 | 
            +
                    loop do
         | 
| 90 | 
            +
                      if @payload.none?
         | 
| 91 | 
            +
                        @waiting = true
         | 
| 92 | 
            +
                        Thread.stop
         | 
| 93 | 
            +
                        @waiting = false
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                      sleep(@flush_period)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                      payload = nil
         | 
| 99 | 
            +
                      @mutex.synchronize do
         | 
| 100 | 
            +
                        payload = @payload
         | 
| 101 | 
            +
                        @payload = {}
         | 
| 102 | 
            +
                      end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                      send(payload, Airbrake::Promise.new)
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def send(payload, promise)
         | 
| 110 | 
            +
                  signature = "#{self.class.name}##{__method__}"
         | 
| 111 | 
            +
                  raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  logger.debug { "#{LOG_LABEL} #{signature}: #{payload}" }
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  with_grouped_payload(payload) do |resource_hash, destination|
         | 
| 116 | 
            +
                    url = URI.join(
         | 
| 117 | 
            +
                      @config.host,
         | 
| 118 | 
            +
                      "api/v5/projects/#{@config.project_id}/#{destination}"
         | 
| 119 | 
            +
                    )
         | 
| 120 | 
            +
                    @sender.send(resource_hash, promise, url)
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  promise
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def with_grouped_payload(raw_payload)
         | 
| 127 | 
            +
                  grouped_payload = raw_payload.group_by do |resource, _stats|
         | 
| 128 | 
            +
                    [resource.cargo, resource.destination]
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  grouped_payload.each do |(cargo, destination), resources|
         | 
| 132 | 
            +
                    payload = {}
         | 
| 133 | 
            +
                    payload[cargo] = serialize_resources(resources)
         | 
| 134 | 
            +
                    payload['environment'] = @config.environment if @config.environment
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                    yield(payload, destination)
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                def serialize_resources(resources)
         | 
| 141 | 
            +
                  resources.map do |resource, stats|
         | 
| 142 | 
            +
                    resource_hash = resource.to_h.merge!(stats[:total].to_h)
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                    if resource.groups.any?
         | 
| 145 | 
            +
                      group_stats = stats.reject { |name, _stat| name == :total }
         | 
| 146 | 
            +
                      resource_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
         | 
| 147 | 
            +
                        stat.to_h
         | 
| 148 | 
            +
                      end
         | 
| 149 | 
            +
                    end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                    resource_hash
         | 
| 152 | 
            +
                  end
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
              end
         | 
| 155 | 
            +
            end
         | 
| @@ -0,0 +1,109 @@ | |
| 1 | 
            +
            module Airbrake
         | 
| 2 | 
            +
              # Represents a simplified promise object (similar to promises found in
         | 
| 3 | 
            +
              # JavaScript), which allows chaining callbacks that are executed when the
         | 
| 4 | 
            +
              # promise is either resolved or rejected.
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
         | 
| 7 | 
            +
              # @see https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/promise.rb
         | 
| 8 | 
            +
              # @since v1.7.0
         | 
| 9 | 
            +
              class Promise
         | 
| 10 | 
            +
                def initialize
         | 
| 11 | 
            +
                  @on_resolved = []
         | 
| 12 | 
            +
                  @on_rejected = []
         | 
| 13 | 
            +
                  @value = {}
         | 
| 14 | 
            +
                  @mutex = Mutex.new
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                # Attaches a callback to be executed when the promise is resolved.
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @example
         | 
| 20 | 
            +
                #   Airbrake::Promise.new.then { |response| puts response }
         | 
| 21 | 
            +
                #   #=> {"id"=>"00054415-8201-e9c6-65d6-fc4d231d2871",
         | 
| 22 | 
            +
                #   #    "url"=>"http://localhost/locate/00054415-8201-e9c6-65d6-fc4d231d2871"}
         | 
| 23 | 
            +
                #
         | 
| 24 | 
            +
                # @yield [response]
         | 
| 25 | 
            +
                # @yieldparam response [Hash<String,String>] Contains the `id` & `url` keys
         | 
| 26 | 
            +
                # @return [self]
         | 
| 27 | 
            +
                def then(&block)
         | 
| 28 | 
            +
                  @mutex.synchronize do
         | 
| 29 | 
            +
                    if @value.key?('ok')
         | 
| 30 | 
            +
                      yield(@value['ok'])
         | 
| 31 | 
            +
                      return self
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    @on_resolved << block
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  self
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # Attaches a callback to be executed when the promise is rejected.
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                # @example
         | 
| 43 | 
            +
                #   Airbrake::Promise.new.rescue { |error| raise error }
         | 
| 44 | 
            +
                #
         | 
| 45 | 
            +
                # @yield [error] The error message from the API
         | 
| 46 | 
            +
                # @yieldparam error [String]
         | 
| 47 | 
            +
                # @return [self]
         | 
| 48 | 
            +
                def rescue(&block)
         | 
| 49 | 
            +
                  @mutex.synchronize do
         | 
| 50 | 
            +
                    if @value.key?('error')
         | 
| 51 | 
            +
                      yield(@value['error'])
         | 
| 52 | 
            +
                      return self
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    @on_rejected << block
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  self
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                # @example
         | 
| 62 | 
            +
                #   Airbrake::Promise.new.resolve('id' => '123')
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                # @param reason [Object]
         | 
| 65 | 
            +
                # @return [self]
         | 
| 66 | 
            +
                def resolve(reason = 'resolved')
         | 
| 67 | 
            +
                  @mutex.synchronize do
         | 
| 68 | 
            +
                    @value['ok'] = reason
         | 
| 69 | 
            +
                    @on_resolved.each { |callback| callback.call(reason) }
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  self
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                # @example
         | 
| 76 | 
            +
                #   Airbrake::Promise.new.reject('Something went wrong')
         | 
| 77 | 
            +
                #
         | 
| 78 | 
            +
                # @param reason [String]
         | 
| 79 | 
            +
                # @return [self]
         | 
| 80 | 
            +
                def reject(reason = 'rejected')
         | 
| 81 | 
            +
                  @mutex.synchronize do
         | 
| 82 | 
            +
                    @value['error'] = reason
         | 
| 83 | 
            +
                    @on_rejected.each { |callback| callback.call(reason) }
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  self
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                # @return [Boolean]
         | 
| 90 | 
            +
                def rejected?
         | 
| 91 | 
            +
                  @value.key?('error')
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                # @return [Boolean]
         | 
| 95 | 
            +
                def resolved?
         | 
| 96 | 
            +
                  @value.key?('ok')
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                # @return [Hash<String,String>] either successful response containing the
         | 
| 100 | 
            +
                #   +id+ key or unsuccessful response containing the +error+ key
         | 
| 101 | 
            +
                # @note This is a non-blocking call!
         | 
| 102 | 
            +
                # @todo Get rid of this method and use an accessor. The resolved guard is
         | 
| 103 | 
            +
                #   needed for compatibility but it shouldn't exist in the future
         | 
| 104 | 
            +
                def value
         | 
| 105 | 
            +
                  return @value['ok'] if resolved?
         | 
| 106 | 
            +
                  @value
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
            end
         | 
| @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            module Airbrake
         | 
| 2 | 
            +
              # Query holds SQL query data that powers SQL query collection.
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # @see Airbrake.notify_query
         | 
| 5 | 
            +
              # @api public
         | 
| 6 | 
            +
              # @since v3.2.0
         | 
| 7 | 
            +
              # rubocop:disable Metrics/ParameterLists, Metrics/BlockLength
         | 
| 8 | 
            +
              Query = Struct.new(
         | 
| 9 | 
            +
                :method, :route, :query, :func, :file, :line, :start_time, :end_time
         | 
| 10 | 
            +
              ) do
         | 
| 11 | 
            +
                include HashKeyable
         | 
| 12 | 
            +
                include Ignorable
         | 
| 13 | 
            +
                include Stashable
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def initialize(
         | 
| 16 | 
            +
                  method:,
         | 
| 17 | 
            +
                  route:,
         | 
| 18 | 
            +
                  query:,
         | 
| 19 | 
            +
                  func: nil,
         | 
| 20 | 
            +
                  file: nil,
         | 
| 21 | 
            +
                  line: nil,
         | 
| 22 | 
            +
                  start_time:,
         | 
| 23 | 
            +
                  end_time: Time.now
         | 
| 24 | 
            +
                )
         | 
| 25 | 
            +
                  @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
         | 
| 26 | 
            +
                  super(method, route, query, func, file, line, start_time, end_time)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def destination
         | 
| 30 | 
            +
                  'queries-stats'
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def cargo
         | 
| 34 | 
            +
                  'queries'
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def groups
         | 
| 38 | 
            +
                  {}
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def to_h
         | 
| 42 | 
            +
                  {
         | 
| 43 | 
            +
                    'method' => method,
         | 
| 44 | 
            +
                    'route' => route,
         | 
| 45 | 
            +
                    'query' => query,
         | 
| 46 | 
            +
                    'time' => @start_time_utc,
         | 
| 47 | 
            +
                    'function' => func,
         | 
| 48 | 
            +
                    'file' => file,
         | 
| 49 | 
            +
                    'line' => line
         | 
| 50 | 
            +
                  }.delete_if { |_key, val| val.nil? }
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
                # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
            end
         |