airbrake-ruby 4.8.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 +128 -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 +276 -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,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,54 @@
|
|
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
|
+
# @since ?.?.?
|
44
|
+
# @return [void]
|
45
|
+
def self.reset
|
46
|
+
@data = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.data
|
50
|
+
@data ||= {}
|
51
|
+
end
|
52
|
+
private_class_method :data
|
53
|
+
end
|
54
|
+
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
|