exceptio 0.2.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/MIT-LICENSE +20 -0
- data/README.md +63 -0
- data/Rakefile +37 -0
- data/app/assets/config/exceptio_manifest.js +2 -0
- data/app/assets/javascripts/exceptio/application.js +2 -0
- data/app/assets/stylesheets/exceptio/application.css +15 -0
- data/app/controllers/exceptio/application_controller.rb +7 -0
- data/app/controllers/exceptio/exceptions_controller.rb +27 -0
- data/app/drops/exceptio/application_drop.rb +17 -0
- data/app/drops/exceptio/exception_drop.rb +9 -0
- data/app/drops/exceptio/instance_drop.rb +5 -0
- data/app/helpers/exceptio/application_helper.rb +13 -0
- data/app/jobs/exceptio/application_job.rb +4 -0
- data/app/mailers/exceptio/application_mailer.rb +6 -0
- data/app/models/exceptio/application_record.rb +9 -0
- data/app/models/exceptio/exception.rb +19 -0
- data/app/models/exceptio/instance.rb +44 -0
- data/app/services/exceptio/application_service.rb +22 -0
- data/app/services/exceptio/backtrace_enrichment_service.rb +59 -0
- data/app/services/exceptio/exception_recording_service.rb +51 -0
- data/app/tables/exceptio/exceptions_table.rb +58 -0
- data/app/views/exceptio/exception.html.slim +101 -0
- data/app/views/exceptio/exceptions/_trace_element.html.slim +15 -0
- data/app/views/exceptio/exceptions/index.html.slim +5 -0
- data/app/views/exceptio/exceptions/show.html.slim +74 -0
- data/app/views/exceptio/instances/_instance.html.slim +48 -0
- data/config/importmap.rb +4 -0
- data/config/locales/en.yml +16 -0
- data/config/locales/nl.yml +16 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20190729153259_create_exceptions.rb +23 -0
- data/db/migrate/20201027102223_add_indices1.rb +8 -0
- data/db/migrate/20221229092150_add_ownable_to_instance.rb +6 -0
- data/lib/exceptio/engine.rb +28 -0
- data/lib/exceptio/rack/rails_instrumentation.rb +52 -0
- data/lib/exceptio/railtie.rb +19 -0
- data/lib/exceptio/recording.rb +92 -0
- data/lib/exceptio/version.rb +5 -0
- data/lib/exceptio.rb +69 -0
- metadata +222 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
= sts.card :exceptio_exceptions, title: @exception.exception_class, description: @exception.code_location do |card|
|
|
2
|
+
- card.with_action
|
|
3
|
+
= link_to exception_path(@exception), data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' }, class: 'button'
|
|
4
|
+
i.fas.fa-trash
|
|
5
|
+
- card.with_tab :exception, padding: true
|
|
6
|
+
.grid.grid-cols-4.gap-4.mb-4
|
|
7
|
+
.col-span-3
|
|
8
|
+
h1 = @exception.exception_class
|
|
9
|
+
pre.text-xs style="height: 400px; overflow-y: scroll;"
|
|
10
|
+
code
|
|
11
|
+
= @exception.message
|
|
12
|
+
.col-span-1
|
|
13
|
+
= column_chart @exception.instances.group_by_day(:created_at).count
|
|
14
|
+
|
|
15
|
+
.grid.grid-cols-4.gap-4
|
|
16
|
+
.col-span-3
|
|
17
|
+
ul role="list" class="space-y-6"
|
|
18
|
+
= render partial: 'trace_element', collection: @exception.detailed_backtrace.map(&:symbolize_keys)
|
|
19
|
+
.col-span-1
|
|
20
|
+
|
|
21
|
+
- card.with_tab :instances, padding: true, badge: @exception.instances_count
|
|
22
|
+
.flow-root
|
|
23
|
+
ul.list-none.-mb-8
|
|
24
|
+
- @exception.instances.order(created_at: :desc).limit(10).each do |instance|
|
|
25
|
+
li
|
|
26
|
+
.relative.pb-8
|
|
27
|
+
span.absolute.left-5.top-5.-ml-px.h-full.w-0.5.bg-gray-200
|
|
28
|
+
.relative.flex.items-start.space-x-3
|
|
29
|
+
.relative
|
|
30
|
+
.flex.h-10.w-10.items-center.justify-center.rounded-full.bg-gray-100.ring-8.ring-white
|
|
31
|
+
- if instance.job?
|
|
32
|
+
i.fas.fa-dumbbell
|
|
33
|
+
- elsif instance.web?
|
|
34
|
+
i.fas.fa-globe
|
|
35
|
+
|
|
36
|
+
.min-w-0.flex-1
|
|
37
|
+
div
|
|
38
|
+
.text-sm
|
|
39
|
+
// could be a linK?
|
|
40
|
+
.font-medium.text-gray-900
|
|
41
|
+
= instance.message
|
|
42
|
+
p.mt-1.text-sm.text-gray-500
|
|
43
|
+
= ln(instance.created_at)
|
|
44
|
+
- if instance.browser
|
|
45
|
+
span.mr-1
|
|
46
|
+
- if instance.browser.chrome?
|
|
47
|
+
i.fab.fa-chrome
|
|
48
|
+
- elsif instance.browser.firefox?
|
|
49
|
+
i.fab.fa-chrome
|
|
50
|
+
- elsif instance.browser.edge?
|
|
51
|
+
i.fab.fa-edge
|
|
52
|
+
- else
|
|
53
|
+
i.fab.fa-safari
|
|
54
|
+
span.mr-1
|
|
55
|
+
- if instance.browser.platform.windows?
|
|
56
|
+
i.fab.fa-windows
|
|
57
|
+
- elsif instance.browser.platform.mac?
|
|
58
|
+
i.fab.fa-apple
|
|
59
|
+
- elsif instance.browser.platform.linux?
|
|
60
|
+
i.fab.fa-linux
|
|
61
|
+
|
|
62
|
+
.mt-2.text-sm.text-gray-700
|
|
63
|
+
= sts.info class: "grid grid-cols-1 gap-4 sm:grid-cols-3 mt-5" do |info|
|
|
64
|
+
= info.with_item :path, class: "sm:col-span-1"
|
|
65
|
+
- instance.request_env['REQUEST_URI']
|
|
66
|
+
= info.with_item :method, class: "sm:col-span-1"
|
|
67
|
+
- instance.request_env['REQUEST_METHOD']
|
|
68
|
+
= info.with_item :remote_ip, class: "sm:col-span-1"
|
|
69
|
+
- instance.request_env['REMOTE_ADDR']
|
|
70
|
+
= info.with_item :queue, class: "sm:col-span-1"
|
|
71
|
+
- instance.job['queue']
|
|
72
|
+
/= instance.context
|
|
73
|
+
|
|
74
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.grid.grid-cols-1.gap-4.px-6.py-6.sm:grid-cols-3
|
|
2
|
+
.sm:col-span-3
|
|
3
|
+
= sts.info class: "grid grid-cols-1 gap-4 sm:grid-cols-2" do |info|
|
|
4
|
+
= info.with_item :error, content: instance.message, class: "sm:col-span-1"
|
|
5
|
+
= info.with_item :created_at, content: ln(instance.created_at), class: "sm:col-span-1", icon: 'fal fa-calendar'
|
|
6
|
+
= info.with_item :exceptionable_id, content: instance.exceptio_exceptionable_id, class: "sm:col-span-1"
|
|
7
|
+
= info.with_item :exceptionable_type, content: instance.exceptio_exceptionable_type, class: "sm:col-span-1"
|
|
8
|
+
br
|
|
9
|
+
- instance.related_data.each do |data|
|
|
10
|
+
.col-span-1.sm:col-span-1
|
|
11
|
+
= data.first.class.name
|
|
12
|
+
.col-span-1.sm:col-span-1
|
|
13
|
+
= link_to(data.second)
|
|
14
|
+
= data.last
|
|
15
|
+
- instance.context.each do |field, value|
|
|
16
|
+
.col-span-1.sm:col-span-1
|
|
17
|
+
= sts.card(title: field) do |card|
|
|
18
|
+
textarea data-controller="editor" data-editor-target="textarea" data-editor-mode="ruby" data-editor-height="400px" data-editor-readonly="true" = value
|
|
19
|
+
- if field == "job"
|
|
20
|
+
- gids = []
|
|
21
|
+
- hashes = eval(value)["args"].compact.map {|a| a.is_a?(Hash) ? a["arguments"] : nil}.compact.first&.select{|i| i.is_a?(Hash)} || []
|
|
22
|
+
- hashes.each do |h|
|
|
23
|
+
- h.each do |h1, v1|
|
|
24
|
+
- if v1.is_a?(Hash)
|
|
25
|
+
- gids << v1.values.first
|
|
26
|
+
- elsif v1.is_a?(String)
|
|
27
|
+
- gids << v1
|
|
28
|
+
- gids = gids.select{|k| k.start_with?("gid://")}
|
|
29
|
+
- GlobalID::Locator.locate_many(gids).each do |thing|
|
|
30
|
+
- if thing.is_a?(User)
|
|
31
|
+
= link_to(thing.name, admin_user_path(thing))
|
|
32
|
+
- elsif thing.is_a?(Account)
|
|
33
|
+
= link_to(thing.name, admin_account_path(thing))
|
|
34
|
+
- elsif thing.is_a?(Location)
|
|
35
|
+
= link_to(thing.name, admin_location_path(thing))
|
|
36
|
+
- elsif thing.is_a?(Order)
|
|
37
|
+
= link_to(thing.name, order_path(thing))
|
|
38
|
+
- elsif thing.is_a?(Shipment)
|
|
39
|
+
= link_to(thing.name, shipment_path(thing))
|
|
40
|
+
- elsif thing.is_a?(Nuntius::Message)
|
|
41
|
+
= link_to(thing.transport.upcase, Nuntius::Engine.routes.url_helpers.admin_message_path(thing))
|
|
42
|
+
- elsif thing.is_a?(Integration)
|
|
43
|
+
= link_to(thing.name, admin_integration_path(thing))
|
|
44
|
+
- elsif thing.is_a?(Vorto::Flow)
|
|
45
|
+
= link_to(thing.name, Vorto::Engine.routes.url_helpers.flow_path(thing))
|
|
46
|
+
- elsif ![Exceptio::Instance].member?(thing.class)
|
|
47
|
+
= link_to(thing)
|
|
48
|
+
br
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateExceptions < ActiveRecord::Migration[5.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :exceptio_exceptions, id: :uuid do |t|
|
|
4
|
+
t.string :exception_class, null: false
|
|
5
|
+
t.string :code_location, null: false
|
|
6
|
+
t.jsonb :detailed_backtrace, default: [], null: false
|
|
7
|
+
t.integer :instances_count, default: 0, null: false
|
|
8
|
+
|
|
9
|
+
t.timestamp :last_instance_at
|
|
10
|
+
t.timestamps null: false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
create_table :exceptio_instances do |t|
|
|
14
|
+
t.uuid :exception_id, null: false
|
|
15
|
+
t.string :message, null: false
|
|
16
|
+
t.string :related_sgids, array: true, default: [], null: false
|
|
17
|
+
t.string :log_lines, array: true, default: [], null: false
|
|
18
|
+
t.jsonb :context, default: [], null: false
|
|
19
|
+
|
|
20
|
+
t.timestamps null: false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'slim'
|
|
2
|
+
require 'tailwindcss-rails'
|
|
3
|
+
require "importmap-rails"
|
|
4
|
+
require "turbo-rails"
|
|
5
|
+
require "stimulus-rails"
|
|
6
|
+
|
|
7
|
+
require 'chartkick'
|
|
8
|
+
require 'groupdate'
|
|
9
|
+
|
|
10
|
+
module Exceptio
|
|
11
|
+
class Engine < ::Rails::Engine
|
|
12
|
+
isolate_namespace Exceptio
|
|
13
|
+
|
|
14
|
+
initializer 'exceptio.assets' do |app|
|
|
15
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
16
|
+
app.config.assets.paths << root.join("app/components")
|
|
17
|
+
app.config.assets.paths << Exceptio::Engine.root.join("vendor/javascript")
|
|
18
|
+
app.config.assets.precompile += %w[exceptio_manifest]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer 'exceptio.importmap', before: "importmap" do |app|
|
|
22
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
23
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
24
|
+
app.config.importmap.cache_sweepers << root.join("app/components")
|
|
25
|
+
app.config.importmap.cache_sweepers << Exceptio::Engine.root.join("vendor/javascript")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
|
|
3
|
+
module Exceptio
|
|
4
|
+
# @api private
|
|
5
|
+
module Rack
|
|
6
|
+
class RailsInstrumentation
|
|
7
|
+
def initialize(app, options = {})
|
|
8
|
+
# Exceptio.logger.debug 'Initializing Exceptio::Rack::RailsInstrumentation'
|
|
9
|
+
@app = app
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
if Exceptio.active?
|
|
15
|
+
call_with_exceptio(env)
|
|
16
|
+
else
|
|
17
|
+
@app.call(env)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call_with_exceptio(env)
|
|
22
|
+
request = ActionDispatch::Request.new(env)
|
|
23
|
+
|
|
24
|
+
recording = Exceptio::Recording.create(
|
|
25
|
+
request_id(env),
|
|
26
|
+
'http_request',
|
|
27
|
+
request,
|
|
28
|
+
:params_method => :filtered_parameters
|
|
29
|
+
)
|
|
30
|
+
begin
|
|
31
|
+
@app.call(env)
|
|
32
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
33
|
+
recording.set_error(error)
|
|
34
|
+
controller = env['action_controller.instance']
|
|
35
|
+
if controller
|
|
36
|
+
recording.set_metadata('action', "#{controller.class}##{controller.action_name}")
|
|
37
|
+
end
|
|
38
|
+
recording.set_metadata('path', request.path)
|
|
39
|
+
recording.set_metadata('method', request.request_method)
|
|
40
|
+
Exceptio::Recording.store!
|
|
41
|
+
raise error
|
|
42
|
+
ensure
|
|
43
|
+
Exceptio::Recording.clear_current_recording!
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def request_id(env)
|
|
48
|
+
env['action_dispatch.request_id'] || SecureRandom.uuid
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Exceptio.logger.info("Loading Rails (#{Rails.version}) integration")
|
|
2
|
+
|
|
3
|
+
require "exceptio/rack/rails_instrumentation"
|
|
4
|
+
|
|
5
|
+
module Exceptio
|
|
6
|
+
# @api private
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
initializer "exceptio.configure_rails_initialization" do |app|
|
|
9
|
+
Exceptio::Railtie.initialize_exceptio(app)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.initialize_exceptio(app)
|
|
13
|
+
app.middleware.insert_after(
|
|
14
|
+
ActionDispatch::DebugExceptions,
|
|
15
|
+
Exceptio::Rack::RailsInstrumentation
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "exceptio/rack/rails_instrumentation"
|
|
2
|
+
|
|
3
|
+
module Exceptio
|
|
4
|
+
# @api private
|
|
5
|
+
class Recording
|
|
6
|
+
|
|
7
|
+
attr_reader :recording_id
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def create(recording_id, namespace, request, options = {})
|
|
11
|
+
# Allow middleware to force a new recording
|
|
12
|
+
if options.include?(:force) && options[:force]
|
|
13
|
+
Thread.current[:exceptio_recording] = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if we already have a running recording
|
|
17
|
+
if Thread.current[:exceptio_recording] != nil
|
|
18
|
+
# Log the issue and return the current recording
|
|
19
|
+
Exceptio.logger.debug "Trying to start new recording with id " \
|
|
20
|
+
"'#{recording_id}', but a recording with id '#{current.recording_id}' " \
|
|
21
|
+
"is already running. Using recording '#{current.recording_id}'."
|
|
22
|
+
|
|
23
|
+
# Return the current (running) recording
|
|
24
|
+
current
|
|
25
|
+
else
|
|
26
|
+
# Otherwise, start a new exceptio_recording
|
|
27
|
+
Thread.current[:exceptio_recording] = Exceptio::Recording.new(recording_id, namespace, request, options)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def current
|
|
32
|
+
Thread.current[:exceptio_recording]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def store!
|
|
36
|
+
current.store
|
|
37
|
+
rescue => e
|
|
38
|
+
Exceptio.logger.error("Failed to complete recording ##{current.recording_id}. #{e.message}")
|
|
39
|
+
ensure
|
|
40
|
+
clear_current_recording!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Remove current recording from current Thread.
|
|
44
|
+
# @api private
|
|
45
|
+
def clear_current_recording!
|
|
46
|
+
Thread.current[:exceptio_recording] = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(recording_id, namespace, request, options = {})
|
|
51
|
+
@recording_id = recording_id
|
|
52
|
+
@action = nil
|
|
53
|
+
@namespace = namespace
|
|
54
|
+
@request = request
|
|
55
|
+
@tags = {}
|
|
56
|
+
@options = options
|
|
57
|
+
@options[:params_method] ||= :params
|
|
58
|
+
@error = nil
|
|
59
|
+
@metadata = {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_error(error)
|
|
63
|
+
return unless error
|
|
64
|
+
|
|
65
|
+
backtrace = cleaned_backtrace(error.backtrace)
|
|
66
|
+
@error = { class_name: error.class.name, message: error.message.to_s, backtrace: backtrace || [] }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def set_metadata(key, value = '')
|
|
70
|
+
@metadata[key] = value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cleaned_backtrace(backtrace)
|
|
74
|
+
if defined?(::Rails) && backtrace
|
|
75
|
+
::Rails.backtrace_cleaner.clean(backtrace, nil)
|
|
76
|
+
else
|
|
77
|
+
backtrace
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def environment
|
|
82
|
+
return {} unless request.respond_to?(:env)
|
|
83
|
+
return {} unless request.env
|
|
84
|
+
|
|
85
|
+
request.env
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def store
|
|
89
|
+
Exceptio.logger.debug "recording: #{self.inspect}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/exceptio.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "exceptio/engine"
|
|
2
|
+
|
|
3
|
+
module Exceptio
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :base_controller, :max_occurences
|
|
8
|
+
attr_writer :logger, :related_sgids, :sgid_to_object_url_name, :context_details, :after_exception, :layout, :admin_layout
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@logger = Logger.new(STDOUT)
|
|
12
|
+
@logger.level = Logger::WARN
|
|
13
|
+
@base_controller = '::ApplicationController'
|
|
14
|
+
@context_details = -> { {} }
|
|
15
|
+
@related_sgids = -> { [] }
|
|
16
|
+
@sgid_to_object_url_name = -> { nil }
|
|
17
|
+
@after_exception = ->(exception, instance) {}
|
|
18
|
+
@max_occurences = 100
|
|
19
|
+
@layout = 'application'
|
|
20
|
+
@admin_layout = 'application'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Config: logger [Object].
|
|
24
|
+
def logger
|
|
25
|
+
@logger.is_a?(Proc) ? instance_exec(&@logger) : @logger
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Proc returning array of sgids of relevant objects, like current user or account
|
|
29
|
+
def related_sgids
|
|
30
|
+
instance_exec(&@related_sgids) if @related_sgids.is_a?(Proc)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Proc returning [object, path, name] for the specified sgid (path to and name of user or account)
|
|
34
|
+
def sgid_to_object_url_name(sgid)
|
|
35
|
+
instance_exec(sgid, &@sgid_to_object_url_name) if @sgid_to_object_url_name.is_a?(Proc)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Proc returning extra environment details (hash) to be stored with the exception
|
|
39
|
+
def context_details
|
|
40
|
+
context = instance_exec(&@context_details) if @context_details.is_a?(Proc)
|
|
41
|
+
context || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def after_exception(exception, instance)
|
|
45
|
+
instance_exec(exception, instance, &@after_exception) if @after_exception.is_a?(Proc)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
def config
|
|
51
|
+
@config ||= Configuration.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def setup
|
|
55
|
+
yield config
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def logger
|
|
59
|
+
config.logger
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def active?
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
require "exceptio/railtie" if defined?(::Rails)
|
|
69
|
+
require "exceptio/recording"
|