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,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,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