errordeck 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +58 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/errordeck-ruby.gemspec +37 -0
- data/lib/errordeck/configuration.rb +26 -0
- data/lib/errordeck/errordeck/backtrace.rb +40 -0
- data/lib/errordeck/errordeck/context.rb +26 -0
- data/lib/errordeck/errordeck/issue/event.rb +61 -0
- data/lib/errordeck/errordeck/issue/exception.rb +39 -0
- data/lib/errordeck/errordeck/issue/request.rb +60 -0
- data/lib/errordeck/errordeck/issue/stacktrace.rb +64 -0
- data/lib/errordeck/middleware/rack.rb +28 -0
- data/lib/errordeck/middleware/rails.rb +51 -0
- data/lib/errordeck/plugin_require.rb +14 -0
- data/lib/errordeck/request_handler.rb +124 -0
- data/lib/errordeck/scrubber/base.rb +9 -0
- data/lib/errordeck/scrubber/cookie.rb +27 -0
- data/lib/errordeck/scrubber/header.rb +31 -0
- data/lib/errordeck/scrubber/query_param.rb +33 -0
- data/lib/errordeck/scrubber/scrubber.rb +8 -0
- data/lib/errordeck/scrubber/url.rb +19 -0
- data/lib/errordeck/version.rb +5 -0
- data/lib/errordeck/wrapper.rb +164 -0
- data/lib/errordeck.rb +67 -0
- metadata +79 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Errordeck
|
|
6
|
+
class Stacktrace
|
|
7
|
+
attr_accessor :abs_path, :function, :in_app, :filename, :lineno, :module, :vars, :context_line, :pre_context,
|
|
8
|
+
:post_context
|
|
9
|
+
|
|
10
|
+
def initialize(project_root, line)
|
|
11
|
+
@abs_path = line.file
|
|
12
|
+
@function = line.method
|
|
13
|
+
@in_app = line.file.start_with?(project_root)
|
|
14
|
+
@filename = File.basename(line.file)
|
|
15
|
+
@lineno = line.line
|
|
16
|
+
@module = line.module_name
|
|
17
|
+
@vars = {}
|
|
18
|
+
set_contexts(line.file, line.line)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def as_json(*_options)
|
|
22
|
+
{
|
|
23
|
+
abs_path: @abs_path,
|
|
24
|
+
function: @function,
|
|
25
|
+
in_app: @in_app,
|
|
26
|
+
filename: @filename,
|
|
27
|
+
lineno: @lineno,
|
|
28
|
+
module: @module,
|
|
29
|
+
vars: @vars,
|
|
30
|
+
context_line: @context_line,
|
|
31
|
+
pre_context: @pre_context,
|
|
32
|
+
post_context: @post_context
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_json(*options)
|
|
37
|
+
JSON.generate(as_json, *options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.parse_from_backtrace(backtrace, project_root = nil)
|
|
41
|
+
project_root ||= File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))
|
|
42
|
+
error_backtrace = Errordeck::Backtrace.parse(backtrace)
|
|
43
|
+
return nil if error_backtrace.nil?
|
|
44
|
+
|
|
45
|
+
error_backtrace.lines.map { |line| new(project_root, line) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def set_contexts(file, line_number)
|
|
51
|
+
return unless File.exist?(file)
|
|
52
|
+
|
|
53
|
+
source = File.readlines(file)
|
|
54
|
+
return if source.empty?
|
|
55
|
+
|
|
56
|
+
range_start = [line_number - 5, 0].max
|
|
57
|
+
range_end = [line_number + 3, source.length - 1].min
|
|
58
|
+
|
|
59
|
+
@pre_context = source[range_start...line_number - 1]
|
|
60
|
+
@context_line = source[line_number - 1]
|
|
61
|
+
@post_context = source[line_number...range_end + 1]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rack middleware to capture request context and exception to send to errordeck
|
|
4
|
+
# Compare this snippet from lib/errordeck/middleware/rack.rb:
|
|
5
|
+
|
|
6
|
+
module Errordeck
|
|
7
|
+
module Middleware
|
|
8
|
+
module Rack
|
|
9
|
+
def self.new(app)
|
|
10
|
+
->(env) { call(env, app) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.call(env, app)
|
|
14
|
+
Errordeck.wrap do |b|
|
|
15
|
+
b.set_request(env)
|
|
16
|
+
b.set_transaction(env["PATH_INFO"])
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
app.call(env)
|
|
20
|
+
rescue Exception => e
|
|
21
|
+
b.capture(e)
|
|
22
|
+
raise e
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
module Middleware
|
|
5
|
+
module Rails
|
|
6
|
+
class ErrordeckMiddleware
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
dup.call!(env)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call!(env)
|
|
16
|
+
response = @app.call(env)
|
|
17
|
+
rescue Exception => e
|
|
18
|
+
notify_exception(env, e)
|
|
19
|
+
raise e
|
|
20
|
+
else
|
|
21
|
+
exception = collect_exception(env)
|
|
22
|
+
notify_exception(env, exception) if exception
|
|
23
|
+
response
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def notify_exception(env, exception)
|
|
29
|
+
return unless exception
|
|
30
|
+
|
|
31
|
+
Errordeck.wrap do |b|
|
|
32
|
+
b.set_action_context(env)
|
|
33
|
+
b.capture(exception)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def collect_exception(env)
|
|
38
|
+
return nil unless env
|
|
39
|
+
|
|
40
|
+
env["action_dispatch.exception"] || env["sinatra.error"] || env["rack.exception"]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Railtie < Rails::Railtie
|
|
47
|
+
initializer "errordeck.middleware.rails" do |app|
|
|
48
|
+
app.config.middleware.insert 0, Errordeck::Middleware::Rails::ErrordeckMiddleware
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# check if rack is defined and require middleware
|
|
4
|
+
if defined?(Rack)
|
|
5
|
+
require_relative "middleware/rack"
|
|
6
|
+
if defined?(Rack::Builder)
|
|
7
|
+
# Rack 2.0
|
|
8
|
+
Rack::Builder.include Errordeck::Middleware::Rack
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
# check if rails is defined and require middleware
|
|
12
|
+
if defined?(Rails) && Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("3.2") && defined?(Rails::Railtie)
|
|
13
|
+
require_relative "middleware/rails"
|
|
14
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Errordeck
|
|
6
|
+
class RequestHandler
|
|
7
|
+
attr_accessor :request
|
|
8
|
+
|
|
9
|
+
def initialize(request)
|
|
10
|
+
@request = request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.set_request_from_env(rack_env)
|
|
14
|
+
if defined?(Sinatra)
|
|
15
|
+
Sinatra::Request.new(rack_env)
|
|
16
|
+
elsif defined?(Rack)
|
|
17
|
+
Rack::Request.new(rack_env)
|
|
18
|
+
else
|
|
19
|
+
request_hash(rack_env)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.request_hash(rack_env)
|
|
24
|
+
request = {}
|
|
25
|
+
rack_env = rack_env.dup.transform_keys { |k| k.to_s.upcase }
|
|
26
|
+
|
|
27
|
+
if rack_env["REQUEST_URI"] || rack_env["URL"]
|
|
28
|
+
url = URI.parse(rack_env["REQUEST_URI"] || rack_env["URL"])
|
|
29
|
+
url.scheme ||= rack_env["RACK.URL_SCHEME"] || "https"
|
|
30
|
+
url.host ||= rack_env["HTTP_HOST"] || "localhost"
|
|
31
|
+
else
|
|
32
|
+
scheme = rack_env["RACK.URL_SCHEME"] || "https"
|
|
33
|
+
host = rack_env["HTTP_HOST"] || "localhost"
|
|
34
|
+
path = rack_env["PATH_INFO"] || "/"
|
|
35
|
+
query_string = rack_env["QUERY_STRING"] || ""
|
|
36
|
+
url = URI.parse("#{scheme}://#{host}#{path}")
|
|
37
|
+
url.query = query_string unless query_string.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
request[:url] = url.to_s
|
|
41
|
+
request[:query_string] = url.query || ""
|
|
42
|
+
request[:params] = rack_env["PARAMS"] || parse_query_string(request[:query_string])
|
|
43
|
+
request[:method] = rack_env["REQUEST_METHOD"] || "GET"
|
|
44
|
+
request[:cookies] =
|
|
45
|
+
rack_env["HTTP_COOKIE"] || rack_env["COOKIES"] ? parse_query_string(rack_env["HTTP_COOKIE"] || rack_env["COOKIES"]) : {}
|
|
46
|
+
request[:headers] = (rack_env["HEADERS"] || rack_env).select { |k, _| k.start_with?("HTTP_") }
|
|
47
|
+
request
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.parse_from_rack_env(rack_env)
|
|
51
|
+
request = set_request_from_env(rack_env.dup)
|
|
52
|
+
new(request)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def method
|
|
56
|
+
if @request.respond_to?(:request_method)
|
|
57
|
+
@request.request_method
|
|
58
|
+
else
|
|
59
|
+
@request[:method]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def url
|
|
64
|
+
if @request.respond_to?(:url)
|
|
65
|
+
@request.url
|
|
66
|
+
else
|
|
67
|
+
@request[:url]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def params
|
|
72
|
+
if @request.respond_to?(:params)
|
|
73
|
+
(request.env["action_dispatch.request.parameters"] || request.params).to_hash || {}
|
|
74
|
+
else
|
|
75
|
+
@request[:params] || {}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def controller
|
|
80
|
+
params["controller"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def action
|
|
84
|
+
params["action"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def query_string
|
|
88
|
+
@request[:query_string] || @request.query_string
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def cookies
|
|
92
|
+
@request[:cookies] || @request.cookies || {}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def headers
|
|
96
|
+
@request[:headers] || @request.env || {}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_hash
|
|
100
|
+
{
|
|
101
|
+
method: method,
|
|
102
|
+
url: url,
|
|
103
|
+
query_string: query_string,
|
|
104
|
+
cookies: cookies,
|
|
105
|
+
headers: headers,
|
|
106
|
+
params: params
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.parse_query_string(query_string)
|
|
111
|
+
return query_string if query_string.is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
query_string.split("&").each_with_object({}) do |pair, hash|
|
|
114
|
+
key, value = pair.split("=").map { |part| CGI.unescape(part) }
|
|
115
|
+
if hash.key?(key)
|
|
116
|
+
hash[key] = Array(hash[key])
|
|
117
|
+
hash[key] << value
|
|
118
|
+
else
|
|
119
|
+
hash[key] = value
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
module Scrubber
|
|
5
|
+
SENSITIVE_PARAMS = %w[password password_confirmation email secret token session].freeze
|
|
6
|
+
SENSITIVE_HEADERS = %w[Authorization Cookie Set-Cookie].freeze
|
|
7
|
+
SENSITIVE_VALUE = "[FILTERED]"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
module Scrubber
|
|
5
|
+
class Cookie
|
|
6
|
+
# scrub a cookie
|
|
7
|
+
def initialize(cookie)
|
|
8
|
+
@cookie = cookie
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# scrub a cookie
|
|
12
|
+
def scrub
|
|
13
|
+
scrub_cookie(@cookie)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def scrub_cookie(cookie)
|
|
19
|
+
return nil if cookie.nil?
|
|
20
|
+
|
|
21
|
+
cookie.each do |key, _value|
|
|
22
|
+
cookie[key] = Errordeck::Scrubber::SENSITIVE_VALUE if Errordeck::Scrubber::SENSITIVE_PARAMS.include?(key)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
module Scrubber
|
|
5
|
+
class Header
|
|
6
|
+
# scrub an HTTP header
|
|
7
|
+
def initialize(header)
|
|
8
|
+
@header = header
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# scrub an HTTP header
|
|
12
|
+
def scrub
|
|
13
|
+
scrub_header(@header)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def scrub_header(header)
|
|
19
|
+
return nil if header.nil?
|
|
20
|
+
|
|
21
|
+
header.to_h do |key, value|
|
|
22
|
+
if Errordeck::Scrubber::SENSITIVE_HEADERS.include?(key)
|
|
23
|
+
[key, Errordeck::Scrubber::SENSITIVE_VALUE]
|
|
24
|
+
else
|
|
25
|
+
[key, value]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
module Scrubber
|
|
5
|
+
class QueryParam
|
|
6
|
+
# scrub a query parameter
|
|
7
|
+
def initialize(query, filter = nil)
|
|
8
|
+
@query = query
|
|
9
|
+
@filter = filter || Errordeck::Scrubber::SENSITIVE_PARAMS
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# scrub a query parameter
|
|
13
|
+
def scrub
|
|
14
|
+
scrub_query(@query)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def scrub_query(query)
|
|
20
|
+
return nil if query.nil?
|
|
21
|
+
|
|
22
|
+
query.split("&").map do |param|
|
|
23
|
+
key, value = param.split("=")
|
|
24
|
+
if @filter.include?(key)
|
|
25
|
+
"#{key}=#{Errordeck::Scrubber::SENSITIVE_VALUE}"
|
|
26
|
+
else
|
|
27
|
+
param
|
|
28
|
+
end
|
|
29
|
+
end.join("&")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
module Scrubber
|
|
5
|
+
class Url
|
|
6
|
+
def initialize(url, filter = nil)
|
|
7
|
+
@url = url
|
|
8
|
+
@filter = filter || Errordeck::Scrubber::SENSITIVE_PARAMS
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# remove sensitive data from url
|
|
12
|
+
def scrub
|
|
13
|
+
uri = URI.parse(@url)
|
|
14
|
+
uri.query = Errordeck::Scrubber::QueryParam.new(uri.query, @filter).scrub
|
|
15
|
+
uri.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errordeck
|
|
4
|
+
class Wrapper
|
|
5
|
+
attr_accessor :context
|
|
6
|
+
attr_reader :error_event, :transaction, :request, :user, :tags, :modules
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@error_event = nil
|
|
10
|
+
@transaction = nil
|
|
11
|
+
@request = nil
|
|
12
|
+
@user = nil
|
|
13
|
+
@tags = nil
|
|
14
|
+
@message = false
|
|
15
|
+
@already_sent = false
|
|
16
|
+
if Gem::Specification.respond_to?(:map)
|
|
17
|
+
@modules = Gem::Specification.to_h do |spec|
|
|
18
|
+
[spec.name, spec.version.to_s]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
@context = Context.context
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def send_event
|
|
25
|
+
return if @error_event.nil? && @message == true
|
|
26
|
+
return if @already_sent
|
|
27
|
+
|
|
28
|
+
uri = URI.parse("https://app.errordeck.com/api/#{config.project_id}/store")
|
|
29
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
+
http.use_ssl = true
|
|
31
|
+
request = Net::HTTP::Post.new(uri.request_uri, "Authorization" => "Bearer #{config.token}")
|
|
32
|
+
request["Content-Type"] = "application/json"
|
|
33
|
+
event_json = @error_event&.to_json
|
|
34
|
+
response = http.request(request, event_json)
|
|
35
|
+
@already_sent = true
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
raise Error, "Error sending issue to Errordeck: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def capture(exception, user = nil, tags = nil)
|
|
41
|
+
@message = false
|
|
42
|
+
@error_event = generate_from_exception(exception)
|
|
43
|
+
@error_event.user = user || @user
|
|
44
|
+
@error_event.tags = tags || @tags
|
|
45
|
+
@error_event.request = @request
|
|
46
|
+
@error_event
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def message(level, message, extra = nil)
|
|
50
|
+
@message = true
|
|
51
|
+
@error_event = generate_boxing_event(level, message, extra)
|
|
52
|
+
@error_event.user = @user
|
|
53
|
+
@error_event.tags = @tags
|
|
54
|
+
@error_event.request = @request
|
|
55
|
+
@error_event
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_action_context(env)
|
|
59
|
+
request_handler = RequestHandler.parse_from_rack_env(env)
|
|
60
|
+
@request = Request.parse_from_request_handler(request_handler)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def set_transaction(transaction = nil)
|
|
64
|
+
@transaction = transaction
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def set_request(env = nil)
|
|
68
|
+
@request = Request.parse_from_rack_env(env)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def user_context=(user)
|
|
72
|
+
@user = user
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def tags_context=(tags)
|
|
76
|
+
@tags = tags
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def generate_from_exception(exception)
|
|
82
|
+
exceptions = Errordeck::Exception.parse_from_exception(exception, project_root)
|
|
83
|
+
Event.new(
|
|
84
|
+
level: config.level || exception_severity(exception),
|
|
85
|
+
transaction: transaction,
|
|
86
|
+
server_name: server_name_env,
|
|
87
|
+
release: config.release,
|
|
88
|
+
dist: config.dist,
|
|
89
|
+
environment: config.environment || find_environment,
|
|
90
|
+
message: exception.message,
|
|
91
|
+
modules: modules,
|
|
92
|
+
exceptions: exceptions,
|
|
93
|
+
contexts: context
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def project_root
|
|
98
|
+
return Rails.root.to_s if defined?(Rails)
|
|
99
|
+
return Sinatra::Application.root.to_s if defined?(Sinatra)
|
|
100
|
+
return Rack::Directory.new("").root.to_s if defined?(Rack)
|
|
101
|
+
return Bundler.root.to_s if defined?(Bundler)
|
|
102
|
+
|
|
103
|
+
File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def generate_boxing_event(level, message, extra = nil)
|
|
107
|
+
Event.new(
|
|
108
|
+
level: level,
|
|
109
|
+
transaction: transaction,
|
|
110
|
+
server_name: server_name_env,
|
|
111
|
+
release: config.release,
|
|
112
|
+
dist: config.dist,
|
|
113
|
+
environment: config.environment,
|
|
114
|
+
message: message,
|
|
115
|
+
modules: modules,
|
|
116
|
+
extra: extra,
|
|
117
|
+
contexts: context
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def find_environment
|
|
122
|
+
# check if rails env is set or sinatra env is set
|
|
123
|
+
if defined?(Rails)
|
|
124
|
+
Rails.env
|
|
125
|
+
elsif defined?(Sinatra)
|
|
126
|
+
Sinatra::Base.environment
|
|
127
|
+
else
|
|
128
|
+
"production"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def exception_severity(exception)
|
|
133
|
+
case exception.class.to_s
|
|
134
|
+
when "SystemExit", "SignalException", "Interrupt", "NoMemoryError", "SecurityError"
|
|
135
|
+
:critical
|
|
136
|
+
when "RuntimeError", "TypeError", "LoadError", "NameError", "ArgumentError", "IndexError", "KeyError", "RangeError",
|
|
137
|
+
"NoMethodError", "FrozenError", "SocketError", "EncodingError", "Encoding::InvalidByteSequenceError",
|
|
138
|
+
"Encoding::UndefinedConversionError", "ZeroDivisionError", "SystemCallError", "Errno::EACCES",
|
|
139
|
+
"Errno::EADDRINUSE", "Errno::ECONNREFUSED", "Errno::ECONNRESET", "Errno::EEXIST", "Errno::EHOSTUNREACH",
|
|
140
|
+
"Errno::EINTR", "Errno::EINVAL", "Errno::EISDIR", "Errno::ENETDOWN", "Errno::ENETUNREACH", "Errno::ENOENT",
|
|
141
|
+
"Errno::ENOMEM", "Errno::ENOSPC", "Errno::ENOTCONN", "Errno::ENOTDIR", "Errno::EPIPE", "Errno::ERANGE",
|
|
142
|
+
"Errno::ETIMEDOUT", "Errno::ENOTEMPTY"
|
|
143
|
+
:error
|
|
144
|
+
when "Warning", "SecurityWarning", "DeprecatedError", "DeprecationWarning", "RuntimeWarning", "SyntaxError",
|
|
145
|
+
"NameError::UndefinedVariable", "LoadError::MissingFile", "NoMethodError::MissingMethod",
|
|
146
|
+
"ArgumentError::InvalidValue", "ArgumentError::MissingRequiredParameter", "IndexError::OutOfRange",
|
|
147
|
+
"ActiveRecord::RecordNotFound", "Mongoid::Errors::DocumentNotFound", "Redis::CommandError", "Net::ReadTimeout",
|
|
148
|
+
"Faraday::TimeoutError"
|
|
149
|
+
:warning
|
|
150
|
+
else
|
|
151
|
+
:error
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def config
|
|
156
|
+
Errordeck.configuration
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def server_name_env
|
|
160
|
+
# set server_name context
|
|
161
|
+
config.server_name || ENV.fetch("SERVER_NAME", nil)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/errordeck.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "errordeck/version"
|
|
8
|
+
require_relative "errordeck/configuration"
|
|
9
|
+
Dir["#{File.dirname(__FILE__)}/errordeck/errordeck/**/*.rb"].sort.each { |file| require file }
|
|
10
|
+
require_relative "errordeck/request_handler"
|
|
11
|
+
require_relative "errordeck/wrapper"
|
|
12
|
+
require_relative "errordeck/scrubber/scrubber"
|
|
13
|
+
require_relative "errordeck/plugin_require"
|
|
14
|
+
|
|
15
|
+
module Errordeck
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def info(message:, extra: nil)
|
|
20
|
+
generate_event(level: "info", message: message, extra: extra)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def warning(message:, extra: nil)
|
|
24
|
+
generate_event(level: "warning", message: message, extra: extra)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def error(message:, extra: nil)
|
|
28
|
+
generate_event(level: "error", message: message, extra: extra)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fatal(message:, extra: nil)
|
|
32
|
+
generate_event(level: "fatal", message: message, extra: extra)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def generate_event(level:, message:, extra: nil, capture: true)
|
|
36
|
+
wrap(capture) do |b|
|
|
37
|
+
b.message(level, message, extra)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def message(level:, message:, extra: nil, capture: true)
|
|
42
|
+
wrap(capture) do |b|
|
|
43
|
+
b.message(level, message, extra)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def capture(exception:, user: nil, tags: nil, capture: true)
|
|
48
|
+
wrap(capture) do |b|
|
|
49
|
+
b.user_context = user if user
|
|
50
|
+
b.tags_context = tags if tags
|
|
51
|
+
b.capture(exception)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def wrap(capture = true)
|
|
56
|
+
wrapper = Wrapper.new
|
|
57
|
+
begin
|
|
58
|
+
yield(wrapper)
|
|
59
|
+
wrapper.send_event if capture
|
|
60
|
+
rescue Exception => e
|
|
61
|
+
wrapper.capture(e)
|
|
62
|
+
wrapper.send_event if capture
|
|
63
|
+
raise e
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|