airbrake-ruby 4.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,64 @@
|
|
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 = detect_git_version
|
15
|
+
@weight = 116
|
16
|
+
end
|
17
|
+
|
18
|
+
# @macro call_filter
|
19
|
+
def call(notice)
|
20
|
+
return if notice[:context].key?(:repository)
|
21
|
+
attach_repository(notice)
|
22
|
+
end
|
23
|
+
|
24
|
+
def attach_repository(notice)
|
25
|
+
if @repository
|
26
|
+
notice[:context][:repository] = @repository
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
return unless File.exist?(@git_path)
|
31
|
+
return unless @git_version
|
32
|
+
|
33
|
+
@repository =
|
34
|
+
if @git_version >= Gem::Version.new('2.7.0')
|
35
|
+
`cd #{@git_path} && git config --get remote.origin.url`.chomp
|
36
|
+
else
|
37
|
+
"`git remote get-url` is unsupported in git #{@git_version}. " \
|
38
|
+
'Consider an upgrade to 2.7+'
|
39
|
+
end
|
40
|
+
|
41
|
+
return unless @repository
|
42
|
+
notice[:context][:repository] = @repository
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def detect_git_version
|
48
|
+
return unless which('git')
|
49
|
+
Gem::Version.new(`git --version`.split[2])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Cross-platform way to tell if an executable is accessible.
|
53
|
+
def which(cmd)
|
54
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
55
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).find do |path|
|
56
|
+
exts.find do |ext|
|
57
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
58
|
+
File.executable?(exe) && !File.directory?(exe)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
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,49 @@
|
|
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
|
+
# [:email, /credit/i, 'password']
|
9
|
+
# )
|
10
|
+
# airbrake.add_filter(filter)
|
11
|
+
# airbrake.notify(StandardError.new('App crashed!'), {
|
12
|
+
# user: 'John'
|
13
|
+
# password: 's3kr3t',
|
14
|
+
# email: 'john@example.com',
|
15
|
+
# credit_card: '5555555555554444'
|
16
|
+
# })
|
17
|
+
#
|
18
|
+
# # The dashboard will display this parameter as is, but all other
|
19
|
+
# # values will be filtered:
|
20
|
+
# # { user: 'John',
|
21
|
+
# # password: '[Filtered]',
|
22
|
+
# # email: '[Filtered]',
|
23
|
+
# # credit_card: '[Filtered]' }
|
24
|
+
#
|
25
|
+
# @see KeysWhitelist
|
26
|
+
# @see KeysFilter
|
27
|
+
# @api private
|
28
|
+
class KeysBlacklist
|
29
|
+
include KeysFilter
|
30
|
+
|
31
|
+
def initialize(*)
|
32
|
+
super
|
33
|
+
@weight = -110
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] true if the key matches at least one pattern, false
|
37
|
+
# otherwise
|
38
|
+
def should_filter?(key)
|
39
|
+
@patterns.any? 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,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
|
+
include Loggable
|
30
|
+
|
31
|
+
# @return [Integer]
|
32
|
+
attr_reader :weight
|
33
|
+
|
34
|
+
# Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
|
35
|
+
# +patterns+ for filtering a notice's payload.
|
36
|
+
#
|
37
|
+
# @param [Array<String,Regexp,Symbol>] patterns
|
38
|
+
def initialize(patterns)
|
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,48 @@
|
|
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
|
+
# [:email, /credit/i, 'password']
|
9
|
+
# )
|
10
|
+
# airbrake.add_filter(filter)
|
11
|
+
# airbrake.notify(StandardError.new('App crashed!'), {
|
12
|
+
# user: 'John',
|
13
|
+
# password: 's3kr3t',
|
14
|
+
# email: 'john@example.com',
|
15
|
+
# account_id: 42
|
16
|
+
# })
|
17
|
+
#
|
18
|
+
# # The dashboard will display this parameters as filtered, but other
|
19
|
+
# # values won't be affected:
|
20
|
+
# # { user: 'John',
|
21
|
+
# # password: '[Filtered]',
|
22
|
+
# # email: 'john@example.com',
|
23
|
+
# # account_id: 42 }
|
24
|
+
#
|
25
|
+
# @see KeysBlacklist
|
26
|
+
# @see KeysFilter
|
27
|
+
class KeysWhitelist
|
28
|
+
include KeysFilter
|
29
|
+
|
30
|
+
def initialize(*)
|
31
|
+
super
|
32
|
+
@weight = -100
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Boolean] true if the key doesn't match any pattern, false
|
36
|
+
# otherwise.
|
37
|
+
def should_filter?(key)
|
38
|
+
@patterns.none? do |pattern|
|
39
|
+
if pattern.is_a?(Regexp)
|
40
|
+
key.match(pattern)
|
41
|
+
else
|
42
|
+
key.to_s == pattern.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
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,125 @@
|
|
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 [Regexp] the regexp that is applied after the feature regexps
|
41
|
+
# were used
|
42
|
+
POST_FILTER = /(?<=[values|in ]\().+(?=\))/i
|
43
|
+
|
44
|
+
# @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
|
45
|
+
# to a certain dialect
|
46
|
+
DIALECT_FEATURES = {
|
47
|
+
default: ALL_FEATURES.keys,
|
48
|
+
mysql: %i[
|
49
|
+
single_quotes double_quotes numeric_literals boolean_literals
|
50
|
+
hexadecimal_literals comments multi_line_comments
|
51
|
+
].freeze,
|
52
|
+
postgres: %i[
|
53
|
+
single_quotes dollar_quotes uuids numeric_literals boolean_literals
|
54
|
+
comments multi_line_comments
|
55
|
+
].freeze,
|
56
|
+
sqlite: %i[
|
57
|
+
single_quotes numeric_literals boolean_literals hexadecimal_literals
|
58
|
+
comments multi_line_comments
|
59
|
+
].freeze,
|
60
|
+
oracle: %i[
|
61
|
+
single_quotes oracle_quoted_strings numeric_literals comments
|
62
|
+
multi_line_comments
|
63
|
+
].freeze,
|
64
|
+
cassandra: %i[
|
65
|
+
single_quotes uuids numeric_literals boolean_literals
|
66
|
+
hexadecimal_literals comments multi_line_comments
|
67
|
+
].freeze
|
68
|
+
}.freeze
|
69
|
+
|
70
|
+
# @return [Hash{Symbol=>Regexp}] a set of regexps to check for unmatches
|
71
|
+
# quotes after filtering (should be none)
|
72
|
+
UNMATCHED_PAIR = {
|
73
|
+
mysql: %r{'|"|/\*|\*/},
|
74
|
+
mysql2: %r{'|"|/\*|\*/},
|
75
|
+
postgres: %r{'|/\*|\*/|\$(?!\?)},
|
76
|
+
sqlite: %r{'|/\*|\*/},
|
77
|
+
cassandra: %r{'|/\*|\*/},
|
78
|
+
oracle: %r{'|/\*|\*/},
|
79
|
+
oracle_enhanced: %r{'|/\*|\*/}
|
80
|
+
}.freeze
|
81
|
+
|
82
|
+
# @return [Array<Regexp>] the list of queries to be ignored
|
83
|
+
IGNORED_QUERIES = [
|
84
|
+
/\ACOMMIT/i,
|
85
|
+
/\ABEGIN/i,
|
86
|
+
/\ASET/i,
|
87
|
+
/\ASHOW/i,
|
88
|
+
/\AWITH/i,
|
89
|
+
/FROM pg_attribute/i
|
90
|
+
].freeze
|
91
|
+
|
92
|
+
def initialize(dialect)
|
93
|
+
@dialect =
|
94
|
+
case dialect
|
95
|
+
when /mysql/i then :mysql
|
96
|
+
when /postgres/i then :postgres
|
97
|
+
when /sqlite/i then :sqlite
|
98
|
+
when /oracle/i then :oracle
|
99
|
+
when /cassandra/i then :cassandra
|
100
|
+
else
|
101
|
+
:default
|
102
|
+
end
|
103
|
+
|
104
|
+
features = DIALECT_FEATURES[@dialect].map { |f| ALL_FEATURES[f] }
|
105
|
+
@regexp = Regexp.union(features)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @param [Airbrake::Query] resource
|
109
|
+
def call(resource)
|
110
|
+
return unless resource.respond_to?(:query)
|
111
|
+
|
112
|
+
query = resource.query
|
113
|
+
if IGNORED_QUERIES.any? { |q| q =~ query }
|
114
|
+
resource.ignore!
|
115
|
+
return
|
116
|
+
end
|
117
|
+
|
118
|
+
q = query.gsub(@regexp, FILTERED)
|
119
|
+
q.gsub!(POST_FILTER, FILTERED) if q =~ POST_FILTER
|
120
|
+
q = ERROR_MSG if UNMATCHED_PAIR[@dialect] =~ q
|
121
|
+
resource.query = q
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|