airbrake-ruby 3.2.2-java

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +554 -0
  3. data/lib/airbrake-ruby/async_sender.rb +119 -0
  4. data/lib/airbrake-ruby/backtrace.rb +194 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +53 -0
  6. data/lib/airbrake-ruby/config.rb +238 -0
  7. data/lib/airbrake-ruby/config/validator.rb +63 -0
  8. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  9. data/lib/airbrake-ruby/file_cache.rb +48 -0
  10. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  11. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  18. data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
  19. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  20. data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  25. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  26. data/lib/airbrake-ruby/ignorable.rb +44 -0
  27. data/lib/airbrake-ruby/nested_exception.rb +39 -0
  28. data/lib/airbrake-ruby/notice.rb +165 -0
  29. data/lib/airbrake-ruby/notice_notifier.rb +228 -0
  30. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  31. data/lib/airbrake-ruby/promise.rb +99 -0
  32. data/lib/airbrake-ruby/response.rb +71 -0
  33. data/lib/airbrake-ruby/stat.rb +56 -0
  34. data/lib/airbrake-ruby/sync_sender.rb +111 -0
  35. data/lib/airbrake-ruby/tdigest.rb +393 -0
  36. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  37. data/lib/airbrake-ruby/truncator.rb +115 -0
  38. data/lib/airbrake-ruby/version.rb +6 -0
  39. data/spec/airbrake_spec.rb +171 -0
  40. data/spec/async_sender_spec.rb +154 -0
  41. data/spec/backtrace_spec.rb +438 -0
  42. data/spec/code_hunk_spec.rb +118 -0
  43. data/spec/config/validator_spec.rb +189 -0
  44. data/spec/config_spec.rb +281 -0
  45. data/spec/deploy_notifier_spec.rb +41 -0
  46. data/spec/file_cache.rb +36 -0
  47. data/spec/filter_chain_spec.rb +83 -0
  48. data/spec/filters/context_filter_spec.rb +25 -0
  49. data/spec/filters/dependency_filter_spec.rb +14 -0
  50. data/spec/filters/exception_attributes_filter_spec.rb +63 -0
  51. data/spec/filters/gem_root_filter_spec.rb +44 -0
  52. data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
  53. data/spec/filters/git_repository_filter.rb +53 -0
  54. data/spec/filters/git_revision_filter_spec.rb +126 -0
  55. data/spec/filters/keys_blacklist_spec.rb +236 -0
  56. data/spec/filters/keys_whitelist_spec.rb +205 -0
  57. data/spec/filters/root_directory_filter_spec.rb +42 -0
  58. data/spec/filters/sql_filter_spec.rb +219 -0
  59. data/spec/filters/system_exit_filter_spec.rb +14 -0
  60. data/spec/filters/thread_filter_spec.rb +279 -0
  61. data/spec/fixtures/notroot.txt +7 -0
  62. data/spec/fixtures/project_root/code.rb +221 -0
  63. data/spec/fixtures/project_root/empty_file.rb +0 -0
  64. data/spec/fixtures/project_root/long_line.txt +1 -0
  65. data/spec/fixtures/project_root/short_file.rb +3 -0
  66. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  67. data/spec/helpers.rb +9 -0
  68. data/spec/ignorable_spec.rb +14 -0
  69. data/spec/nested_exception_spec.rb +75 -0
  70. data/spec/notice_notifier_spec.rb +436 -0
  71. data/spec/notice_notifier_spec/options_spec.rb +266 -0
  72. data/spec/notice_spec.rb +297 -0
  73. data/spec/performance_notifier_spec.rb +287 -0
  74. data/spec/promise_spec.rb +165 -0
  75. data/spec/response_spec.rb +82 -0
  76. data/spec/spec_helper.rb +102 -0
  77. data/spec/stat_spec.rb +35 -0
  78. data/spec/sync_sender_spec.rb +140 -0
  79. data/spec/tdigest_spec.rb +230 -0
  80. data/spec/time_truncate_spec.rb +13 -0
  81. data/spec/truncator_spec.rb +238 -0
  82. metadata +278 -0
