airbrake-ruby 4.6.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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +513 -0
  3. data/lib/airbrake-ruby/async_sender.rb +142 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +48 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +45 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +125 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +53 -0
  38. data/lib/airbrake-ruby/request.rb +45 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  45. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  46. data/lib/airbrake-ruby/truncator.rb +115 -0
  47. data/lib/airbrake-ruby/version.rb +6 -0
  48. data/spec/airbrake_spec.rb +324 -0
  49. data/spec/async_sender_spec.rb +155 -0
  50. data/spec/backtrace_spec.rb +427 -0
  51. data/spec/benchmark_spec.rb +33 -0
  52. data/spec/code_hunk_spec.rb +115 -0
  53. data/spec/config/validator_spec.rb +184 -0
  54. data/spec/config_spec.rb +154 -0
  55. data/spec/deploy_notifier_spec.rb +48 -0
  56. data/spec/file_cache.rb +36 -0
  57. data/spec/filter_chain_spec.rb +92 -0
  58. data/spec/filters/context_filter_spec.rb +23 -0
  59. data/spec/filters/dependency_filter_spec.rb +12 -0
  60. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  61. data/spec/filters/gem_root_filter_spec.rb +41 -0
  62. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  63. data/spec/filters/git_repository_filter.rb +61 -0
  64. data/spec/filters/git_revision_filter_spec.rb +126 -0
  65. data/spec/filters/keys_blacklist_spec.rb +225 -0
  66. data/spec/filters/keys_whitelist_spec.rb +194 -0
  67. data/spec/filters/root_directory_filter_spec.rb +39 -0
  68. data/spec/filters/sql_filter_spec.rb +219 -0
  69. data/spec/filters/system_exit_filter_spec.rb +14 -0
  70. data/spec/filters/thread_filter_spec.rb +277 -0
  71. data/spec/fixtures/notroot.txt +7 -0
  72. data/spec/fixtures/project_root/code.rb +221 -0
  73. data/spec/fixtures/project_root/empty_file.rb +0 -0
  74. data/spec/fixtures/project_root/long_line.txt +1 -0
  75. data/spec/fixtures/project_root/short_file.rb +3 -0
  76. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  77. data/spec/helpers.rb +9 -0
  78. data/spec/ignorable_spec.rb +14 -0
  79. data/spec/inspectable_spec.rb +45 -0
  80. data/spec/monotonic_time_spec.rb +12 -0
  81. data/spec/nested_exception_spec.rb +73 -0
  82. data/spec/notice_notifier_spec.rb +356 -0
  83. data/spec/notice_notifier_spec/options_spec.rb +259 -0
  84. data/spec/notice_spec.rb +296 -0
  85. data/spec/performance_breakdown_spec.rb +12 -0
  86. data/spec/performance_notifier_spec.rb +435 -0
  87. data/spec/promise_spec.rb +197 -0
  88. data/spec/query_spec.rb +11 -0
  89. data/spec/request_spec.rb +11 -0
  90. data/spec/response_spec.rb +88 -0
  91. data/spec/spec_helper.rb +100 -0
  92. data/spec/stashable_spec.rb +23 -0
  93. data/spec/stat_spec.rb +47 -0
  94. data/spec/sync_sender_spec.rb +133 -0
  95. data/spec/tdigest_spec.rb +230 -0
  96. data/spec/time_truncate_spec.rb +13 -0
  97. data/spec/timed_trace_spec.rb +125 -0
  98. data/spec/truncator_spec.rb +238 -0
  99. metadata +213 -0
