airbrake-ruby 3.1.0 → 3.2.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +197 -43
  3. data/lib/airbrake-ruby/config.rb +43 -11
  4. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  5. data/lib/airbrake-ruby/filter_chain.rb +32 -50
  6. data/lib/airbrake-ruby/filters/git_repository_filter.rb +9 -1
  7. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  8. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  9. data/lib/airbrake-ruby/ignorable.rb +44 -0
  10. data/lib/airbrake-ruby/notice.rb +2 -22
  11. data/lib/airbrake-ruby/{notifier.rb → notice_notifier.rb} +66 -46
  12. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  13. data/lib/airbrake-ruby/stat.rb +56 -0
  14. data/lib/airbrake-ruby/tdigest.rb +393 -0
  15. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  16. data/lib/airbrake-ruby/version.rb +1 -1
  17. data/spec/airbrake_spec.rb +57 -13
  18. data/spec/async_sender_spec.rb +0 -2
  19. data/spec/backtrace_spec.rb +0 -2
  20. data/spec/code_hunk_spec.rb +0 -2
  21. data/spec/config/validator_spec.rb +0 -2
  22. data/spec/config_spec.rb +16 -4
  23. data/spec/deploy_notifier_spec.rb +41 -0
  24. data/spec/file_cache.rb +0 -2
  25. data/spec/filter_chain_spec.rb +1 -7
  26. data/spec/filters/context_filter_spec.rb +0 -2
  27. data/spec/filters/dependency_filter_spec.rb +0 -2
  28. data/spec/filters/exception_attributes_filter_spec.rb +0 -2
  29. data/spec/filters/gem_root_filter_spec.rb +0 -2
  30. data/spec/filters/git_last_checkout_filter_spec.rb +0 -2
  31. data/spec/filters/git_repository_filter.rb +0 -2
  32. data/spec/filters/git_revision_filter_spec.rb +0 -2
  33. data/spec/filters/keys_blacklist_spec.rb +0 -2
  34. data/spec/filters/keys_whitelist_spec.rb +0 -2
  35. data/spec/filters/root_directory_filter_spec.rb +0 -2
  36. data/spec/filters/sql_filter_spec.rb +219 -0
  37. data/spec/filters/system_exit_filter_spec.rb +0 -2
  38. data/spec/filters/thread_filter_spec.rb +0 -2
  39. data/spec/ignorable_spec.rb +14 -0
  40. data/spec/nested_exception_spec.rb +0 -2
  41. data/spec/{notifier_spec.rb → notice_notifier_spec.rb} +24 -114
  42. data/spec/{notifier_spec → notice_notifier_spec}/options_spec.rb +40 -39
  43. data/spec/notice_spec.rb +2 -4
  44. data/spec/performance_notifier_spec.rb +287 -0
  45. data/spec/promise_spec.rb +0 -2
  46. data/spec/response_spec.rb +0 -2
  47. data/spec/stat_spec.rb +35 -0
  48. data/spec/sync_sender_spec.rb +0 -2
  49. data/spec/tdigest_spec.rb +230 -0
  50. data/spec/time_truncate_spec.rb +13 -0
  51. data/spec/truncator_spec.rb +0 -2
  52. metadata +34 -15
  53. data/lib/airbrake-ruby/route_sender.rb +0 -175
  54. data/spec/route_sender_spec.rb +0 -130
@@ -0,0 +1,47 @@
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
+ # @param [Airbrake::Config] config
14
+ def initialize(config)
15
+ @config =
16
+ if config.is_a?(Config)
17
+ config
18
+ else
19
+ loc = caller_locations(1..1).first
20
+ signature = "#{self.class.name}##{__method__}"
21
+ warn(
22
+ "#{loc.path}:#{loc.lineno}: warning: passing a Hash to #{signature} " \
23
+ 'is deprecated. Pass `Airbrake::Config` instead'
24
+ )
25
+ Config.new(config)
26
+ end
27
+
28
+ @sender = SyncSender.new(@config)
29
+ end
30
+
31
+ # @see Airbrake.create_deploy
32
+ def notify(deploy_info, promise = Airbrake::Promise.new)
33
+ if @config.ignored_environment?
34
+ return promise.reject("The '#{@config.environment}' environment is ignored")
35
+ end
36
+
37
+ deploy_info[:environment] ||= @config.environment
38
+ @sender.send(
39
+ deploy_info,
40
+ promise,
41
+ URI.join(@config.host, "api/v4/projects/#{@config.project_id}/deploys")
42
+ )
43
+
44
+ promise
45
+ end
46
+ end
47
+ end
@@ -1,29 +1,44 @@
1
1
  module Airbrake
