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.
@@ -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
@@ -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