@@ -0,0 +1,91 @@
1
+ module Airbrake
2
+ class Config
3
+ # Validator validates values of {Airbrake::Config} options. A valid config
4
+ # is a config that guarantees that data can be sent to Airbrake given its
5
+ # configuration.
6
+ #
7
+ # @api private
8
+ # @since v1.5.0
9
+ class Validator
10
+ # @return [Array<Class>] the list of allowed types to configure the
11
+ # environment option
12
+ VALID_ENV_TYPES = [NilClass, String, Symbol].freeze
13
+
14
+ class << self
15
+ # @param [Airbrake::Config] config
16
+ # @since v4.1.0
17
+ def validate(config)
18
+ promise = Airbrake::Promise.new
19
+
20
+ unless valid_project_id?(config)
21
+ return promise.reject(':project_id is required')
22
+ end
23
+
24
+ unless valid_project_key?(config)
25
+ return promise.reject(':project_key is required')
26
+ end
27
+
28
+ unless valid_environment?(config)
29
+ return promise.reject(
30
+ "the 'environment' option must be configured " \
31
+ "with a Symbol (or String), but '#{config.environment.class}' was " \
32
+ "provided: #{config.environment}"
33
+ )
34
+ end
35
+
36
+ promise.resolve(:ok)
37
+ end
38
+
39
+ # Whether the given +config+ allows sending data to Airbrake. It doesn't
40
+ # matter if it's valid or invalid.
41
+ #
42
+ # @param [Airbrake::Config] config
43
+ # @since v4.1.0
44
+ def check_notify_ability(config)
45
+ promise = Airbrake::Promise.new
46
+
47
+ if ignored_environment?(config)
48
+ return promise.reject(
49
+ "current environment '#{config.environment}' is ignored"
50
+ )
51
+ end
52
+
53
+ promise.resolve(:ok)
54
+ end
55
+
56
+ private
57
+
58
+ def valid_project_id?(config)
59
+ return true if config.project_id.to_i > 0
60
+ false
61
+ end
62
+
63
+ def valid_project_key?(config)
64
+ return false unless config.project_key.is_a?(String)
65
+ return false if config.project_key.empty?
66
+ true
67
+ end
68
+
69
+ def valid_environment?(config)
70
+ VALID_ENV_TYPES.any? { |type| config.environment.is_a?(type) }
71
+ end
72
+
73
+ def ignored_environment?(config)
74
+ if config.ignore_environments.any? && config.environment.nil?
75
+ config.logger.warn(
76
+ "#{LOG_LABEL} the 'environment' option is not set, " \
77
+ "'ignore_environments' has no effect"
78
+ )
79
+ end
80
+
81
+ return false if config.ignore_environments.none? || !config.environment
82
+
83
+ env = config.environment.to_s
84
+ config.ignore_environments.any? do |pattern|
85
+ pattern.is_a?(Regexp) ? env.match(pattern) : env == pattern.to_s
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,36 @@
1
+ module Airbrake
2
+ # DeployNotifier sends deploy information to Airbrake. The information
3
+ # consists of:
4
+ # - environment
5
+ # - username
6
+ # - repository
7
+ # - revision
8
+ # - version
9
+ #
10
+ # @api public
11
+ # @since v3.2.0
12
+ class DeployNotifier
13
+ include Inspectable
14
+
15
+ def initialize
16
+ @config = Airbrake::Config.instance
17
+ @sender = SyncSender.new
18
+ end
19
+
20
+ # @see Airbrake.notify_deploy
21
+ def notify(deploy_info)
22
+ promise = @config.check_configuration
23
+ return promise if promise.rejected?
24
+
25
+ promise = Airbrake::Promise.new
26
+ deploy_info[:environment] ||= @config.environment
27
+ @sender.send(
28
+ deploy_info,
29
+ promise,
30
+ URI.join(@config.host, "api/v4/projects/#{@config.project_id}/deploys")
31
+ )
32
+
33
+ promise
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ module Airbrake
2
+ # Extremely simple global cache.
3
+ #
4
+ # @api private
5
+ # @since v2.4.1
6
+ module FileCache
7
+ # @return [Integer]
8
+ MAX_SIZE = 50
9
+
10
+ # @return [Mutex]
11
+ MUTEX = Mutex.new
12
+
13
+ # Associates the value given by +value+ with the key given by +key+. Deletes
14
+ # entries that exceed +MAX_SIZE+.
15
+ #
16
+ # @param [Object] key
17
+ # @param [Object] value
18
+ # @return [Object] the corresponding value
19
+ def self.[]=(key, value)
20
+ MUTEX.synchronize do
21
+ data[key] = value
22
+ data.delete(data.keys.first) if data.size > MAX_SIZE
23
+ end
24
+ end
25
+
26
+ # Retrieve an object from the cache.
27
+ #
28
+ # @param [Object] key
29
+ # @return [Object] the corresponding value
30
+ def self.[](key)
31
+ MUTEX.synchronize do
32
+ data[key]
33
+ end
34
+ end
35
+
36
+ # Checks whether the cache is empty. Needed only for the test suite.
37
+ #
38
+ # @return [Boolean]
39
+ def self.empty?
40
+ data.empty?
41
+ end
42
+
43
+ def self.data
44
+ @data ||= {}
45
+ end
46
+ private_class_method :data
47
+ end
48
+ end
@@ -0,0 +1,95 @@
1
+ module Airbrake
2
+ # FilterChain represents an ordered array of filters.
3
+ #
4
+ # A filter is an object that responds to <b>#call</b> (typically a Proc or a
5
+ # class that implements the call method). The <b>#call</b> method must accept
6
+ # exactly one argument: an object to be filtered.
7
+ #
8
+ # When you add a new filter to the chain, it gets inserted according to its
9
+ # <b>weight</b>. Smaller weight means the filter will be somewhere in the
10
+ # beginning of the array. Larger - in the end. If a filter doesn't implement
11
+ # weight, the chain assumes it's equal to 0.
12
+ #
13
+ # @example
14
+ # class MyFilter
15
+ # attr_reader :weight
16
+ #
17
+ # def initialize
18
+ # @weight = 1
19
+ # end
20
+ #
21
+ # def call(obj)
22
+ # puts 'Filtering...'
23
+ # obj[:data] = '[Filtered]'
24
+ # end
25
+ # end
26
+ #
27
+ # filter_chain = FilterChain.new
28
+ # filter_chain.add_filter(MyFilter)
29
+ #
30
+ # filter_chain.refine(obj)
31
+ # #=> Filtering...
32
+ #
33
+ # @see Airbrake.add_filter
34
+ # @api private
35
+ # @since v1.0.0
36
+ class FilterChain
37
+ # @return [Integer]
38
+ DEFAULT_WEIGHT = 0
39
+
40
+ def initialize
41
+ @filters = []
42
+ end
43
+
44
+ # Adds a filter to the filter chain. Sorts filters by weight.
45
+ #
46
+ # @param [#call] filter The filter object (proc, class, module, etc)
47
+ # @return [void]
48
+ def add_filter(filter)
49
+ @filters = (@filters << filter).sort_by do |f|
50
+ f.respond_to?(:weight) ? f.weight : DEFAULT_WEIGHT
51
+ end.reverse!
52
+ end
53
+
54
+ # Deletes a filter from the the filter chain.
55
+ #
56
+ # @param [Class] filter_class The class of the filter you want to delete
57
+ # @return [void]
58
+ # @since v3.1.0
59
+ def delete_filter(filter_class)
60
+ index = @filters.index { |f| f.class.name == filter_class.name }
61
+ @filters.delete_at(index) if index
62
+ end
63
+
64
+ # Applies all the filters in the filter chain to the given notice. Does not
65
+ # filter ignored notices.
66
+ #
67
+ # @param [Airbrake::Notice] notice The notice to be filtered
68
+ # @return [void]
69
+ # @todo Make it work with anything, not only notices
70
+ def refine(notice)
71
+ @filters.each do |filter|
72
+ break if notice.ignored?
73
+ filter.call(notice)
74
+ end
75
+ end
76
+
77
+ # @return [String] customized inspect to lessen the amount of clutter
78
+ def inspect
79
+ @filters.map(&:class).to_s
80
+ end
81
+
82
+ # @return [String] {#inspect} for PrettyPrint
83
+ def pretty_print(q)
84
+ q.text('[')
85
+
86
+ # Make nesting of the first element consistent on JRuby and MRI.
87
+ q.nest(2) { q.breakable } if @filters.any?
88
+
89
+ q.nest(2) do
90
+ q.seplist(@filters) { |f| q.pp(f.class) }
91
+ end
92
+ q.text(']')
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,29 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Adds user context to the notice object. Clears the context after it's
4
+ # attached.
5
+ #
6
+ # @api private
7
+ # @since v2.9.0
8
+ class ContextFilter
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize(context)
13
+ @context = context
14
+ @weight = 119
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ # @macro call_filter
19
+ def call(notice)
20
+ @mutex.synchronize do
21
+ return if @context.empty?
22
+
23
+ notice[:params][:airbrake_context] = @context.dup
24
+ @context.clear
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Attaches loaded dependencies to the notice object.
4
+ #
5
+ # @api private
6
+ # @since v2.10.0
7
+ class DependencyFilter
8
+ def initialize
9
+ @weight = 117
10
+ end
11
+
12
+ # @macro call_filter
13
+ def call(notice)
14
+ deps = {}
15
+ Gem.loaded_specs.map.with_object(deps) do |(name, spec), h|
16
+ h[name] = "#{spec.version}#{git_version(spec)}"
17
+ end
18
+
19
+ notice[:context][:versions] = {} unless notice[:context].key?(:versions)
20
+ notice[:context][:versions][:dependencies] = deps
21
+ end
22
+
23
+ private
24
+
25
+ def git_version(spec)
26
+ return unless spec.respond_to?(:git_version) || spec.git_version
27
+ spec.git_version.to_s
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ module Airbrake
2
+ module Filters
3
+ # ExceptionAttributesFilter attempts to call `#to_airbrake` on the stashed
4
+ # exception and attaches returned data to the notice object.
5
+ #
6
+ # @api private
7
+ # @since v2.10.0
8
+ class ExceptionAttributesFilter
9
+ include Loggable
10
+
11
+ def initialize
12
+ @weight = 118
13
+ end
14
+
15
+ # @macro call_filter
16
+ def call(notice)
17
+ exception = notice.stash[:exception]
18
+ return unless exception.respond_to?(:to_airbrake)
19
+
20
+ attributes = nil
21
+ begin
22
+ attributes = exception.to_airbrake
23
+ rescue StandardError => ex
24
+ logger.error(
25
+ "#{LOG_LABEL} #{exception.class}#to_airbrake failed. #{ex.class}: #{ex}"
26
+ )
27
+ end
28
+
29
+ unless attributes.is_a?(Hash)
30
+ logger.error(
31
+ "#{LOG_LABEL} #{self.class}: wanted Hash, got #{attributes.class}"
32
+ )
33
+ return
34
+ end
35
+
36
+ attributes.each do |key, attrs|
37
+ if notice[key]
38
+ notice[key].merge!(attrs)
39
+ else
40
+ notice[key] = attrs
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Replaces paths to gems with a placeholder.
4
+ # @api private
5
+ class GemRootFilter
6
+ # @return [String]
7
+ GEM_ROOT_LABEL = '/GEM_ROOT'.freeze
8
+
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize
13
+ @weight = 120
14
+ end
15
+
16
+ # @macro call_filter
17
+ def call(notice)
18
+ return unless defined?(Gem)
19
+
20
+ notice[:errors].each do |error|
21
+ Gem.path.each do |gem_path|
22
+ error[:backtrace].each do |frame|
23
+ # If the frame is unparseable, then 'file' is nil, thus nothing to
24
+ # filter (all frame's data is in 'function' instead).
25
+ next unless (file = frame[:file])
26
+ frame[:file] = file.sub(/\A#{gem_path}/, GEM_ROOT_LABEL)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,92 @@
1
+ require 'date'
2
+
3
+ module Airbrake
4
+ module Filters
5
+ # Attaches git checkout info to `context`. The info includes:
6
+ # * username
7
+ # * email
8
+ # * revision
9
+ # * time
10
+ #
11
+ # This information is used to track deploys automatically.
12
+ #
13
+ # @api private
14
+ # @since v2.12.0
15
+ class GitLastCheckoutFilter
16
+ # @return [Integer]
17
+ attr_reader :weight
18
+
19
+ # @return [Integer] least possible amount of columns in git's `logs/HEAD`
20
+ # file (checkout information is omitted)
21
+ MIN_HEAD_COLS = 6
22
+
23
+ include Loggable
24
+
25
+ # @param [String] root_directory
26
+ def initialize(root_directory)
27
+ @git_path = File.join(root_directory, '.git')
28
+ @weight = 116
29
+ @last_checkout = nil
30
+ end
31
+
32
+ # @macro call_filter
33
+ def call(notice)
34
+ return if notice[:context].key?(:lastCheckout)
35
+
36
+ if @last_checkout
37
+ notice[:context][:lastCheckout] = @last_checkout
38
+ return
39
+ end
40
+
41
+ return unless File.exist?(@git_path)
42
+ return unless (checkout = last_checkout)
43
+ notice[:context][:lastCheckout] = checkout
44
+ end
45
+
46
+ private
47
+
48
+ # rubocop:disable Metrics/AbcSize
49
+ def last_checkout
50
+ return unless (line = last_checkout_line)
51
+
52
+ parts = line.chomp.split("\t").first.split(' ')
53
+ if parts.size < MIN_HEAD_COLS
54
+ logger.error(
55
+ "#{LOG_LABEL} Airbrake::#{self.class.name}: can't parse line: #{line}"
56
+ )
57
+ return
58
+ end
59
+
60
+ author = parts[2..-4]
61
+ @last_checkout = {
62
+ username: author[0..1].join(' '),
63
+ email: parts[-3][1..-2],
64
+ revision: parts[1],
65
+ time: timestamp(parts[-2].to_i)
66
+ }
67
+ end
68
+ # rubocop:enable Metrics/AbcSize
69
+
70
+ def last_checkout_line
71
+ head_path = File.join(@git_path, 'logs', 'HEAD')
72
+ return unless File.exist?(head_path)
73
+
74
+ last_line = nil
75
+ IO.foreach(head_path) do |line|
76
+ last_line = line if checkout_line?(line)
77
+ end
78
+ last_line
79
+ end
80
+
81
+ def checkout_line?(line)
82
+ line.include?("\tclone:") ||
83
+ line.include?("\tpull:") ||
84
+ line.include?("\tcheckout:")
85
+ end
86
+
87
+ def timestamp(utime)
88
+ Time.at(utime).to_datetime.rfc3339
89
+ end
90
+ end
91
+ end
92
+ end