2
- # Represents the mechanism for filtering notices. Defines a few default
3
- # filters.
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...
4
32
  #
5
33
  # @see Airbrake.add_filter
6
34
  # @api private
7
35
  # @since v1.0.0
8
36
  class FilterChain
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
37
  # @return [Integer]
19
38
  DEFAULT_WEIGHT = 0
20
39
 
21
- def initialize(config, context)
22
- @config = config
23
- @context = context
40
+ def initialize
24
41
  @filters = []
25
- DEFAULT_FILTERS.each { |f| add_filter(f.new) }
26
- add_default_filters
27
42
  end
28
43
 
29
44
  # Adds a filter to the filter chain. Sorts filters by weight.
@@ -51,6 +66,7 @@ module Airbrake
51
66
  #
52
67
  # @param [Airbrake::Notice] notice The notice to be filtered
53
68
  # @return [void]
69
+ # @todo Make it work with anything, not only notices
54
70
  def refine(notice)
55
71
  @filters.each do |filter|
56
72
  break if notice.ignored?
@@ -75,39 +91,5 @@ module Airbrake
75
91
  end
76
92
  q.text(']')
77
93
  end
78
-
79
- private
80
-
81
- # rubocop:disable Metrics/AbcSize
82
- def add_default_filters
83
- if (whitelist_keys = @config.whitelist_keys).any?
84
- add_filter(
85
- Airbrake::Filters::KeysWhitelist.new(@config.logger, whitelist_keys)
86
- )
87
- end
88
-
89
- if (blacklist_keys = @config.blacklist_keys).any?
90
- add_filter(
91
- Airbrake::Filters::KeysBlacklist.new(@config.logger, blacklist_keys)
92
- )
93
- end
94
-
95
- add_filter(Airbrake::Filters::ContextFilter.new(@context))
96
- add_filter(Airbrake::Filters::ExceptionAttributesFilter.new(@config.logger))
97
-
98
- return unless (root_directory = @config.root_directory)
99
- [
100
- Airbrake::Filters::RootDirectoryFilter,
101
- Airbrake::Filters::GitRevisionFilter,
102
- Airbrake::Filters::GitRepositoryFilter
103
- ].each do |filter|
104
- add_filter(filter.new(root_directory))
105
- end
106
-
107
- add_filter(
108
- Airbrake::Filters::GitLastCheckoutFilter.new(@config.logger, root_directory)
109
- )
110
- end
111
- # rubocop:enable Metrics/AbcSize
112
94
  end
113
95
  end
@@ -11,6 +11,7 @@ module Airbrake
11
11
  def initialize(root_directory)
12
12
  @git_path = File.join(root_directory, '.git')
13
13
  @repository = nil
14
+ @git_version = Gem::Version.new(`git --version`.split.last)
14
15
  @weight = 116
15
16
  end
16
17
 
@@ -25,7 +26,14 @@ module Airbrake
25
26
 
26
27
  return unless File.exist?(@git_path)
27
28
 
28
- @repository = `cd #{@git_path} && git remote get-url origin`.chomp
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
+
29
37
  return unless @repository
30
38
  notice[:context][:repository] = @repository
31
39
  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
