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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ class BugnoGenerator < Rails::Generators::Base
6
+ source_root File.expand_path(__dir__)
7
+
8
+ argument :api_key, required: false
9
+
10
+ def generate_layout
11
+ template 'bugno_initializer.rb.erb', 'config/initializers/bugno.rb'
12
+ end
13
+ end
@@ -0,0 +1,36 @@
1
+ Bugno.configure do |config|
2
+ <% if api_key -%>
3
+ # Specify api key to send exception to Bugno
4
+ config.api_key = '<%= api_key %>'
5
+ <% else -%>
6
+ # Specify api key to send exception to Bugno
7
+ config.api_key = 'MISSING_API_KEY'
8
+ <% end -%>
9
+
10
+ # Send in background with threading:
11
+ config.send_in_background = true
12
+
13
+ # Skip rails related exceptions:
14
+ config.exclude_rails_exceptions = false
15
+
16
+ # Specify which rails exception to skip:
17
+ config.excluded_exceptions = [
18
+ 'AbstractController::ActionNotFound',
19
+ 'ActionController::InvalidAuthenticityToken',
20
+ 'ActionController::RoutingError',
21
+ 'ActionController::UnknownAction',
22
+ 'ActiveRecord::RecordNotFound',
23
+ 'ActiveJob::DeserializationError'
24
+ ]
25
+
26
+ # Specify or add usage environments:
27
+ config.usage_environments = %w[production]
28
+ # config.usage_environments << 'development'
29
+
30
+ # Specify current user method:
31
+ config.current_user_method = 'current_user'
32
+
33
+ # Add scrub fields and headers:
34
+ # config.scrub_fields << 'password'
35
+ # config.scrub_headers << 'access_token'
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugno/event'
4
+ require 'bugno/reporter'
5
+
6
+ module Bugno
7
+ class Handler
8
+ attr_reader :event, :reporter, :exception, :env, :job
9
+
10
+ def initialize(options = {})
11
+ @exception = options[:exception]
12
+ @event = Event.new(exception: options[:exception], env: options[:env], job: options[:job])
13
+ @reporter = Reporter.new
14
+ end
15
+
16
+ def self.call(options = {})
17
+ self.new(options).handle_exception
18
+ end
19
+
20
+ def handle_exception
21
+ return if excluded_exception? || !usage_environment?
22
+
23
+ @reporter.request.body = @event.data.to_json
24
+ Bugno.configuration.send_in_background ? Thread.new { @reporter.send } : @reporter.send
25
+ end
26
+
27
+ private
28
+
29
+ def excluded_exception?
30
+ Bugno.configuration.exclude_rails_exceptions && \
31
+ Bugno.configuration.excluded_exceptions.include?(@exception.class.inspect)
32
+ end
33
+
34
+ def usage_environment?
35
+ Bugno.configuration.usage_environments.include?(Bugno.configuration.environment)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugno
4
+ class << self
5
+ def logger
6
+ @logger ||= Logger.new(STDOUT)
7
+ end
8
+
9
+ %w[debug info warn error].each do |level|
10
+ define_method(:"log_#{level}") do |message|
11
+ message = "[Bugno] #{message}"
12
+ logger.send(level, message)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugno/handler'
4
+
5
+ module Bugno
6
+ module Middleware
7
+ module Rails
8
+ module ActiveJobExtensions
9
+ ADAPTERS = %w[ActiveJob::QueueAdapters::SidekiqAdapter ActiveJob::QueueAdapters::DelayedJobAdapter].freeze
10
+
11
+ def self.included(base)
12
+ base.class_eval do
13
+ around_perform { |job, block| capture_and_reraise(job, block) }
14
+ end
15
+ end
16
+
17
+ def capture_and_reraise(job, block)
18
+ block.call
19
+ rescue Error
20
+ raise
21
+ rescue Exception => e
22
+ Handler.call(exception: e, job: job_data(job)) if Bugno.configured?
23
+ raise e
24
+ end
25
+
26
+ def job_data(job)
27
+ data = {
28
+ active_job: job.class.name,
29
+ arguments: job.arguments,
30
+ scheduled_at: job.scheduled_at,
31
+ job_id: job.job_id,
32
+ locale: job.locale
33
+ }
34
+ data[:provider_job_id] = job.provider_job_id if job.respond_to?(:provider_job_id)
35
+ data
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ class ActiveJob::Base
43
+ include Bugno::Middleware::Rails::ActiveJobExtensions
44
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugno/handler'
4
+
5
+ module Bugno
6
+ module Middleware
7
+ module Rails
8
+ class BugnoMiddleware
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ @app.call(env)
15
+ rescue Error
16
+ raise
17
+ rescue Exception => e
18
+ Handler.call(exception: e, env: env) if Bugno.configured?
19
+ raise e
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bugno/handler'
4
+
5
+ module Bugno
6
+ module Middleware
7
+ module ShowExceptions
8
+ def render_exception_with_bugno(env, exception)
9
+ if exception.is_a?(ActionController::RoutingError)
10
+ Handler.call(exception: exception, env: extract_scope_from(env)) if Bugno.configured?
11
+ end
12
+
13
+ render_exception_without_bugno(env, exception)
14
+ end
15
+
16
+ def call_with_bugno(env)
17
+ call_without_bugno(env)
18
+ rescue ActionController::RoutingError => e
19
+ raise e
20
+ end
21
+
22
+ def extract_scope_from(env)
23
+ env.env
24
+ end
25
+
26
+ def self.included(base)
27
+ base.send(:alias_method, :call_without_bugno, :call)
28
+ base.send(:alias_method, :call, :call_with_bugno)
29
+
30
+ base.send(:alias_method, :render_exception_without_bugno, :render_exception)
31
+ base.send(:alias_method, :render_exception, :render_exception_with_bugno)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Bugno
6
+ class Railtie < ::Rails::Railtie
7
+ initializer 'bugno.middleware.rails' do |app|
8
+ require 'bugno/middleware/rails/bugno'
9
+ require 'bugno/middleware/rails/show_exceptions'
10
+ require 'bugno/middleware/rails/active_job_extensions'
11
+ app.config.middleware.insert_after ActionDispatch::DebugExceptions,
12
+ Bugno::Middleware::Rails::BugnoMiddleware
13
+ ActionDispatch::DebugExceptions.send(:include, Bugno::Middleware::ShowExceptions)
14
+ end
15
+
16
+ initializer 'bugno.configuration' do
17
+ config.after_initialize do
18
+ Bugno.configure do |config|
19
+ config.environment = ENV['RACK_ENV'] || ::Rails.env
20
+ config.framework = 'rails'
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module Bugno
8
+ class Reporter
9
+ attr_reader :uri, :http
10
+ attr_accessor :request
11
+
12
+ def initialize
13
+ @uri = URI.parse("#{Bugno.configuration.api_url}/api/v1/projects/#{Bugno.configuration.api_key}/events")
14
+ @http = Net::HTTP.new(uri.host, uri.port)
15
+ @request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type': 'application/json')
16
+ end
17
+
18
+ def send
19
+ http.use_ssl = true if uri.scheme == 'https'
20
+
21
+ response = http.request(request)
22
+ Bugno.log_info(api_response(response))
23
+ rescue StandardError => e
24
+ Bugno.log_error(e.message)
25
+ end
26
+
27
+ def api_response(response)
28
+ body = JSON.parse(response.body.presence || '{}')
29
+ message = body['message'] || body['error'] || response.message
30
+ "#{message.capitalize} | Code: #{response.code}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'bugno/filter/params'
5
+ require 'bugno/encoding/encoder'
6
+
7
+ module Bugno
8
+ module RequestDataExtractor
9
+ ALLOWED_HEADERS_REGEX = /^HTTP_|^CONTENT_TYPE$|^CONTENT_LENGTH$/.freeze
10
+ ALLOWED_BODY_PARSEABLE_METHODS = %w[POST PUT PATCH DELETE].freeze
11
+
12
+ def extract_request_data_from_rack(env)
13
+ rack_req = ::Rack::Request.new(env)
14
+ sensitive_params = sensitive_params_list(env)
15
+
16
+ post_params = scrub_params(post_params(rack_req), sensitive_params)
17
+ get_params = scrub_params(get_params(rack_req), sensitive_params)
18
+ route_params = scrub_params(route_params(env), sensitive_params)
19
+ session = scrub_params(request_session(env), sensitive_params)
20
+ cookies = scrub_params(request_cookies(rack_req), sensitive_params)
21
+ person_data = scrub_params(person_data(env), sensitive_params)
22
+
23
+ data = {
24
+ url: request_url(env),
25
+ ip_address: ip_address(env),
26
+ headers: headers(env),
27
+ http_method: request_method(env),
28
+ params: get_params,
29
+ route_params: route_params,
30
+ session: session,
31
+ cookies: cookies,
32
+ person_data: person_data
33
+ }
34
+ data[:params] = post_params if data[:params].empty?
35
+
36
+ data
37
+ end
38
+
39
+ def scrub_params(params, sensitive_params)
40
+ options = {
41
+ params: params,
42
+ config: Bugno.configuration.scrub_fields,
43
+ extra_fields: sensitive_params,
44
+ whitelist: Bugno.configuration.scrub_whitelist
45
+ }
46
+ Bugno::Filter::Params.call(options)
47
+ end
48
+
49
+ def person_data(env)
50
+ current_user = Bugno.configuration.current_user_method
51
+ controller = env['action_controller.instance']
52
+ person_data = begin
53
+ controller.send(current_user).attributes
54
+ rescue StandardError
55
+ {}
56
+ end
57
+ person_data
58
+ end
59
+
60
+ def sensitive_params_list(env)
61
+ Array(env['action_dispatch.parameter_filter'])
62
+ end
63
+
64
+ def request_session(env)
65
+ session = env.fetch('rack.session', {})
66
+
67
+ session.to_hash
68
+ rescue StandardError
69
+ {}
70
+ end
71
+
72
+ def request_cookies(rack_req)
73
+ rack_req.cookies
74
+ rescue StandardError
75
+ {}
76
+ end
77
+
78
+ def headers(env)
79
+ env.keys.grep(ALLOWED_HEADERS_REGEX).map do |header|
80
+ name = header.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
81
+ if name == 'Cookie'
82
+ {}
83
+ elsif sensitive_headers_list.include?(name)
84
+ { name => Bugno::Filter::Params.scrub_value }
85
+ else
86
+ { name => env[header] }
87
+ end
88
+ end.inject(:merge)
89
+ end
90
+
91
+ def request_method(env)
92
+ env['REQUEST_METHOD'] || env[:method]
93
+ end
94
+
95
+ def request_url(env)
96
+ forwarded_proto = env['HTTP_X_FORWARDED_PROTO'] || env['rack.url_scheme'] || ''
97
+ scheme = forwarded_proto.split(',').first
98
+
99
+ host = env['HTTP_X_FORWARDED_HOST'] || env['HTTP_HOST'] || env['SERVER_NAME'] || ''
100
+ host = host.split(',').first.strip unless host.empty?
101
+
102
+ path = env['ORIGINAL_FULLPATH'] || env['REQUEST_URI']
103
+ unless path.nil? || path.empty?
104
+ path = '/' + path.to_s if path.to_s.slice(0, 1) != '/'
105
+ end
106
+
107
+ port = env['HTTP_X_FORWARDED_PORT']
108
+ if port && !(!scheme.nil? && scheme.casecmp('http').zero? && port.to_i == 80) && \
109
+ !(!scheme.nil? && scheme.casecmp('https').zero? && port.to_i == 443) && \
110
+ !(host.include? ':')
111
+ host = host + ':' + port
112
+ end
113
+
114
+ [scheme, '://', host, path].join
115
+ end
116
+
117
+ def ip_address(env)
118
+ ip_address_string = (env['action_dispatch.remote_ip'] || env['HTTP_X_REAL_IP'] || env['REMOTE_ADDR']).to_s
119
+ end
120
+
121
+ def get_params(rack_req)
122
+ rack_req.GET
123
+ rescue StandardError
124
+ {}
125
+ end
126
+
127
+ def post_params(rack_req)
128
+ rack_req.POST
129
+ rescue StandardError
130
+ {}
131
+ end
132
+
133
+ def route_params(env)
134
+ return {} unless defined?(Rails)
135
+
136
+ begin
137
+ environment = { method: request_method(env) }
138
+
139
+ ::Rails.application.routes.recognize_path(env['PATH_INFO'],
140
+ environment)
141
+ rescue StandardError
142
+ {}
143
+ end
144
+ end
145
+
146
+ def sensitive_headers_list
147
+ Bugno.configuration.scrub_headers || []
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugno
4
+ VERSION = '0.1.9'
5
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bugno-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.9
5
+ platform: ruby
6
+ authors:
7
+ - Dmitry Syvoglaz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-12-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ - grayeyed16@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - CHANGELOG.md
108
+ - CODE_OF_CONDUCT.md
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - bin/console
115
+ - bin/setup
116
+ - bugno.gemspec
117
+ - lib/bugno.rb
118
+ - lib/bugno/backtrace.rb
119
+ - lib/bugno/configuration.rb
120
+ - lib/bugno/encoding/encoder.rb
121
+ - lib/bugno/encoding/encoding.rb
122
+ - lib/bugno/encoding/legacy_encoder.rb
123
+ - lib/bugno/event.rb
124
+ - lib/bugno/filter/params.rb
125
+ - lib/bugno/generator/bugno_generator.rb
126
+ - lib/bugno/generator/bugno_initializer.rb.erb
127
+ - lib/bugno/handler.rb
128
+ - lib/bugno/logger.rb
129
+ - lib/bugno/middleware/rails/active_job_extensions.rb
130
+ - lib/bugno/middleware/rails/bugno.rb
131
+ - lib/bugno/middleware/rails/show_exceptions.rb
132
+ - lib/bugno/railtie.rb
133
+ - lib/bugno/reporter.rb
134
+ - lib/bugno/request_data_extractor.rb
135
+ - lib/bugno/version.rb
136
+ homepage: https://bugno.io/
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubygems_version: 3.0.3
156
+ signing_key:
157
+ specification_version: 4
158
+ summary: Track your rails exceptions
159
+ test_files: []