@@ -0,0 +1,42 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Attaches git repository URL to `context`.
4
+ # @api private
5
+ # @since v2.12.0
6
+ class GitRepositoryFilter
7
+ # @return [Integer]
8
+ attr_reader :weight
9
+
10
+ # @param [String] root_directory
11
+ def initialize(root_directory)
12
+ @git_path = File.join(root_directory, '.git')
13
+ @repository = nil
14
+ @git_version = Gem::Version.new(`git --version`.split[2])
15
+ @weight = 116
16
+ end
17
+
18
+ # @macro call_filter
19
+ def call(notice)
20
+ return if notice[:context].key?(:repository)
21
+
22
+ if @repository
23
+ notice[:context][:repository] = @repository
24
+ return
25
+ end
26
+
27
+ return unless File.exist?(@git_path)
28
+
29
+ @repository =
30
+ if @git_version >= Gem::Version.new('2.7.0')
31
+ `cd #{@git_path} && git config --get remote.origin.url`.chomp
32
+ else
33
+ "`git remote get-url` is unsupported in git #{@git_version}. " \
34
+ 'Consider an upgrade to 2.7+'
35
+ end
36
+
37
+ return unless @repository
38
+ notice[:context][:repository] = @repository
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Attaches current git revision to `context`.
4
+ # @api private
5
+ # @since v2.11.0
6
+ class GitRevisionFilter
7
+ # @return [Integer]
8
+ attr_reader :weight
9
+
10
+ # @return [String]
11
+ PREFIX = 'ref: '.freeze
12
+
13
+ # @param [String] root_directory
14
+ def initialize(root_directory)
15
+ @git_path = File.join(root_directory, '.git')
16
+ @revision = nil
17
+ @weight = 116
18
+ end
19
+
20
+ # @macro call_filter
21
+ def call(notice)
22
+ return if notice[:context].key?(:revision)
23
+
24
+ if @revision
25
+ notice[:context][:revision] = @revision
26
+ return
27
+ end
28
+
29
+ return unless File.exist?(@git_path)
30
+
31
+ @revision = find_revision
32
+ return unless @revision
33
+ notice[:context][:revision] = @revision
34
+ end
35
+
36
+ private
37
+
38
+ def find_revision
39
+ head_path = File.join(@git_path, 'HEAD')
40
+ return unless File.exist?(head_path)
41
+
42
+ head = File.read(head_path)
43
+ return head unless head.start_with?(PREFIX)
44
+ head = head.chomp[PREFIX.size..-1]
45
+
46
+ ref_path = File.join(@git_path, head)
47
+ return File.read(ref_path).chomp if File.exist?(ref_path)
48
+
49
+ find_from_packed_refs(head)
50
+ end
51
+
52
+ def find_from_packed_refs(head)
53
+ packed_refs_path = File.join(@git_path, 'packed-refs')
54
+ return head unless File.exist?(packed_refs_path)
55
+
56
+ File.readlines(packed_refs_path).each do |line|
57
+ next if %w[# ^].include?(line[0])
58
+ next unless (parts = line.split(' ')).size == 2
59
+ return parts.first if parts.last == head
60
+ end
61
+
62
+ nil
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,50 @@
1
+ module Airbrake
2
+ module Filters
3
+ # A default Airbrake notice filter. Filters only specific keys listed in the
4
+ # list of parameters in the payload of a notice.
5
+ #
6
+ # @example
7
+ # filter = Airbrake::Filters::KeysBlacklist.new(
8
+ # Logger.new(STDOUT),
9
+ # [:email, /credit/i, 'password']
10
+ # )
11
+ # airbrake.add_filter(filter)
12
+ # airbrake.notify(StandardError.new('App crashed!'), {
13
+ # user: 'John'
14
+ # password: 's3kr3t',
15
+ # email: 'john@example.com',
16
+ # credit_card: '5555555555554444'
17
+ # })
18
+ #
19
+ # # The dashboard will display this parameter as is, but all other
20
+ # # values will be filtered:
21
+ # # { user: 'John',
22
+ # # password: '[Filtered]',
23
+ # # email: '[Filtered]',
24
+ # # credit_card: '[Filtered]' }
25
+ #
26
+ # @see KeysWhitelist
27
+ # @see KeysFilter
28
+ # @api private
29
+ class KeysBlacklist
30
+ include KeysFilter
31
+
32
+ def initialize(*)
33
+ super
34
+ @weight = -110
35
+ end
36
+
37
+ # @return [Boolean] true if the key matches at least one pattern, false
38
+ # otherwise
39
+ def should_filter?(key)
40
+ @patterns.any? do |pattern|
41
+ if pattern.is_a?(Regexp)
42
+ key.match(pattern)
43
+ else
44
+ key.to_s == pattern.to_s
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,140 @@
1
+ module Airbrake
2
+ # Namespace for all standard filters. Custom filters can also go under this
3
+ # namespace.
4
+ module Filters
5
+ # This is a filter helper that endows a class ability to filter notices'
6
+ # payload based on the return value of the +should_filter?+ method that a
7
+ # class that includes this module must implement.
8
+ #
9
+ # @see Notice
10
+ # @see KeysWhitelist
11
+ # @see KeysBlacklist
12
+ # @api private
13
+ module KeysFilter
14
+ # @return [String] The label to replace real values of filtered payload
15
+ FILTERED = '[Filtered]'.freeze
16
+
17
+ # @return [Array<String,Symbol,Regexp>] the array of classes instances of
18
+ # which can compared with payload keys
19
+ VALID_PATTERN_CLASSES = [String, Symbol, Regexp].freeze
20
+
21
+ # @return [Array<Symbol>] parts of a Notice's payload that can be modified
22
+ # by blacklist/whitelist filters
23
+ FILTERABLE_KEYS = %i[environment session params].freeze
24
+
25
+ # @return [Array<Symbol>] parts of a Notice's *context* payload that can
26
+ # be modified by blacklist/whitelist filters
27
+ FILTERABLE_CONTEXT_KEYS = %i[user headers].freeze
28
+
29
+ # @return [Integer]
30
+ attr_reader :weight
31
+
32
+ # Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
33
+ # +patterns+ for filtering a notice's payload.
34
+ #
35
+ # @param [Logger, #error] logger
36
+ # @param [Array<String,Regexp,Symbol>] patterns
37
+ def initialize(logger, patterns)
38
+ @logger = logger
39
+ @patterns = patterns
40
+ @valid_patterns = false
41
+ end
42
+
43
+ # @!macro call_filter
44
+ # This is a mandatory method required by any filter integrated with
45
+ # FilterChain.
46
+ #
47
+ # @param [Notice] notice the notice to be filtered
48
+ # @return [void]
49
+ # @see FilterChain
50
+ def call(notice)
51
+ unless @valid_patterns
52
+ eval_proc_patterns!
53
+ validate_patterns
54
+ end
55
+
56
+ FILTERABLE_KEYS.each { |key| filter_hash(notice[key]) }
57
+ FILTERABLE_CONTEXT_KEYS.each { |key| filter_context_key(notice, key) }
58
+
59
+ return unless notice[:context][:url]
60
+ filter_url(notice)
61
+ end
62
+
63
+ # @raise [NotImplementedError] if called directly
64
+ def should_filter?(_key)
65
+ raise NotImplementedError, 'method must be implemented in the included class'
66
+ end
67
+
68
+ private
69
+
70
+ def filter_hash(hash)
71
+ return hash unless hash.is_a?(Hash)
72
+
73
+ hash.each_key do |key|
74
+ if should_filter?(key.to_s)
75
+ hash[key] = FILTERED
76
+ elsif hash[key].is_a?(Hash)
77
+ filter_hash(hash[key])
78
+ elsif hash[key].is_a?(Array)
79
+ hash[key].each { |h| filter_hash(h) }
80
+ end
81
+ end
82
+ end
83
+
84
+ def filter_url_params(url)
85
+ url.query = Hash[URI.decode_www_form(url.query)].map do |key, val|
86
+ # Ruby < 2.2 raises InvalidComponentError if the query contains
87
+ # invalid characters, so be sure to escape individual components.
88
+ if should_filter?(key)
89
+ "#{URI.encode_www_form_component(key)}=[Filtered]"
90
+ else
91
+ "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(val)}"
92
+ end
93
+ end.join('&')
94
+
95
+ url.to_s
96
+ end
97
+
98
+ def filter_url(notice)
99
+ begin
100
+ url = URI(notice[:context][:url])
101
+ rescue URI::InvalidURIError
102
+ return
103
+ end
104
+
105
+ return unless url.query
106
+ notice[:context][:url] = filter_url_params(url)
107
+ end
108
+
109
+ def eval_proc_patterns!
110
+ return unless @patterns.any? { |pattern| pattern.is_a?(Proc) }
111
+
112
+ @patterns = @patterns.flat_map do |pattern|
113
+ next(pattern) unless pattern.respond_to?(:call)
114
+ pattern.call
115
+ end
116
+ end
117
+
118
+ def validate_patterns
119
+ @valid_patterns = @patterns.all? do |pattern|
120
+ VALID_PATTERN_CLASSES.any? { |c| pattern.is_a?(c) }
121
+ end
122
+
123
+ return if @valid_patterns
124
+
125
+ @logger.error(
126
+ "#{LOG_LABEL} one of the patterns in #{self.class} is invalid. " \
127
+ "Known patterns: #{@patterns}"
128
+ )
129
+ end
130
+
131
+ def filter_context_key(notice, key)
132
+ return unless notice[:context][key]
133
+ return if notice[:context][key] == FILTERED
134
+ return filter_hash(notice[:context][key]) unless should_filter?(key)
135
+
136
+ notice[:context][key] = FILTERED
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,49 @@
1
+ module Airbrake
2
+ module Filters
3
+ # A default Airbrake notice filter. Filters everything in the payload of a
4
+ # notice, but specified keys.
5
+ #
6
+ # @example
7
+ # filter = Airbrake::Filters::KeysBlacklist.new(
8
+ # Logger.new(STDOUT),
9
+ # [:email, /credit/i, 'password']
10
+ # )
11
+ # airbrake.add_filter(filter)
12
+ # airbrake.notify(StandardError.new('App crashed!'), {
13
+ # user: 'John',
14
+ # password: 's3kr3t',
15
+ # email: 'john@example.com',
16
+ # account_id: 42
17
+ # })
18
+ #
19
+ # # The dashboard will display this parameters as filtered, but other
20
+ # # values won't be affected:
21
+ # # { user: 'John',
22
+ # # password: '[Filtered]',
23
+ # # email: 'john@example.com',
24
+ # # account_id: 42 }
25
+ #
26
+ # @see KeysBlacklist
27
+ # @see KeysFilter
28
+ class KeysWhitelist
29
+ include KeysFilter
30
+
31
+ def initialize(*)
32
+ super
33
+ @weight = -100
34
+ end
35
+
36
+ # @return [Boolean] true if the key doesn't match any pattern, false
37
+ # otherwise.
38
+ def should_filter?(key)
39
+ @patterns.none? do |pattern|
40
+ if pattern.is_a?(Regexp)
41
+ key.match(pattern)
42
+ else
43
+ key.to_s == pattern.to_s
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,28 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Replaces root directory with a label.
4
+ # @api private
5
+ class RootDirectoryFilter
6
+ # @return [String]
7
+ PROJECT_ROOT_LABEL = '/PROJECT_ROOT'.freeze
8
+
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize(root_directory)
13
+ @root_directory = root_directory
14
+ @weight = 100
15
+ end
16
+
17
+ # @macro call_filter
18
+ def call(notice)
19
+ notice[:errors].each do |error|
20
+ error[:backtrace].each do |frame|
21
+ next unless (file = frame[:file])
22
+ file.sub!(/\A#{@root_directory}/, PROJECT_ROOT_LABEL)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,104 @@
1
+ module Airbrake
2
+ module Filters
3
+ # SqlFilter filters out sensitive data from {Airbrake::Query}. Sensitive
4
+ # data is everything that is not table names or fields (e.g. column values
5
+ # and such).
6
+ #
7
+ # Supports the following SQL dialects:
8
+ # * PostgreSQL
9
+ # * MySQL
10
+ # * SQLite
11
+ # * Cassandra
12
+ # * Oracle
13
+ #
14
+ # @api private
15
+ # @since v3.2.0
16
+ class SqlFilter
17
+ # @return [String] the label to replace real values of filtered query
18
+ FILTERED = '?'.freeze
19
+
20
+ # @return [String] the string that will replace the query in case we
21
+ # cannot filter it
22
+ ERROR_MSG = 'Error: Airbrake::Query was not filtered'.freeze
23
+
24
+ # @return [Hash{Symbol=>Regexp}] matchers for certain features of SQL
25
+ ALL_FEATURES = {
26
+ # rubocop:disable Metrics/LineLength
27
+ single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
28
+ double_quotes: /"(?:[^"]|"")*?(?:\\".*|"(?!"))/,
29
+ dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
30
+ uuids: /\{?(?:[0-9a-fA-F]\-*){32}\}?/,
31
+ numeric_literals: /\b-?(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/,
32
+ boolean_literals: /\b(?:true|false|null)\b/i,
33
+ hexadecimal_literals: /0x[0-9a-fA-F]+/,
34
+ comments: /(?:#|--).*?(?=\r|\n|$)/i,
35
+ multi_line_comments: %r{/\*(?:[^/]|/[^*])*?(?:\*/|/\*.*)},
36
+ oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/
37
+ # rubocop:enable Metrics/LineLength
38
+ }.freeze
39
+
40
+ # @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
41
+ # to a certain dialect
42
+ DIALECT_FEATURES = {
43
+ default: ALL_FEATURES.keys,
44
+ mysql: %i[
45
+ single_quotes double_quotes numeric_literals boolean_literals
46
+ hexadecimal_literals comments multi_line_comments
47
+ ].freeze,
48
+ postgres: %i[
49
+ single_quotes dollar_quotes uuids numeric_literals boolean_literals
50
+ comments multi_line_comments
51
+ ].freeze,
52
+ sqlite: %i[
53
+ single_quotes numeric_literals boolean_literals hexadecimal_literals
54
+ comments multi_line_comments
55
+ ].freeze,
56
+ oracle: %i[
57
+ single_quotes oracle_quoted_strings numeric_literals comments
58
+ multi_line_comments
59
+ ].freeze,
60
+ cassandra: %i[
61
+ single_quotes uuids numeric_literals boolean_literals
62
+ hexadecimal_literals comments multi_line_comments
63
+ ].freeze
64
+ }.freeze
65
+
66
+ # @return [Hash{Symbol=>Regexp}] a set of regexps to check for unmatches
67
+ # quotes after filtering (should be none)
68
+ UNMATCHED_PAIR = {
69
+ mysql: %r{'|"|/\*|\*/},
70
+ mysql2: %r{'|"|/\*|\*/},
71
+ postgres: %r{'|/\*|\*/|\$(?!\?)},
72
+ sqlite: %r{'|/\*|\*/},
73
+ cassandra: %r{'|/\*|\*/},
74
+ oracle: %r{'|/\*|\*/},
75
+ oracle_enhanced: %r{'|/\*|\*/}
76
+ }.freeze
77
+
78
+ def initialize(dialect)
79
+ @dialect =
80
+ case dialect
81
+ when /mysql/i then :mysql
82
+ when /postgres/i then :postgres
83
+ when /sqlite/i then :sqlite
84
+ when /oracle/i then :oracle
85
+ when /cassandra/i then :cassandra
86
+ else
87
+ :default
88
+ end
89
+
90
+ features = DIALECT_FEATURES[@dialect].map { |f| ALL_FEATURES[f] }
91
+ @regexp = Regexp.union(features)
92
+ end
93
+
94
+ # @param [Airbrake::Query] resource
95
+ def call(resource)
96
+ return unless resource.respond_to?(:query)
97
+
98
+ q = resource.query.gsub(@regexp, FILTERED)
99
+ q = ERROR_MSG if UNMATCHED_PAIR[@dialect] =~ q
100
+ resource.query = q
101
+ end
102
+ end
103
+ end
104
+ end