@@ -0,0 +1,37 @@
1
+ module Airbrake
2
+ # HashKeyable allows instances of the class to be used as a Hash key in a
3
+ # consistent manner.
4
+ #
5
+ # The class that includes it must implement *to_h*, which defines properties
6
+ # that all of the instances must share in order to produce the same {#hash}.
7
+ #
8
+ # @example
9
+ # class C
10
+ # include Airbrake::HashKeyable
11
+ #
12
+ # def initialize(key)
13
+ # @key = key
14
+ # end
15
+ #
16
+ # def to_h
17
+ # { 'key' => @key }
18
+ # end
19
+ # end
20
+ #
21
+ # h = {}
22
+ # h[C.new('key1')] = 1
23
+ # h[C.new('key1')] #=> 1
24
+ # h[C.new('key2')] #=> nil
25
+ module HashKeyable
26
+ # @param [Object] other
27
+ # @return [Boolean]
28
+ def eql?(other)
29
+ other.is_a?(self.class) && other.hash == hash
30
+ end
31
+
32
+ # @return [Integer]
33
+ def hash
34
+ to_h.hash
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ module Airbrake
2
+ # Ignorable contains methods that allow the includee to be ignored.
3
+ #
4
+ # @example
5
+ # class A
6
+ # include Airbrake::Ignorable
7
+ # end
8
+ #
9
+ # a = A.new
10
+ # a.ignore!
11
+ # a.ignored? #=> true
12
+ #
13
+ # @since v3.2.0
14
+ # @api private
15
+ module Ignorable
16
+ attr_accessor :ignored
17
+
18
+ # Checks whether the instance was ignored.
19
+ # @return [Boolean]
20
+ # @see #ignore!
21
+ # rubocop:disable Style/DoubleNegation
22
+ def ignored?
23
+ !!ignored
24
+ end
25
+ # rubocop:enable Style/DoubleNegation
26
+
27
+ # Ignores an instance. Ignored instances must never reach the Airbrake
28
+ # dashboard.
29
+ # @return [void]
30
+ # @see #ignored?
31
+ def ignore!
32
+ self.ignored = true
33
+ end
34
+
35
+ private
36
+
37
+ # A method that is meant to be used as a guard.
38
+ # @raise [Airbrake::Error] when instance is ignored
39
+ def raise_if_ignored
40
+ return unless ignored?
41
+ raise Airbrake::Error, "cannot access ignored #{self.class}"
42
+ end
43
+ end
44
+ end
@@ -49,6 +49,8 @@ module Airbrake
49
49
  # @return [String]
50
50
  DEFAULT_SEVERITY = 'error'.freeze
51
51
 
52
+ include Ignorable
53
+
52
54
  # @since v1.7.0
53
55
  # @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
54
56
  # in filters
@@ -91,23 +93,6 @@ module Airbrake
91
93
  end
92
94
  end
93
95
 
94
- # Ignores a notice. Ignored notices never reach the Airbrake dashboard.
95
- #
96
- # @return [void]
97
- # @see #ignored?
98
- # @note Ignored noticed can't be unignored
99
- def ignore!
100
- @payload = nil
101
- end
102
-
103
- # Checks whether the notice was ignored.
104
- #
105
- # @return [Boolean]
106
- # @see #ignore!
107
- def ignored?
108
- @payload.nil?
109
- end
110
-
111
96
  # Reads a value from notice's payload.
112
97
  #
113
98
  # @return [Object]
@@ -160,11 +145,6 @@ module Airbrake
160
145
  }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
161
146
  end
162
147
 
163
- def raise_if_ignored
164
- return unless ignored?
165
- raise Airbrake::Error, 'cannot access ignored notice'
166
- end
167
-
168
148
  def truncate
169
149
  TRUNCATABLE_KEYS.each do |key|
170
150
  @payload[key] = @truncator.truncate(@payload[key])
@@ -1,12 +1,12 @@
1
1
  module Airbrake
2
- # This class is reponsible for sending notices to Airbrake. It supports
2
+ # NoticeNotifier is reponsible for sending notices to Airbrake. It supports
3
3
  # synchronous and asynchronous delivery.
4
4
  #
5
5
  # @see Airbrake::Config The list of options
6
6
  # @since v1.0.0
7
- # @api private
7
+ # @api public
8
8
  # rubocop:disable Metrics/ClassLength
9
- class Notifier
9
+ class NoticeNotifier
10
10
  # @return [String] the label to be prepended to the log output
11
11
  LOG_LABEL = '**Airbrake:'.freeze
12
12
 
@@ -16,31 +16,44 @@ module Airbrake
16
16
  "project_key=\"%<project_key>s\" " \
17
17
  "host=\"%<host>s\" filter_chain=%<filter_chain>s>".freeze
18
18
 
19
- # Creates a new Airbrake notifier with the given config options.
20
- #
21
- # @example Configuring with a Hash
22
- # airbrake = Airbrake.new(project_id: 123, project_key: '321')
19
+ # @return [Array<Class>] filters to be executed first
20
+ DEFAULT_FILTERS = [
21
+ Airbrake::Filters::SystemExitFilter,
22
+ Airbrake::Filters::GemRootFilter
23
+
24
+ # Optional filters (must be included by users):
25
+ # Airbrake::Filters::ThreadFilter
26
+ ].freeze
27
+
28
+ # Creates a new notice notifier with the given config options.
23
29
  #
24
- # @example Configuring with an Airbrake::Config
30
+ # @example
25
31
  # config = Airbrake::Config.new
26
32
  # config.project_id = 123
27
33
  # config.project_key = '321'
