bugno-ruby 0.1.9
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +32 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +171 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bugno.gemspec +41 -0
- data/lib/bugno.rb +30 -0
- data/lib/bugno/backtrace.rb +82 -0
- data/lib/bugno/configuration.rb +49 -0
- data/lib/bugno/encoding/encoder.rb +65 -0
- data/lib/bugno/encoding/encoding.rb +29 -0
- data/lib/bugno/encoding/legacy_encoder.rb +22 -0
- data/lib/bugno/event.rb +52 -0
- data/lib/bugno/filter/params.rb +117 -0
- data/lib/bugno/generator/bugno_generator.rb +13 -0
- data/lib/bugno/generator/bugno_initializer.rb.erb +36 -0
- data/lib/bugno/handler.rb +38 -0
- data/lib/bugno/logger.rb +16 -0
- data/lib/bugno/middleware/rails/active_job_extensions.rb +44 -0
- data/lib/bugno/middleware/rails/bugno.rb +24 -0
- data/lib/bugno/middleware/rails/show_exceptions.rb +35 -0
- data/lib/bugno/railtie.rb +25 -0
- data/lib/bugno/reporter.rb +33 -0
- data/lib/bugno/request_data_extractor.rb +150 -0
- data/lib/bugno/version.rb +5 -0
- metadata +159 -0
data/lib/bugno.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bugno/logger'
|
4
|
+
require 'bugno/configuration'
|
5
|
+
require 'bugno/event'
|
6
|
+
|
7
|
+
if defined?(Rails)
|
8
|
+
require 'bugno/railtie'
|
9
|
+
require 'bugno/generator/bugno_generator'
|
10
|
+
end
|
11
|
+
|
12
|
+
module Bugno
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :configuration
|
17
|
+
|
18
|
+
def configuration
|
19
|
+
@configuration ||= Configuration.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def configured?
|
23
|
+
!!configuration.api_key
|
24
|
+
end
|
25
|
+
|
26
|
+
def configure
|
27
|
+
yield(configuration)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bugno
|
4
|
+
class Backtrace
|
5
|
+
MAX_CONTEXT_LENGTH = 4
|
6
|
+
|
7
|
+
attr_reader :backtrace
|
8
|
+
attr_reader :files
|
9
|
+
|
10
|
+
def initialize(backtrace)
|
11
|
+
@backtrace = backtrace
|
12
|
+
@files = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_backtrace
|
16
|
+
@backtrace.map do |line|
|
17
|
+
match = line.match(/(.*):(\d+)(?::in `([^']+)')?/)
|
18
|
+
|
19
|
+
return nil unless match
|
20
|
+
|
21
|
+
filename = match[1]
|
22
|
+
lineno = match[2].to_i
|
23
|
+
method = match[3]&.tr('0-9', '')
|
24
|
+
frame_data = {
|
25
|
+
code: nil,
|
26
|
+
lineno: lineno,
|
27
|
+
method: method,
|
28
|
+
context: nil,
|
29
|
+
filename: filename
|
30
|
+
}
|
31
|
+
|
32
|
+
frame_data.merge(extra_frame_data(filename, lineno))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def extra_frame_data(filename, lineno)
|
37
|
+
file_lines = get_file_lines(filename)
|
38
|
+
|
39
|
+
{
|
40
|
+
code: code_data(file_lines, lineno),
|
41
|
+
context: context_data(file_lines, lineno)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_file_lines(filename)
|
46
|
+
@files[filename] ||= read_file(filename)
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_file(filename)
|
50
|
+
return unless File.exist?(filename)
|
51
|
+
|
52
|
+
File.read(filename).split("\n")
|
53
|
+
rescue StandardError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def code_data(file_lines, lineno)
|
58
|
+
file_lines[lineno - 1]
|
59
|
+
end
|
60
|
+
|
61
|
+
def context_data(file_lines, lineno)
|
62
|
+
{
|
63
|
+
pre: pre_data(file_lines, lineno),
|
64
|
+
post: post_data(file_lines, lineno)
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def post_data(file_lines, lineno)
|
69
|
+
from_line = lineno
|
70
|
+
number_of_lines = [from_line + MAX_CONTEXT_LENGTH, file_lines.size].min - from_line
|
71
|
+
|
72
|
+
file_lines[from_line, number_of_lines]
|
73
|
+
end
|
74
|
+
|
75
|
+
def pre_data(file_lines, lineno)
|
76
|
+
to_line = lineno - 2
|
77
|
+
from_line = [to_line - MAX_CONTEXT_LENGTH + 1, 0].max
|
78
|
+
|
79
|
+
file_lines[from_line, (to_line - from_line + 1)].select { |line| line && !line.empty? }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bugno
|
4
|
+
API_URL = 'https://api.bugno.io'
|
5
|
+
IGNORE_DEFAULT = [
|
6
|
+
'AbstractController::ActionNotFound',
|
7
|
+
'ActionController::InvalidAuthenticityToken',
|
8
|
+
'ActionController::RoutingError',
|
9
|
+
'ActionController::UnknownAction',
|
10
|
+
'ActiveRecord::RecordNotFound',
|
11
|
+
'ActiveJob::DeserializationError'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
class Configuration
|
15
|
+
attr_accessor :api_key
|
16
|
+
attr_accessor :environment
|
17
|
+
attr_accessor :framework
|
18
|
+
attr_accessor :api_url
|
19
|
+
attr_accessor :exclude_rails_exceptions
|
20
|
+
attr_accessor :excluded_exceptions
|
21
|
+
attr_accessor :scrub_fields
|
22
|
+
attr_accessor :scrub_headers
|
23
|
+
attr_accessor :scrub_user
|
24
|
+
attr_accessor :scrub_password
|
25
|
+
attr_accessor :scrub_whitelist
|
26
|
+
attr_accessor :current_user_method
|
27
|
+
attr_accessor :send_in_background
|
28
|
+
attr_accessor :usage_environments
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@api_key = nil
|
32
|
+
@environment = nil
|
33
|
+
@framework = 'ruby'
|
34
|
+
@api_url = API_URL
|
35
|
+
@excluded_exceptions = IGNORE_DEFAULT
|
36
|
+
@exclude_rails_exceptions = false
|
37
|
+
@scrub_fields = %i[passwd password password_confirmation secret
|
38
|
+
confirm_password password_confirmation secret_token
|
39
|
+
api_key access_token session_id]
|
40
|
+
@scrub_headers = ['Authorization']
|
41
|
+
@scrub_user = true
|
42
|
+
@scrub_password = true
|
43
|
+
@scrub_whitelist = []
|
44
|
+
@current_user_method = 'current_user'
|
45
|
+
@send_in_background = true
|
46
|
+
@usage_environments = %w[production]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bugno
|
4
|
+
module Encoding
|
5
|
+
class Encoder
|
6
|
+
ALL_ENCODINGS = [::Encoding::UTF_8, ::Encoding::ISO_8859_1, ::Encoding::ASCII_8BIT, ::Encoding::US_ASCII].freeze
|
7
|
+
ASCII_ENCODINGS = [::Encoding::US_ASCII, ::Encoding::ASCII_8BIT, ::Encoding::ISO_8859_1].freeze
|
8
|
+
ENCODING_OPTIONS = { invalid: :replace, undef: :replace, replace: '' }.freeze
|
9
|
+
UTF8 = 'UTF-8'
|
10
|
+
BINARY = 'binary'
|
11
|
+
|
12
|
+
attr_accessor :object
|
13
|
+
|
14
|
+
def initialize(object)
|
15
|
+
@object = object
|
16
|
+
end
|
17
|
+
|
18
|
+
def encode
|
19
|
+
value = object.to_s
|
20
|
+
encoding = value.encoding
|
21
|
+
|
22
|
+
# This will be most of cases so avoid force anything for them
|
23
|
+
encoded_value = if encoding == ::Encoding::UTF_8 && value.valid_encoding?
|
24
|
+
value
|
25
|
+
else
|
26
|
+
force_encoding(value).encode(*encoding_args(value))
|
27
|
+
end
|
28
|
+
|
29
|
+
object.is_a?(Symbol) ? encoded_value.to_sym : encoded_value
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def force_encoding(value)
|
35
|
+
return value if value.frozen?
|
36
|
+
|
37
|
+
value.force_encoding(detect_encoding(value)) if value.encoding == ::Encoding::UTF_8
|
38
|
+
|
39
|
+
value
|
40
|
+
end
|
41
|
+
|
42
|
+
def detect_encoding(v)
|
43
|
+
value = v.dup
|
44
|
+
|
45
|
+
ALL_ENCODINGS.detect do |encoding|
|
46
|
+
begin
|
47
|
+
# Seems #codepoints is faster than #valid_encoding?
|
48
|
+
value.force_encoding(encoding).encode(::Encoding::UTF_8).codepoints
|
49
|
+
true
|
50
|
+
rescue StandardError
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def encoding_args(value)
|
57
|
+
args = [UTF8]
|
58
|
+
args << BINARY if ASCII_ENCODINGS.include?(value.encoding)
|
59
|
+
args << ENCODING_OPTIONS
|
60
|
+
|
61
|
+
args
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bugno
|
4
|
+
module Encoding
|
5
|
+
class << self
|
6
|
+
attr_accessor :encoding_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.setup
|
10
|
+
if String.instance_methods.include?(:encode)
|
11
|
+
require 'bugno/encoding/encoder'
|
12
|
+
self.encoding_class = Bugno::Encoding::Encoder
|
13
|
+
else
|
14
|
+
require 'bugno/encoding/legacy_encoder'
|
15
|
+
self.encoding_class = Bugno::Encoding::LegacyEncoder
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.encode(object)
|
20
|
+
can_be_encoded = object.is_a?(String) || object.is_a?(Symbol)
|
21
|
+
|
22
|
+
return object unless can_be_encoded
|
23
|
+
|
24
|
+
encoding_class.new(object).encode
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Bugno::Encoding.setup
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'iconv'
|
4
|
+
|
5
|
+
module Buhhub
|
6
|
+
module Encoding
|
7
|
+
class LegacyEncoder
|
8
|
+
attr_accessor :object
|
9
|
+
|
10
|
+
def initialize(object)
|
11
|
+
@object = object
|
12
|
+
end
|
13
|
+
|
14
|
+
def encode
|
15
|
+
value = object.to_s
|
16
|
+
encoded_value = ::Iconv.conv('UTF-8//IGNORE', 'UTF-8', value)
|
17
|
+
|
18
|
+
object.is_a?(Symbol) ? encoded_value.to_sym : encoded_value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/bugno/event.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bugno/request_data_extractor'
|
4
|
+
require 'bugno/backtrace'
|
5
|
+
require 'bugno/encoding/encoding'
|
6
|
+
require 'rails' if defined?(Rails)
|
7
|
+
|
8
|
+
module Bugno
|
9
|
+
class Event
|
10
|
+
include RequestDataExtractor
|
11
|
+
attr_reader :data
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@env = options[:env]
|
15
|
+
@job = options[:job]
|
16
|
+
@exception = options[:exception]
|
17
|
+
build_data
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def build_data
|
23
|
+
@data = {
|
24
|
+
title: truncate(@exception.class.inspect),
|
25
|
+
message: truncate(@exception.message),
|
26
|
+
server_data: server_data,
|
27
|
+
created_at: Time.now.to_i,
|
28
|
+
framework: Bugno.configuration.framework,
|
29
|
+
environment: Bugno.configuration.environment
|
30
|
+
}
|
31
|
+
merge_extra_data
|
32
|
+
end
|
33
|
+
|
34
|
+
def merge_extra_data
|
35
|
+
@data.merge!(backtrace: Backtrace.new(@exception.backtrace).parse_backtrace) if @exception.backtrace
|
36
|
+
@data.merge!(extract_request_data_from_rack(@env)) if @env
|
37
|
+
@data.merge!(background_data: @job) if @job
|
38
|
+
end
|
39
|
+
|
40
|
+
# TODO: move to utility module
|
41
|
+
def truncate(string)
|
42
|
+
string.is_a?(String) ? string[0...3000] : ''
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: refactor
|
46
|
+
def server_data
|
47
|
+
data = { host: Socket.gethostname }
|
48
|
+
data[:root] = Rails.root.to_s if defined?(Rails)
|
49
|
+
data
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module Bugno
|
6
|
+
module Filter
|
7
|
+
class Params
|
8
|
+
SKIPPED_CLASSES = [::Tempfile].freeze
|
9
|
+
ATTACHMENT_CLASSES = %w[ActionDispatch::Http::UploadedFile Rack::Multipart::UploadedFile].freeze
|
10
|
+
SCRUB_ALL = :scrub_all
|
11
|
+
|
12
|
+
def self.call(*args)
|
13
|
+
new.call(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.scrub_value
|
17
|
+
'[FILTERED]'
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(options = {})
|
21
|
+
params = options[:params]
|
22
|
+
return {} unless params
|
23
|
+
|
24
|
+
@scrubbed_object_ids = {}
|
25
|
+
|
26
|
+
config = options[:config]
|
27
|
+
extra_fields = options[:extra_fields]
|
28
|
+
whitelist = options[:whitelist] || []
|
29
|
+
|
30
|
+
scrub(params, build_scrub_options(config, extra_fields, whitelist))
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def build_scrub_options(config, extra_fields, whitelist)
|
36
|
+
ary_config = Array(config)
|
37
|
+
|
38
|
+
{
|
39
|
+
fields_regex: build_fields_regex(ary_config, extra_fields),
|
40
|
+
scrub_all: ary_config.include?(SCRUB_ALL),
|
41
|
+
whitelist: build_whitelist_regex(whitelist)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_fields_regex(config, extra_fields)
|
46
|
+
fields = config.find_all { |f| f.is_a?(String) || f.is_a?(Symbol) }
|
47
|
+
fields += Array(extra_fields)
|
48
|
+
|
49
|
+
return unless fields.any?
|
50
|
+
|
51
|
+
Regexp.new(fields.map { |val| Regexp.escape(val.to_s).to_s }.join('|'), true)
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_whitelist_regex(whitelist)
|
55
|
+
fields = whitelist.find_all { |f| f.is_a?(String) || f.is_a?(Symbol) }
|
56
|
+
return unless fields.any?
|
57
|
+
|
58
|
+
Regexp.new(fields.map { |val| /\A#{Regexp.escape(val.to_s)}\z/ }.join('|'))
|
59
|
+
end
|
60
|
+
|
61
|
+
def scrub(params, options)
|
62
|
+
return params if @scrubbed_object_ids[params.object_id]
|
63
|
+
|
64
|
+
@scrubbed_object_ids[params.object_id] = true
|
65
|
+
|
66
|
+
fields_regex = options[:fields_regex]
|
67
|
+
scrub_all = options[:scrub_all]
|
68
|
+
whitelist_regex = options[:whitelist]
|
69
|
+
|
70
|
+
return scrub_array(params, options) if params.is_a?(Array)
|
71
|
+
|
72
|
+
params.to_hash.each_with_object({}) do |(key, value), result|
|
73
|
+
encoded_key = Bugno::Encoding.encode(key).to_s
|
74
|
+
result[key] = if (fields_regex === encoded_key) && !(whitelist_regex === encoded_key)
|
75
|
+
scrub_value
|
76
|
+
elsif value.is_a?(Hash)
|
77
|
+
scrub(value, options)
|
78
|
+
elsif scrub_all && !(whitelist_regex === encoded_key)
|
79
|
+
scrub_value
|
80
|
+
elsif value.is_a?(Array)
|
81
|
+
scrub_array(value, options)
|
82
|
+
elsif skip_value?(value)
|
83
|
+
"Skipped value of class '#{value.class.name}'"
|
84
|
+
else
|
85
|
+
bugno_filtered_param_value(value)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def scrub_array(array, options)
|
91
|
+
array.map do |value|
|
92
|
+
value.is_a?(Hash) ? scrub(value, options) : bugno_filtered_param_value(value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def scrub_value
|
97
|
+
'[FILTERED]'
|
98
|
+
end
|
99
|
+
|
100
|
+
def bugno_filtered_param_value(value)
|
101
|
+
if ATTACHMENT_CLASSES.include?(value.class.name)
|
102
|
+
begin
|
103
|
+
attachment_value(value)
|
104
|
+
rescue StandardError
|
105
|
+
'Uploaded file'
|
106
|
+
end
|
107
|
+
else
|
108
|
+
value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def skip_value?(value)
|
113
|
+
SKIPPED_CLASSES.any? { |klass| value.is_a?(klass) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|