errorgap 0.8.0 → 0.10.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 +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +56 -1
- data/exe/errorgap +17 -1
- data/lib/errorgap/configuration.rb +49 -2
- data/lib/errorgap/delivery.rb +248 -0
- data/lib/errorgap/integrations/active_job.rb +58 -0
- data/lib/errorgap/integrations/delayed_job.rb +75 -0
- data/lib/errorgap/integrations/net_http.rb +50 -0
- data/lib/errorgap/integrations/rake.rb +44 -0
- data/lib/errorgap/integrations/resque.rb +45 -0
- data/lib/errorgap/integrations/sidekiq.rb +56 -0
- data/lib/errorgap/log_delivery.rb +3 -25
- data/lib/errorgap/notice.rb +10 -21
- data/lib/errorgap/notifier.rb +30 -21
- data/lib/errorgap/rack_middleware.rb +48 -5
- data/lib/errorgap/rails/railtie.rb +15 -0
- data/lib/errorgap/release_detector.rb +69 -0
- data/lib/errorgap/remote_settings.rb +99 -0
- data/lib/errorgap/sanitizer.rb +91 -0
- data/lib/errorgap/sign_ins.rb +2 -19
- data/lib/errorgap/transacter.rb +3 -24
- data/lib/errorgap/version.rb +1 -1
- data/lib/errorgap.rb +119 -6
- metadata +12 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errorgap
|
|
4
|
+
module Integrations
|
|
5
|
+
# Times outbound `Net::HTTP` requests made during a transaction and
|
|
6
|
+
# records each as an `http` span named by method and host. Most Ruby HTTP
|
|
7
|
+
# clients (Faraday's default adapter, HTTParty, RestClient) use Net::HTTP
|
|
8
|
+
# underneath. Requests errorgap makes itself are left out.
|
|
9
|
+
module NetHTTP
|
|
10
|
+
class << self
|
|
11
|
+
def install
|
|
12
|
+
return false unless defined?(::Net::HTTP)
|
|
13
|
+
return true if @installed
|
|
14
|
+
|
|
15
|
+
::Net::HTTP.prepend(Patch)
|
|
16
|
+
@installed = true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Patch
|
|
21
|
+
def request(req, body = nil, &block)
|
|
22
|
+
# `request` calls itself once when the connection is not started.
|
|
23
|
+
unless started? && Thread.current[SpanCollector::THREAD_KEY] && !Errorgap::Delivery.sending?
|
|
24
|
+
return super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
+
begin
|
|
29
|
+
super
|
|
30
|
+
ensure
|
|
31
|
+
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
|
32
|
+
Errorgap::Integrations::NetHTTP.record(req, address, elapsed_ms)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.record(request, host, elapsed_ms)
|
|
38
|
+
file, line, = SpanCollector.app_call_site
|
|
39
|
+
SpanCollector.store << Span.new(
|
|
40
|
+
kind: "http",
|
|
41
|
+
fn_name: "#{request.method} #{host}",
|
|
42
|
+
file: file, line: line,
|
|
43
|
+
duration_ms: elapsed_ms.round(3)
|
|
44
|
+
)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errorgap
|
|
4
|
+
module Integrations
|
|
5
|
+
# Rake: report an error that ends a task, with the task's name, then
|
|
6
|
+
# re-raise it. The report is flushed before Rake exits.
|
|
7
|
+
#
|
|
8
|
+
# require "errorgap"
|
|
9
|
+
# Errorgap::Integrations::Rake.install # in the Rakefile
|
|
10
|
+
#
|
|
11
|
+
# In Rails this is installed for you.
|
|
12
|
+
module Rake
|
|
13
|
+
class << self
|
|
14
|
+
def install
|
|
15
|
+
return false unless defined?(::Rake::Task)
|
|
16
|
+
return true if @installed
|
|
17
|
+
|
|
18
|
+
::Rake::Task.prepend(Patch)
|
|
19
|
+
@installed = true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Patch
|
|
24
|
+
def execute(args = nil)
|
|
25
|
+
super
|
|
26
|
+
rescue SystemExit, Interrupt
|
|
27
|
+
raise
|
|
28
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
29
|
+
begin
|
|
30
|
+
Errorgap.notify_once(
|
|
31
|
+
exception,
|
|
32
|
+
context: { component: "rake", action: name.to_s },
|
|
33
|
+
params: { "arguments" => args.respond_to?(:to_a) ? args.to_a : nil }
|
|
34
|
+
)
|
|
35
|
+
Errorgap.flush
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
raise
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errorgap
|
|
4
|
+
module Integrations
|
|
5
|
+
# Resque: report failed jobs with their class, queue and arguments.
|
|
6
|
+
# Resque's own failure backend (the Failed tab) keeps working alongside.
|
|
7
|
+
#
|
|
8
|
+
# Errorgap::Integrations::Resque.install # after Resque is loaded
|
|
9
|
+
module Resque
|
|
10
|
+
class << self
|
|
11
|
+
def install
|
|
12
|
+
return false unless defined?(::Resque::Failure::Base)
|
|
13
|
+
return true if @installed
|
|
14
|
+
|
|
15
|
+
require "resque/failure/multiple"
|
|
16
|
+
current = ::Resque::Failure.backend
|
|
17
|
+
backends = current == ::Resque::Failure::Multiple ? ::Resque::Failure::Multiple.classes : [current]
|
|
18
|
+
::Resque::Failure::Multiple.classes = (backends + [backend]).uniq
|
|
19
|
+
::Resque::Failure.backend = ::Resque::Failure::Multiple
|
|
20
|
+
@installed = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The failure backend, defined on first use so loading errorgap does
|
|
24
|
+
# not need Resque.
|
|
25
|
+
def backend
|
|
26
|
+
@backend ||= Class.new(::Resque::Failure::Base) do
|
|
27
|
+
def save
|
|
28
|
+
job_class = payload.is_a?(Hash) ? payload["class"].to_s : ""
|
|
29
|
+
Errorgap.notify_once(
|
|
30
|
+
exception,
|
|
31
|
+
context: { component: "resque", action: job_class },
|
|
32
|
+
environment: { queue: queue.to_s },
|
|
33
|
+
params: { "arguments" => payload.is_a?(Hash) ? payload["args"] : nil }
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.count(*)
|
|
38
|
+
0
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errorgap
|
|
4
|
+
module Integrations
|
|
5
|
+
# Sidekiq server middleware: every job runs as a `job` transaction and an
|
|
6
|
+
# error it raises is reported with the job's class, queue, id and
|
|
7
|
+
# arguments, then re-raised so Sidekiq's retries work as before.
|
|
8
|
+
#
|
|
9
|
+
# Installed automatically when Sidekiq is loaded before errorgap is
|
|
10
|
+
# configured; otherwise:
|
|
11
|
+
#
|
|
12
|
+
# Sidekiq.configure_server do |config|
|
|
13
|
+
# config.server_middleware { |chain| chain.add Errorgap::Integrations::Sidekiq }
|
|
14
|
+
# end
|
|
15
|
+
class Sidekiq
|
|
16
|
+
class << self
|
|
17
|
+
def install
|
|
18
|
+
return false unless defined?(::Sidekiq) && ::Sidekiq.respond_to?(:configure_server)
|
|
19
|
+
return true if @installed
|
|
20
|
+
|
|
21
|
+
::Sidekiq.configure_server do |config|
|
|
22
|
+
config.server_middleware { |chain| chain.add Errorgap::Integrations::Sidekiq }
|
|
23
|
+
end
|
|
24
|
+
@installed = true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(_worker, job, queue, &block)
|
|
29
|
+
# An ActiveJob run by Sidekiq is covered by the ActiveJob integration,
|
|
30
|
+
# which knows the real job class and its arguments.
|
|
31
|
+
return yield if job["wrapped"] && Errorgap::Integrations::ActiveJob.installed?
|
|
32
|
+
|
|
33
|
+
job_class = (job["wrapped"] || job["class"]).to_s
|
|
34
|
+
Errorgap.track_job(job_class, queue: queue.to_s) do
|
|
35
|
+
run(job, job_class, queue, &block)
|
|
36
|
+
end
|
|
37
|
+
ensure
|
|
38
|
+
Errorgap.clear_user
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def run(job, job_class, queue)
|
|
44
|
+
yield
|
|
45
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
46
|
+
Errorgap.notify_once(
|
|
47
|
+
exception,
|
|
48
|
+
context: { component: "sidekiq", action: job_class },
|
|
49
|
+
environment: { queue: queue.to_s, job_id: job["jid"], retry_count: job["retry_count"] }.compact,
|
|
50
|
+
params: { "arguments" => job["args"] }
|
|
51
|
+
)
|
|
52
|
+
raise
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "net/http"
|
|
5
|
-
require "uri"
|
|
6
3
|
require "time"
|
|
7
4
|
|
|
8
5
|
module Errorgap
|
|
@@ -24,6 +21,7 @@ module Errorgap
|
|
|
24
21
|
def log(message, level: "info", source: nil, environment: nil, occurred_at: nil, sync: false)
|
|
25
22
|
return unless @configuration.logs_enabled
|
|
26
23
|
return if @configuration.ignored_environment?
|
|
24
|
+
return unless Errorgap.remote_settings.enabled?(:logs)
|
|
27
25
|
|
|
28
26
|
normalized = self.class.normalize_level(level)
|
|
29
27
|
return if self.class.rank(normalized) < self.class.rank(self.class.normalize_level(@configuration.minimum_log_level))
|
|
@@ -39,27 +37,13 @@ module Errorgap
|
|
|
39
37
|
if sync || !@configuration.async
|
|
40
38
|
deliver(payload)
|
|
41
39
|
else
|
|
42
|
-
Errorgap.
|
|
40
|
+
Errorgap.delivery.enqueue("logs", payload)
|
|
43
41
|
nil
|
|
44
42
|
end
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def deliver(payload)
|
|
48
|
-
|
|
49
|
-
@configuration.endpoint.end_with?("/") ? @configuration.endpoint : "#{@configuration.endpoint}/",
|
|
50
|
-
"api/projects/#{@configuration.project_slug}/logs"
|
|
51
|
-
)
|
|
52
|
-
request = Net::HTTP::Post.new(uri)
|
|
53
|
-
request["Content-Type"] = "application/json"
|
|
54
|
-
request["User-Agent"] = "errorgap-ruby/#{Errorgap::VERSION}"
|
|
55
|
-
request["X-Errorgap-Project-Key"] = @configuration.api_key if present?(@configuration.api_key)
|
|
56
|
-
request.body = JSON.generate(payload)
|
|
57
|
-
|
|
58
|
-
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
59
|
-
http.request(request)
|
|
60
|
-
end
|
|
61
|
-
rescue StandardError => exception
|
|
62
|
-
@configuration.logger&.warn("[errorgap] log delivery error: #{exception.class}: #{exception.message}")
|
|
46
|
+
Errorgap.delivery.post("logs", payload)
|
|
63
47
|
end
|
|
64
48
|
|
|
65
49
|
def self.normalize_level(level)
|
|
@@ -71,11 +55,5 @@ module Errorgap
|
|
|
71
55
|
def self.rank(level)
|
|
72
56
|
LEVELS.index(level) || LEVELS.index("info")
|
|
73
57
|
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
def present?(value)
|
|
78
|
-
!value.nil? && !value.to_s.empty?
|
|
79
|
-
end
|
|
80
58
|
end
|
|
81
59
|
end
|
data/lib/errorgap/notice.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Errorgap
|
|
|
11
11
|
MAX_SOURCE_FRAMES = 25
|
|
12
12
|
MAX_SOURCE_LINE_LENGTH = 400
|
|
13
13
|
MAX_CAUSE_DEPTH = 10
|
|
14
|
+
MAX_MESSAGE_LENGTH = 8192
|
|
14
15
|
|
|
15
16
|
def self.from_exception(error, configuration:, context: {}, environment: {}, session: {}, params: {}, breadcrumbs: [])
|
|
16
17
|
new(
|
|
@@ -40,15 +41,15 @@ module Errorgap
|
|
|
40
41
|
received_at: Time.now.utc.iso8601,
|
|
41
42
|
errors: [
|
|
42
43
|
{
|
|
43
|
-
type: @error.class.name,
|
|
44
|
-
message: @error.message
|
|
44
|
+
type: @error.class.name.to_s,
|
|
45
|
+
message: Sanitizer.string(@error.message, MAX_MESSAGE_LENGTH),
|
|
45
46
|
backtrace: backtrace_frames
|
|
46
47
|
}
|
|
47
48
|
],
|
|
48
|
-
context: default_context.merge(
|
|
49
|
-
environment:
|
|
50
|
-
session:
|
|
51
|
-
params:
|
|
49
|
+
context: default_context.merge(sanitizer.clean(@context)),
|
|
50
|
+
environment: sanitizer.clean(@environment),
|
|
51
|
+
session: sanitizer.filter(@session),
|
|
52
|
+
params: sanitizer.filter(@params)
|
|
52
53
|
}
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -100,7 +101,7 @@ module Errorgap
|
|
|
100
101
|
# The causes beyond the root error, as {type, message} pairs.
|
|
101
102
|
def collect_causes
|
|
102
103
|
error_chain.drop(1).map do |link|
|
|
103
|
-
{ type: link.class.name, message: link.message
|
|
104
|
+
{ type: link.class.name.to_s, message: Sanitizer.string(link.message, MAX_MESSAGE_LENGTH) }
|
|
104
105
|
end
|
|
105
106
|
end
|
|
106
107
|
|
|
@@ -173,20 +174,8 @@ module Errorgap
|
|
|
173
174
|
absolute_path.sub(%r{\A#{Regexp.escape(root)}/?}, "")
|
|
174
175
|
end
|
|
175
176
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
filtered[key] = sensitive?(key) ? "[FILTERED]" : value
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def sensitive?(key)
|
|
183
|
-
@configuration.filter_keys.any? { |filter| key.to_s.downcase.include?(filter.downcase) }
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def stringify_hash(hash)
|
|
187
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
188
|
-
result[key.to_s] = value.is_a?(Hash) ? stringify_hash(value) : value
|
|
189
|
-
end
|
|
177
|
+
def sanitizer
|
|
178
|
+
@sanitizer ||= Sanitizer.new(@configuration.filter_keys)
|
|
190
179
|
end
|
|
191
180
|
end
|
|
192
181
|
end
|
data/lib/errorgap/notifier.rb
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "net/http"
|
|
5
|
-
require "uri"
|
|
6
|
-
|
|
7
3
|
module Errorgap
|
|
8
4
|
class Notifier
|
|
9
5
|
Response = Struct.new(:status, :body, :error, keyword_init: true) do
|
|
@@ -23,6 +19,9 @@ module Errorgap
|
|
|
23
19
|
def notify(error, context: {}, environment: {}, session: {}, params: {}, breadcrumbs: [], sync: false)
|
|
24
20
|
@configuration.validate!
|
|
25
21
|
return Response.new(status: 202, body: "ignored environment") if @configuration.ignored_environment?
|
|
22
|
+
unless Errorgap.remote_settings.enabled?(:errors)
|
|
23
|
+
return Response.new(status: 202, body: "errors are switched off for this project")
|
|
24
|
+
end
|
|
26
25
|
|
|
27
26
|
notice = Notice.from_exception(
|
|
28
27
|
error,
|
|
@@ -33,11 +32,15 @@ module Errorgap
|
|
|
33
32
|
params: params,
|
|
34
33
|
breadcrumbs: breadcrumbs
|
|
35
34
|
)
|
|
35
|
+
payload = run_hooks(notice.to_h)
|
|
36
|
+
return Response.new(status: 202, body: "dropped by before_notify") unless payload
|
|
37
|
+
|
|
38
|
+
Errorgap.record_release_once if @configuration.track_release
|
|
36
39
|
|
|
37
40
|
if sync || !@configuration.async
|
|
38
|
-
deliver(
|
|
41
|
+
deliver(payload)
|
|
39
42
|
else
|
|
40
|
-
Errorgap.
|
|
43
|
+
Errorgap.delivery.enqueue("notices", payload)
|
|
41
44
|
Response.new(status: 202, body: "queued")
|
|
42
45
|
end
|
|
43
46
|
rescue StandardError => exception
|
|
@@ -45,18 +48,11 @@ module Errorgap
|
|
|
45
48
|
Response.new(error: exception)
|
|
46
49
|
end
|
|
47
50
|
|
|
51
|
+
# `notice` is a Notice or an already built payload Hash.
|
|
48
52
|
def deliver(notice)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
request["User-Agent"] = "errorgap-ruby/#{Errorgap::VERSION}"
|
|
53
|
-
request["X-Errorgap-Project-Key"] = @configuration.api_key if present?(@configuration.api_key)
|
|
54
|
-
request.body = JSON.generate(notice.to_h)
|
|
55
|
-
|
|
56
|
-
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
57
|
-
http.request(request)
|
|
58
|
-
end
|
|
59
|
-
Response.new(status: response.code.to_i, body: response.body)
|
|
53
|
+
payload = notice.respond_to?(:to_h) && !notice.is_a?(Hash) ? notice.to_h : notice
|
|
54
|
+
result = Errorgap.delivery.post("notices", payload)
|
|
55
|
+
Response.new(status: result.status, body: result.body, error: result.error)
|
|
60
56
|
rescue StandardError => exception
|
|
61
57
|
log(exception)
|
|
62
58
|
Response.new(error: exception)
|
|
@@ -64,12 +60,25 @@ module Errorgap
|
|
|
64
60
|
|
|
65
61
|
private
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
# A hook that raises is logged and skipped: a bug in a filter must not
|
|
64
|
+
# lose the error it was filtering.
|
|
65
|
+
def run_hooks(payload)
|
|
66
|
+
@configuration.before_notify_hooks.each do |hook|
|
|
67
|
+
begin
|
|
68
|
+
result = hook.call(payload)
|
|
69
|
+
rescue StandardError => exception
|
|
70
|
+
log(exception)
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
return nil if result == false
|
|
74
|
+
|
|
75
|
+
payload = result if result.is_a?(Hash) && result.key?(:errors)
|
|
76
|
+
end
|
|
77
|
+
payload
|
|
69
78
|
end
|
|
70
79
|
|
|
71
|
-
def
|
|
72
|
-
|
|
80
|
+
def log(exception)
|
|
81
|
+
@configuration.logger&.warn("[errorgap] #{exception.class}: #{exception.message}")
|
|
73
82
|
end
|
|
74
83
|
end
|
|
75
84
|
end
|
|
@@ -20,6 +20,7 @@ module Errorgap
|
|
|
20
20
|
notify_once(env, exception)
|
|
21
21
|
raise
|
|
22
22
|
ensure
|
|
23
|
+
Errorgap.clear_user
|
|
23
24
|
elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000.0
|
|
24
25
|
record_transaction(env, status || 500, elapsed_ms, transaction_id)
|
|
25
26
|
end
|
|
@@ -33,6 +34,8 @@ module Errorgap
|
|
|
33
34
|
rescue Exception => exception # rubocop:disable Lint/RescueException
|
|
34
35
|
notify_once(env, exception)
|
|
35
36
|
raise
|
|
37
|
+
ensure
|
|
38
|
+
Errorgap.clear_user
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
UUID = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
@@ -85,18 +88,19 @@ module Errorgap
|
|
|
85
88
|
env["PATH_INFO"]
|
|
86
89
|
end
|
|
87
90
|
|
|
91
|
+
# Queued, not sent inline: a slow or unreachable errorgap must not add
|
|
92
|
+
# its timeout to a request that is already failing.
|
|
88
93
|
def notify_once(env, exception)
|
|
89
94
|
notified = env["errorgap.notified_exception_ids"] ||= {}
|
|
90
95
|
return if notified[exception.object_id]
|
|
91
96
|
|
|
92
97
|
notified[exception.object_id] = true
|
|
93
|
-
Errorgap.
|
|
98
|
+
Errorgap.notify_once(
|
|
94
99
|
exception,
|
|
95
100
|
context: rack_context(env),
|
|
96
101
|
environment: rack_environment(env),
|
|
97
102
|
session: rack_session(env),
|
|
98
|
-
params: rack_params(env)
|
|
99
|
-
sync: true
|
|
103
|
+
params: rack_params(env)
|
|
100
104
|
)
|
|
101
105
|
end
|
|
102
106
|
|
|
@@ -112,15 +116,54 @@ module Errorgap
|
|
|
112
116
|
{
|
|
113
117
|
method: env["REQUEST_METHOD"],
|
|
114
118
|
path: env["PATH_INFO"],
|
|
115
|
-
query_string: env["QUERY_STRING"],
|
|
119
|
+
query_string: filtered_query(env["QUERY_STRING"]),
|
|
116
120
|
user_agent: env["HTTP_USER_AGENT"],
|
|
117
121
|
remote_addr: env["REMOTE_ADDR"]
|
|
118
122
|
}
|
|
119
123
|
end
|
|
120
124
|
|
|
125
|
+
# `?token=abc` is as sensitive in the query string as it is in params.
|
|
126
|
+
def filtered_query(query)
|
|
127
|
+
return query if query.nil? || query.empty?
|
|
128
|
+
|
|
129
|
+
keys = Array(Errorgap.configuration.filter_keys)
|
|
130
|
+
query.split("&").map do |pair|
|
|
131
|
+
name, value = pair.split("=", 2)
|
|
132
|
+
sensitive = keys.any? do |key|
|
|
133
|
+
key.is_a?(Regexp) ? key.match?(name.to_s) : name.to_s.downcase.include?(key.to_s.downcase)
|
|
134
|
+
end
|
|
135
|
+
sensitive && value ? "#{name}=[FILTERED]" : pair
|
|
136
|
+
end.join("&")
|
|
137
|
+
rescue StandardError
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
121
141
|
def rack_session(env)
|
|
122
142
|
session = env["rack.session"]
|
|
123
|
-
session.respond_to?(:to_hash) ? session.to_hash : {}
|
|
143
|
+
data = session.respond_to?(:to_hash) ? session.to_hash : {}
|
|
144
|
+
signed_in_user(env).merge(data)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
{}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The signed-in user (Warden / Devise), unless the app named one itself
|
|
150
|
+
# with `Errorgap.set_user`. Only looks at a user Warden already loaded:
|
|
151
|
+
# it never triggers authentication from inside error handling.
|
|
152
|
+
def signed_in_user(env)
|
|
153
|
+
return {} unless Errorgap.configuration.capture_user
|
|
154
|
+
return {} if Errorgap.current_user
|
|
155
|
+
|
|
156
|
+
warden = env["warden"]
|
|
157
|
+
return {} unless warden.respond_to?(:authenticated?) && warden.respond_to?(:user)
|
|
158
|
+
return {} unless warden.authenticated?
|
|
159
|
+
|
|
160
|
+
user = warden.user
|
|
161
|
+
found = {}
|
|
162
|
+
found["user_id"] = user.id.to_s if user.respond_to?(:id) && user.id
|
|
163
|
+
found["user_email"] = user.email.to_s if user.respond_to?(:email) && user.email
|
|
164
|
+
found
|
|
165
|
+
rescue StandardError
|
|
166
|
+
{}
|
|
124
167
|
end
|
|
125
168
|
|
|
126
169
|
def rack_params(env)
|
|
@@ -23,6 +23,21 @@ module Errorgap
|
|
|
23
23
|
# subscribers no-op unless the middleware starts a per-request
|
|
24
24
|
# collection, which does check the flag.
|
|
25
25
|
SpanCollector.install
|
|
26
|
+
Errorgap::Integrations::NetHTTP.install
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Background jobs: report their errors and time them. Like the span
|
|
30
|
+
# collector these check the configuration when a job runs.
|
|
31
|
+
initializer "errorgap.install_job_integrations" do
|
|
32
|
+
Errorgap::Integrations::ActiveJob.install
|
|
33
|
+
Errorgap::Integrations::Sidekiq.install
|
|
34
|
+
Errorgap::Integrations::Resque.install
|
|
35
|
+
Errorgap::Integrations::DelayedJob.install
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `rails some:task` / `rake some:task` errors.
|
|
39
|
+
rake_tasks do
|
|
40
|
+
Errorgap::Integrations::Rake.install
|
|
26
41
|
end
|
|
27
42
|
|
|
28
43
|
# After config/initializers, so `auth_events` is readable here.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Errorgap
|
|
4
|
+
# Works out which commit is running when `config.release` is not set, so
|
|
5
|
+
# errors are attributed to a deploy without any setup: the commit variables
|
|
6
|
+
# hosting platforms export, Capistrano's REVISION file, then the checkout's
|
|
7
|
+
# `.git/HEAD`.
|
|
8
|
+
module ReleaseDetector
|
|
9
|
+
ENV_KEYS = %w[
|
|
10
|
+
SOURCE_VERSION HEROKU_SLUG_COMMIT HEROKU_BUILD_COMMIT RENDER_GIT_COMMIT
|
|
11
|
+
RAILWAY_GIT_COMMIT_SHA VERCEL_GIT_COMMIT_SHA KAMAL_VERSION GIT_COMMIT GIT_SHA
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def detect(root_directory, env = ENV)
|
|
16
|
+
ENV_KEYS.each do |key|
|
|
17
|
+
value = env[key].to_s.strip
|
|
18
|
+
return value unless value.empty?
|
|
19
|
+
end
|
|
20
|
+
root = root_directory.to_s
|
|
21
|
+
return nil if root.empty?
|
|
22
|
+
|
|
23
|
+
revision_file(root) || git_head(root)
|
|
24
|
+
rescue StandardError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def revision_file(root)
|
|
31
|
+
path = File.join(root, "REVISION")
|
|
32
|
+
return nil unless File.file?(path)
|
|
33
|
+
|
|
34
|
+
value = File.read(path, 200).to_s.strip
|
|
35
|
+
value.empty? ? nil : value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def git_head(root)
|
|
39
|
+
git = File.join(root, ".git")
|
|
40
|
+
# A worktree or submodule has a `.git` file pointing at the real dir.
|
|
41
|
+
if File.file?(git)
|
|
42
|
+
pointer = File.read(git, 1024).to_s[/\Agitdir:\s*(.+)$/, 1]
|
|
43
|
+
return nil unless pointer
|
|
44
|
+
|
|
45
|
+
git = File.expand_path(pointer.strip, root)
|
|
46
|
+
end
|
|
47
|
+
head_path = File.join(git, "HEAD")
|
|
48
|
+
return nil unless File.file?(head_path)
|
|
49
|
+
|
|
50
|
+
head = File.read(head_path, 1024).to_s.strip
|
|
51
|
+
ref = head[/\Aref:\s*(.+)\z/, 1]
|
|
52
|
+
return sha(head) unless ref
|
|
53
|
+
|
|
54
|
+
ref_path = File.join(git, ref)
|
|
55
|
+
return sha(File.read(ref_path, 200).to_s.strip) if File.file?(ref_path)
|
|
56
|
+
|
|
57
|
+
packed = File.join(git, "packed-refs")
|
|
58
|
+
return nil unless File.file?(packed)
|
|
59
|
+
|
|
60
|
+
line = File.foreach(packed).find { |entry| entry.strip.end_with?(" #{ref}") }
|
|
61
|
+
line ? sha(line.split(" ").first) : nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sha(value)
|
|
65
|
+
value.to_s.match?(/\A\h{7,64}\z/) ? value : nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|