28
- # airbake = Airbrake.new(config)
34
+ # notice_notifier = Airbrake::NoticeNotifier.new(config)
29
35
  #
30
- # @param [Hash, Airbrake::Config] user_config The config that contains
31
- # information about how the notifier should operate
32
- # @raise [Airbrake::Error] when either +project_id+ or +project_key+
33
- # is missing (or both)
34
- def initialize(user_config)
35
- @config = (user_config.is_a?(Config) ? user_config : Config.new(user_config))
36
-
37
- raise Airbrake::Error, @config.validation_error_message unless @config.valid?
36
+ # @param [Airbrake::Config] config
37
+ def initialize(config)
38
+ @config =
39
+ if config.is_a?(Config)
40
+ config
41
+ else
42
+ loc = caller_locations(1..1).first
43
+ signature = "#{self.class.name}##{__method__}"
44
+ warn(
45
+ "#{loc.path}:#{loc.lineno}: warning: passing a Hash to #{signature} " \
46
+ 'is deprecated. Pass `Airbrake::Config` instead'
47
+ )
48
+ Config.new(config)
49
+ end
38
50
 
39
51
  @context = {}
40
- @filter_chain = FilterChain.new(@config, @context)
52
+ @filter_chain = FilterChain.new
41
53
  @async_sender = AsyncSender.new(@config)
42
54
  @sync_sender = SyncSender.new(@config)
43
- @route_sender = RouteSender.new(@config)
55
+
56
+ add_default_filters
44
57
  end
45
58
 
46
59
  # @macro see_public_api_method
@@ -83,18 +96,6 @@ module Airbrake
83
96
  @async_sender.close
84
97
  end
85
98
 
86
- # @macro see_public_api_method
87
- def create_deploy(deploy_params)
88
- deploy_params[:environment] ||= @config.environment
89
- promise = Airbrake::Promise.new
90
- @sync_sender.send(
91
- deploy_params,
92
- promise,
93
- URI.join(@config.host, "api/v4/projects/#{@config.project_id}/deploys")
94
- )
95
- promise
96
- end
97
-
98
99
  # @macro see_public_api_method
99
100
  def configured?
100
101
  @config.valid?
@@ -105,21 +106,6 @@ module Airbrake
105
106
  @context.merge!(context)
106
107
  end
107
108
 
108
- # @macro see_public_api_method
109
- def notify_request(request_info)
110
- promise = Airbrake::Promise.new
111
-
112
- if @config.ignored_environment?
113
- return promise.reject("The '#{@config.environment}' environment is ignored")
114
- end
115
-
116
- unless @config.route_stats
117
- return promise.reject("The Route Stats feature is disabled")
118
- end
119
-
120
- @route_sender.notify_request(request_info, promise)
121
- end
122
-
123
109
  # @return [String] customized inspect to lessen the amount of clutter
124
110
  def inspect
125
111
  format(
@@ -193,6 +179,40 @@ module Airbrake
193
179
  return caller_copy if clean_bt.empty?
194
180
  clean_bt
195
181
  end
182
+
183
+ # rubocop:disable Metrics/AbcSize
184
+ def add_default_filters
185
+ DEFAULT_FILTERS.each { |f| add_filter(f.new) }
186
+
187
+ if (whitelist_keys = @config.whitelist_keys).any?
188
+ add_filter(
189
+ Airbrake::Filters::KeysWhitelist.new(@config.logger, whitelist_keys)
190
+ )
191
+ end
192
+
193
+ if (blacklist_keys = @config.blacklist_keys).any?
194
+ add_filter(
195
+ Airbrake::Filters::KeysBlacklist.new(@config.logger, blacklist_keys)
196
+ )
197
+ end
198
+
199
+ add_filter(Airbrake::Filters::ContextFilter.new(@context))
200
+ add_filter(Airbrake::Filters::ExceptionAttributesFilter.new(@config.logger))
201
+
202
+ return unless (root_directory = @config.root_directory)
203
+ [
204
+ Airbrake::Filters::RootDirectoryFilter,
205
+ Airbrake::Filters::GitRevisionFilter,
206
+ Airbrake::Filters::GitRepositoryFilter
207
+ ].each do |filter|
208
+ add_filter(filter.new(root_directory))
209
+ end
210
+
211
+ add_filter(
212
+ Airbrake::Filters::GitLastCheckoutFilter.new(@config.logger, root_directory)
213
+ )
214
+ end
215
+ # rubocop:enable Metrics/AbcSize
196
216
  end
197
217
  # rubocop:enable Metrics/ClassLength
198
218
  end