faultline-rails 0.1.1

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +179 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/faultline_manifest.js +2 -0
  6. data/app/assets/javascripts/faultline/application.js +42 -0
  7. data/app/assets/stylesheets/faultline/application.css +33 -0
  8. data/app/controllers/faultline/application_controller.rb +4 -0
  9. data/app/controllers/faultline/logged_exceptions_controller.rb +105 -0
  10. data/app/helpers/faultline/application_helper.rb +4 -0
  11. data/app/helpers/faultline/logged_exceptions_helper.rb +34 -0
  12. data/app/javascript/controllers/faultline_controller.js +38 -0
  13. data/app/jobs/faultline/application_job.rb +4 -0
  14. data/app/mailers/faultline/application_mailer.rb +6 -0
  15. data/app/models/faultline/application_record.rb +5 -0
  16. data/app/models/faultline/logged_exception.rb +88 -0
  17. data/app/views/faultline/logged_exceptions/_exceptions.html.erb +55 -0
  18. data/app/views/faultline/logged_exceptions/_feed.html.erb +5 -0
  19. data/app/views/faultline/logged_exceptions/_filter_group.html.erb +10 -0
  20. data/app/views/faultline/logged_exceptions/_show.html.erb +37 -0
  21. data/app/views/faultline/logged_exceptions/clear.turbo_stream.erb +6 -0
  22. data/app/views/faultline/logged_exceptions/destroy.turbo_stream.erb +2 -0
  23. data/app/views/faultline/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  24. data/app/views/faultline/logged_exceptions/feed.rss.builder +20 -0
  25. data/app/views/faultline/logged_exceptions/index.html.erb +68 -0
  26. data/app/views/faultline/logged_exceptions/query.html.erb +3 -0
  27. data/app/views/faultline/logged_exceptions/query.turbo_stream.erb +6 -0
  28. data/app/views/faultline/logged_exceptions/show.html.erb +3 -0
  29. data/app/views/faultline/logged_exceptions/show.turbo_stream.erb +5 -0
  30. data/app/views/layouts/faultline/application.html.erb +30 -0
  31. data/config/initializers/date_formats.rb +5 -0
  32. data/config/locales/en.yml +47 -0
  33. data/config/routes.rb +13 -0
  34. data/db/migrate/20240330122311_create_faultline_logged_exceptions.rb +18 -0
  35. data/faultline-rails.gemspec +28 -0
  36. data/lib/faultline/engine.rb +21 -0
  37. data/lib/faultline/version.rb +3 -0
  38. data/lib/faultline-rails.rb +1 -0
  39. data/lib/faultline.rb +94 -0
  40. data/lib/tasks/faultline_tasks.rake +4 -0
  41. metadata +146 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 220502edbdb1e99c681f01b719d8f2987c6c18fef9e45bccd9a67bb98a17483f
4
+ data.tar.gz: a20654cfb53fec6a6778e9e0f8ca7c698ab210fc0aa07b4a78e1950f9ec0a270
5
+ SHA512:
6
+ metadata.gz: 061df31c39e8a80be6ce09f3039d432ca44ba66470ca5c91c2c83a12a006332792eab3e088de0ab56b1e6ede23c0077f883a5195d0f3b50c3d2e7cace7143b0f
7
+ data.tar.gz: 5e23e10e12fea737bf89934a5a2bcf09cf11ca0fd0241cbcd7c64bef5ef7a58f7537e29faf1b55ac6c749fc182bd696393decd9219ca6051cc510e6fafbd81f5
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Tamiru Hailu
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Faultline Rails
2
+
3
+ Faultline is a production-friendly exception dashboard for Rails 8. It records unhandled application exceptions and gives your team a fast, searchable view of messages, requests, environments, and backtraces.
4
+
5
+ The dashboard is server-rendered and progressively enhanced with:
6
+
7
+ - Turbo Frames for filtering and opening exception details without full-page navigation.
8
+ - Turbo Streams for deleting one, many, or all exceptions.
9
+ - Stimulus for loading state and small interaction behavior.
10
+ - Tailwind-compatible utility classes for a responsive dashboard UI.
11
+
12
+ ## Requirements
13
+
14
+ - Ruby 3.2 or newer
15
+ - Rails 8.0 or newer
16
+ - A database supported by Active Record
17
+
18
+ ## Installation
19
+
20
+ Add Faultline to your application:
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem "faultline-rails"
25
+ ```
26
+
27
+ Install the dependencies and copy the engine migration:
28
+
29
+ ```bash
30
+ bundle install
31
+ bin/rails app:faultline:install:migrations
32
+ bin/rails db:migrate
33
+ ```
34
+
35
+ Mount the dashboard in your application:
36
+
37
+ ```ruby
38
+ # config/routes.rb
39
+ Rails.application.routes.draw do
40
+ mount Faultline::Engine => "/faultline"
41
+ end
42
+ ```
43
+
44
+ The dashboard is now available at `/faultline`.
45
+
46
+ ## Start logging exceptions
47
+
48
+ Include `Faultline::ExceptionLoggable` in your application controller:
49
+
50
+ ```ruby
51
+ # app/controllers/application_controller.rb
52
+ class ApplicationController < ActionController::Base
53
+ include Faultline::ExceptionLoggable
54
+ end
55
+ ```
56
+
57
+ Faultline logs the exception and then re-raises it so Rails keeps its normal error handling, status codes, and error pages.
58
+
59
+ ## Protect the dashboard
60
+
61
+ The dashboard contains sensitive information, including request parameters, environment variables, and source paths. Do not expose it to unauthenticated public users.
62
+
63
+ Attach your application's authorization callback:
64
+
65
+ ```ruby
66
+ # config/initializers/faultline.rb
67
+ Rails.application.config.to_prepare do
68
+ Faultline::LoggedExceptionsController.before_action :require_admin!
69
+ end
70
+ ```
71
+
72
+ Replace `require_admin!` with the authentication method provided by your application. You can also configure a policy, basic authentication, or an internal-only route at the host application level.
73
+
74
+ ## Rails 8 frontend setup
75
+
76
+ Faultline includes `turbo-rails` and `stimulus-rails` as dependencies. Install the host application's Hotwire entry points when needed:
77
+
78
+ ```bash
79
+ bin/rails turbo:install
80
+ bin/rails stimulus:install
81
+ ```
82
+
83
+ The default Rails 8 import-map setup will load Faultline's Stimulus controller through the engine asset pipeline. If your application uses a JavaScript bundler, register the controller from `app/javascript/controllers/faultline_controller.js` in your Stimulus application:
84
+
85
+ ```javascript
86
+ import FaultlineController from "./faultline_controller"
87
+
88
+ application.register("faultline", FaultlineController)
89
+ ```
90
+
91
+ ## Tailwind setup
92
+
93
+ Faultline's dashboard uses Tailwind utility classes. Add the gem's view directory to the sources scanned by your Tailwind build; otherwise the host application will purge the dashboard classes.
94
+
95
+ For Tailwind CSS v4, add a source entry to the application's Tailwind stylesheet. Use the absolute path returned by Bundler for the installed gem:
96
+
97
+ ```css
98
+ @import "tailwindcss";
99
+ @source "/absolute/path/to/faultline-rails/app/views";
100
+ ```
101
+
102
+ For Tailwind CSS v3, add the gem path to `content` in `tailwind.config.js`:
103
+
104
+ ```javascript
105
+ const faultlinePath = "/absolute/path/to/faultline-rails"
106
+
107
+ module.exports = {
108
+ content: [
109
+ "./app/views/**/*.{erb,html}",
110
+ `${faultlinePath}/app/views/**/*.html.erb`
111
+ ]
112
+ }
113
+ ```
114
+
115
+ You can find the installed path with:
116
+
117
+ ```bash
118
+ bundle show faultline-rails
119
+ ```
120
+
121
+ ## Configuration
122
+
123
+ Set a title for the dashboard and attach additional application data to each record:
124
+
125
+ ```ruby
126
+ # config/initializers/faultline.rb
127
+ Rails.application.config.to_prepare do
128
+ Faultline::LoggedExceptionsController.application_name = "Acme"
129
+
130
+ ApplicationController.exception_data = lambda do |controller|
131
+ {
132
+ request_id: controller.request.request_id,
133
+ user_id: controller.current_user&.id
134
+ }
135
+ end
136
+ end
137
+ ```
138
+
139
+ Exclude trusted private networks from the dashboard's local-request handling:
140
+
141
+ ```ruby
142
+ class ApplicationController < ActionController::Base
143
+ include Faultline::ExceptionLoggable
144
+
145
+ consider_local "10.0.0.0/8", "192.168.0.0/16"
146
+ end
147
+ ```
148
+
149
+ Rails' `filter_parameters` configuration is respected before request parameters are stored.
150
+
151
+ ## Dashboard features
152
+
153
+ - Search exception messages.
154
+ - Filter by exception class, controller/action, or age.
155
+ - Open full exception details in a Turbo Frame.
156
+ - Delete individual exceptions without leaving the page.
157
+ - Delete the currently visible result set.
158
+ - Clear the complete history.
159
+ - Subscribe to the RSS feed at `/faultline/logged_exceptions/feed.rss`.
160
+
161
+ ## Data storage
162
+
163
+ The engine creates a `faultline_logged_exceptions` table containing the exception class, controller/action, message, backtrace, request, environment, user information, user agent, remote IP, and timestamps.
164
+
165
+ Exception records can contain secrets. Apply your normal database encryption, retention, backup, and access-control policies.
166
+
167
+ ## Development
168
+
169
+ Run the test suite with:
170
+
171
+ ```bash
172
+ bin/rails test
173
+ ```
174
+
175
+ The repository includes a Rails dummy application under `test/dummy` for engine integration testing.
176
+
177
+ ## License
178
+
179
+ Faultline Rails is released under the [MIT License](MIT-LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,2 @@
1
+ //= link_directory ../stylesheets/faultline .css
2
+ //= link_directory ../javascripts/faultline .js
@@ -0,0 +1,42 @@
1
+ class FaultlineController {
2
+ static targets = ["activity"]
3
+
4
+ connect() {
5
+ this.showActivity = this.showActivity.bind(this)
6
+ this.hideActivity = this.hideActivity.bind(this)
7
+
8
+ document.addEventListener("turbo:before-fetch-request", this.showActivity)
9
+ document.addEventListener("turbo:before-fetch-response", this.hideActivity)
10
+ document.addEventListener("turbo:submit-end", this.hideActivity)
11
+ }
12
+
13
+ disconnect() {
14
+ document.removeEventListener("turbo:before-fetch-request", this.showActivity)
15
+ document.removeEventListener("turbo:before-fetch-response", this.hideActivity)
16
+ document.removeEventListener("turbo:submit-end", this.hideActivity)
17
+ }
18
+
19
+ submit() {
20
+ this.showActivity()
21
+ }
22
+
23
+ showActivity() {
24
+ this.activityTarget?.classList.remove("hidden")
25
+ }
26
+
27
+ hideActivity() {
28
+ this.activityTarget?.classList.add("hidden")
29
+ }
30
+
31
+ closeDetails(event) {
32
+ event.preventDefault()
33
+ const details = document.getElementById("exception-details")
34
+ if (details) details.innerHTML = ""
35
+ }
36
+ }
37
+
38
+ if (window.Stimulus) {
39
+ window.Stimulus.register("faultline", FaultlineController)
40
+ }
41
+
42
+ export default FaultlineController
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Faultline's views use Tailwind utility classes. Add the gem's view path to
3
+ * the host application's Tailwind sources so those classes are compiled.
4
+ *
5
+ * This small fallback keeps will_paginate usable when a host app has not yet
6
+ * added Tailwind's pagination styles.
7
+ */
8
+ .faultline-pagination .pagination {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ gap: 0.5rem;
12
+ align-items: center;
13
+ }
14
+
15
+ .faultline-pagination .pagination a,
16
+ .faultline-pagination .pagination span {
17
+ padding: 0.35rem 0.65rem;
18
+ border-radius: 0.5rem;
19
+ }
20
+
21
+ .faultline-pagination .pagination a {
22
+ color: #4338ca;
23
+ }
24
+
25
+ .faultline-pagination .pagination a:hover {
26
+ background: #eef2ff;
27
+ }
28
+
29
+ .faultline-pagination .pagination .current {
30
+ background: #4f46e5;
31
+ color: white;
32
+ font-weight: 600;
33
+ }
@@ -0,0 +1,4 @@
1
+ module Faultline
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,105 @@
1
+ module Faultline
2
+ class LoggedExceptionsController < ApplicationController
3
+ cattr_accessor :application_name
4
+
5
+ helper_method :params_filters
6
+
7
+ def index
8
+ @exception_names = LoggedException.class_names
9
+ @controller_actions = LoggedException.controller_actions
10
+ @exceptions = filtered_exceptions
11
+ end
12
+
13
+ def query
14
+ @exceptions = filtered_exceptions
15
+
16
+ respond_to do |format|
17
+ format.turbo_stream
18
+ format.html { render :query }
19
+ end
20
+ end
21
+
22
+ def feed
23
+ @exceptions = LoggedException.all
24
+
25
+ respond_to do |format|
26
+ format.rss { render layout: false }
27
+ end
28
+ end
29
+
30
+ def show
31
+ @exception = LoggedException.find(params[:id])
32
+
33
+ respond_to do |format|
34
+ format.turbo_stream
35
+ format.html
36
+ end
37
+ end
38
+
39
+ def destroy
40
+ @exception = LoggedException.find(params[:id])
41
+ @exception.destroy!
42
+
43
+ respond_to do |format|
44
+ format.turbo_stream
45
+ format.html { redirect_to logged_exceptions_path, status: :see_other }
46
+ end
47
+ end
48
+
49
+ def destroy_all
50
+ exceptions = if params[:ids].present?
51
+ LoggedException.where(id: params[:ids].to_s.split(","))
52
+ else
53
+ filtered_scope
54
+ end
55
+ exceptions.delete_all
56
+ @exceptions = filtered_exceptions
57
+
58
+ respond_to do |format|
59
+ format.turbo_stream
60
+ format.html { redirect_to logged_exceptions_path(params_filters) }
61
+ end
62
+ end
63
+
64
+ def clear
65
+ LoggedException.delete_all
66
+ @exceptions = filtered_exceptions
67
+
68
+ respond_to do |format|
69
+ format.turbo_stream
70
+ format.html { redirect_back fallback_location: root_path }
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def params_filters
77
+ {
78
+ query: params[:query],
79
+ date_ranges_filter: params[:date_ranges_filter],
80
+ exception_names_filter: params[:exception_names_filter],
81
+ controller_actions_filter: params[:controller_actions_filter]
82
+ }.compact
83
+ end
84
+
85
+ def filtered_exceptions
86
+ filtered_scope.paginate(page: params[:page], per_page: 30)
87
+ end
88
+
89
+ def filtered_scope
90
+ exceptions = LoggedException.sorted
91
+ exceptions = exceptions.where(id: params[:id]) if params[:id].present?
92
+ exceptions = exceptions.message_like(params[:query]) if params[:query].present?
93
+ exceptions = exceptions.days_old(params[:date_ranges_filter]) if params[:date_ranges_filter].present?
94
+ exceptions = exceptions.by_exception_class(params[:exception_names_filter]) if params[:exception_names_filter].present?
95
+
96
+ if params[:controller_actions_filter].present?
97
+ controller_name, action_name = params[:controller_actions_filter].split("/", 2)
98
+ exceptions = exceptions.by_controller(controller_name.underscore)
99
+ exceptions = exceptions.by_action(action_name.downcase) if action_name.present?
100
+ end
101
+
102
+ exceptions
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,4 @@
1
+ module Faultline
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ module Faultline
2
+ module LoggedExceptionsHelper
3
+ def pretty_exception_date(exception)
4
+ if Date.today == exception.created_at.to_date
5
+ "Today, #{exception.created_at.strftime(Time::DATE_FORMATS[:exc_time])}"
6
+ else
7
+ exception.created_at.strftime(Time::DATE_FORMATS[:exc_date])
8
+ end
9
+ end
10
+
11
+ def filtered?
12
+ [:query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].any? { |p| params[p] }
13
+ end
14
+
15
+ def listify(text)
16
+ list_items = text.scan(/^\s*\* (.+)/).map { |match| content_tag(:li, match.first) }
17
+ content_tag(:ul, list_items)
18
+ end
19
+
20
+ def page_title(text)
21
+ title = [controller.application_name.presence, text].compact.join(" :: ")
22
+ content_for(:title, title)
23
+ end
24
+
25
+ # Rescue textilize call if RedCloth is not available.
26
+ def pretty_format(text)
27
+ begin
28
+ textilize(text).html_safe
29
+ rescue
30
+ simple_format(text).html_safe
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["activity"]
5
+
6
+ connect() {
7
+ this.showActivity = this.showActivity.bind(this)
8
+ this.hideActivity = this.hideActivity.bind(this)
9
+
10
+ document.addEventListener("turbo:before-fetch-request", this.showActivity)
11
+ document.addEventListener("turbo:before-fetch-response", this.hideActivity)
12
+ document.addEventListener("turbo:submit-end", this.hideActivity)
13
+ }
14
+
15
+ disconnect() {
16
+ document.removeEventListener("turbo:before-fetch-request", this.showActivity)
17
+ document.removeEventListener("turbo:before-fetch-response", this.hideActivity)
18
+ document.removeEventListener("turbo:submit-end", this.hideActivity)
19
+ }
20
+
21
+ submit() {
22
+ this.showActivity()
23
+ }
24
+
25
+ showActivity() {
26
+ this.activityTarget?.classList.remove("hidden")
27
+ }
28
+
29
+ hideActivity() {
30
+ this.activityTarget?.classList.add("hidden")
31
+ }
32
+
33
+ closeDetails(event) {
34
+ event.preventDefault()
35
+ const details = document.getElementById("exception-details")
36
+ if (details) details.innerHTML = ""
37
+ }
38
+ }
@@ -0,0 +1,4 @@
1
+ module Faultline
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Faultline
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Faultline
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,88 @@
1
+ module Faultline
2
+ class LoggedException < ApplicationRecord
3
+ self.table_name = "faultline_logged_exceptions"
4
+ HOSTNAME = Socket.gethostname
5
+
6
+ class << self
7
+ def create_from_exception(controller, exception, data)
8
+ message = "#{exception.message.inspect}\n* Extra Data\n\n#{data}" unless data.blank?
9
+ create!(
10
+ exception_class: exception.class.name,
11
+ controller_name: controller.controller_path,
12
+ action_name: controller.action_name,
13
+ message: message,
14
+ backtrace: exception.backtrace,
15
+ request: controller.request,
16
+ user_info: controller.respond_to?(:current_user, true) ? controller.current_user : nil,
17
+ remote_ip: controller.request.remote_ip
18
+ )
19
+ end
20
+
21
+ def host_name
22
+ HOSTNAME
23
+ end
24
+ end
25
+
26
+ scope :by_exception_class, lambda { |exception_class| where(:exception_class => exception_class) }
27
+ scope :by_controller_and_action, lambda { |controller_name, action_name| where(:controller_name => controller_name, :action_name => action_name) }
28
+ scope :by_controller, lambda { |controller_name| where(:controller_name => controller_name) }
29
+ scope :by_action, lambda { |action_name| where(:action_name => action_name) }
30
+ scope :message_like, ->(query) { where("message LIKE ?", "%#{sanitize_sql_like(query)}%") }
31
+ scope :days_old, ->(day_number) { where("created_at >= ?", day_number.to_f.days.ago.utc) }
32
+ scope :sorted, -> { order(created_at: :desc) }
33
+
34
+ def name
35
+ "#{self.exception_class} in #{self.controller_action}"
36
+ end
37
+
38
+ def backtrace=(trace)
39
+ trace = sanitize_backtrace(trace) * "\n" unless trace.is_a?(String)
40
+ write_attribute :backtrace, trace
41
+ end
42
+
43
+ def request=(request)
44
+ if request.is_a?(String)
45
+ write_attribute :request, request
46
+ else
47
+ max = request.env.keys.max { |a, b| a.length <=> b.length }
48
+ env = request.env.keys.sort.inject [] do |env, key|
49
+ env << '* ' + ("%-*s: %s" % [max.length, key, request.env[key].to_s.strip])
50
+ end
51
+ write_attribute(:environment, (env << "* Process: #{$$}" << "* Server : #{self.class.host_name}") * "\n")
52
+
53
+ write_attribute(:request, [
54
+ "* URL:#{" #{request.method.to_s.upcase}" unless request.get?} #{request.protocol}#{request.env["HTTP_HOST"]}#{request.fullpath}",
55
+ "* Format: #{request.format.to_s}",
56
+ "* Parameters: #{request.parameters.inspect}",
57
+ "* Rails Root: #{rails_root}"
58
+ ] * "\n")
59
+ end
60
+ end
61
+
62
+ def controller_action
63
+ @controller_action ||= "#{controller_name.camelcase}/#{action_name}"
64
+ end
65
+
66
+ def self.class_names
67
+ select("DISTINCT exception_class").order(:exception_class).collect(&:exception_class)
68
+ end
69
+
70
+ def self.controller_actions
71
+ select("DISTINCT controller_name, action_name").order(:controller_name, :action_name).collect(&:controller_action)
72
+ end
73
+
74
+ private
75
+
76
+ @@rails_root = Pathname.new(Rails.root).cleanpath.to_s
77
+ @@backtrace_regex = /^#{Regexp.escape(@@rails_root)}/
78
+
79
+ def sanitize_backtrace(trace)
80
+ gem_path = Bundler.bundle_path.to_s
81
+ trace.reject { |line| line.include?(gem_path) }.collect { |line| Pathname.new(line.gsub(@@backtrace_regex, "[RAILS_ROOT]")).cleanpath.to_s }
82
+ end
83
+
84
+ def rails_root
85
+ @@rails_root
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,55 @@
1
+ <div id="exceptions-content" class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
2
+ <div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-5 py-4">
3
+ <div>
4
+ <h2 class="text-lg font-semibold text-slate-900">
5
+ <%= t(".heading") %>
6
+ <% if filtered? %>
7
+ <span class="font-normal text-slate-500">(<%= t(".filtered") %>)</span>
8
+ <% end %>
9
+ </h2>
10
+ <p class="mt-1 text-sm text-slate-500"><%= t(".count", count: @exceptions.total_entries) %></p>
11
+ </div>
12
+
13
+ <%= link_to t(".delete_visible"), destroy_all_logged_exceptions_path(params_filters),
14
+ class: "rounded-lg border border-red-200 px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50",
15
+ data: { turbo_method: :post, turbo_confirm: t(".confirm_delete_visible"), turbo_stream: true } %>
16
+ </div>
17
+
18
+ <% if @exceptions.empty? %>
19
+ <div class="px-5 py-16 text-center text-sm text-slate-500"><%= t(".empty") %></div>
20
+ <% else %>
21
+ <div class="overflow-x-auto">
22
+ <table class="min-w-full divide-y divide-slate-200">
23
+ <thead class="bg-slate-50">
24
+ <tr>
25
+ <th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".exception") %></th>
26
+ <th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".date") %></th>
27
+ <th class="px-5 py-3"><span class="sr-only"><%= t(".actions") %></span></th>
28
+ </tr>
29
+ </thead>
30
+ <tbody class="divide-y divide-slate-100">
31
+ <% @exceptions.each do |exception| %>
32
+ <tr id="<%= dom_id(exception) %>" class="group hover:bg-slate-50">
33
+ <td class="max-w-2xl px-5 py-4 align-top">
34
+ <%= link_to exception.name, logged_exception_path(exception),
35
+ class: "font-medium text-indigo-700 hover:text-indigo-900 hover:underline",
36
+ data: { turbo_frame: "exception-details" } %>
37
+ <p class="mt-1 truncate text-sm text-slate-600"><%= exception.message %></p>
38
+ </td>
39
+ <td class="whitespace-nowrap px-5 py-4 align-top text-sm text-slate-500"><%= pretty_exception_date(exception) %></td>
40
+ <td class="whitespace-nowrap px-5 py-4 text-right align-top">
41
+ <%= link_to t(".delete"), logged_exception_path(exception),
42
+ class: "text-sm font-medium text-red-600 hover:text-red-800 hover:underline",
43
+ data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
44
+ </td>
45
+ </tr>
46
+ <% end %>
47
+ </tbody>
48
+ </table>
49
+ </div>
50
+ <% end %>
51
+
52
+ <div class="faultline-pagination border-t border-slate-200 px-5 py-4 text-sm">
53
+ <%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters) %>
54
+ </div>
55
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="mt-6 border-t border-slate-200 pt-5">
2
+ <%= link_to feed_logged_exceptions_path(format: :rss), class: "text-sm font-medium text-indigo-700 hover:underline" do %>
3
+ <%= t(".rss_feed") %>
4
+ <% end %>
5
+ </div>
@@ -0,0 +1,10 @@
1
+ <div class="mt-6">
2
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500"><%= title %></h3>
3
+ <nav class="mt-2 max-h-40 space-y-1 overflow-y-auto">
4
+ <% values.each do |value| %>
5
+ <%= link_to value, query_logged_exceptions_path(parameter => value),
6
+ class: "block truncate rounded-lg px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900",
7
+ data: { turbo_frame: "exceptions" } %>
8
+ <% end %>
9
+ </nav>
10
+ </div>
@@ -0,0 +1,37 @@
1
+ <div class="flex items-center justify-between gap-4 border-b border-slate-200 pb-4">
2
+ <div>
3
+ <p class="text-sm text-slate-500"><%= @exception.created_at.strftime(Time::DATE_FORMATS[:exc_full]) %></p>
4
+ <h2 class="mt-1 break-words text-xl font-semibold text-slate-900"><%= @exception.name %></h2>
5
+ </div>
6
+ <button type="button" class="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100" data-action="click->faultline#closeDetails">
7
+ <%= t(".close") %>
8
+ </button>
9
+ </div>
10
+
11
+ <div class="mt-5 space-y-5 text-sm text-slate-700">
12
+ <section>
13
+ <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".request") %></h3>
14
+ <div class="overflow-x-auto rounded-lg bg-slate-50 p-4"><%= pretty_format(@exception.request) %></div>
15
+ </section>
16
+
17
+ <section>
18
+ <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".message") %></h3>
19
+ <div class="rounded-lg bg-slate-50 p-4"><%= simple_format(@exception.message) %></div>
20
+ </section>
21
+
22
+ <section>
23
+ <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".backtrace") %></h3>
24
+ <pre class="max-h-96 overflow-auto rounded-lg bg-slate-950 p-4 text-xs leading-5 text-slate-100"><%= @exception.backtrace %></pre>
25
+ </section>
26
+
27
+ <section>
28
+ <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".environment") %></h3>
29
+ <div class="overflow-x-auto rounded-lg bg-slate-50 p-4"><%= pretty_format(@exception.environment) %></div>
30
+ </section>
31
+ </div>
32
+
33
+ <div class="mt-6 border-t border-slate-200 pt-4">
34
+ <%= link_to t(".delete"), logged_exception_path(@exception),
35
+ class: "text-sm font-medium text-red-600 hover:text-red-800 hover:underline",
36
+ data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
37
+ </div>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.replace "exceptions" do %>
2
+ <%= turbo_frame_tag "exceptions" do %>
3
+ <%= render "exceptions" %>
4
+ <% end %>
5
+ <% end %>
6
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,2 @@
1
+ <%= turbo_stream.remove dom_id(@exception) %>
2
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.replace "exceptions" do %>
2
+ <%= turbo_frame_tag "exceptions" do %>
3
+ <%= render "exceptions" %>
4
+ <% end %>
5
+ <% end %>
6
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,20 @@
1
+ xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
2
+
3
+ xml.rss "version" => "2.0" do
4
+ xml.channel do
5
+ xml.title "#{Faultline::LoggedExceptionsController.application_name}"
6
+ xml.link url_for(:only_path => false, :skip_relative_url_root => false)
7
+ xml.language "en-us"
8
+ xml.ttl "60"
9
+
10
+ @exceptions.each do |exc|
11
+ xml.item do
12
+ xml.title "#{exc.name} @ #{exc.created_at.rfc822}"
13
+ xml.description exc.message
14
+ xml.pubDate exc.created_at.rfc822
15
+ xml.guid [request.host_with_port, 'exceptions', exc.id.to_s].join(":"), "isPermaLink" => "false"
16
+ xml.link url_for(:action => 'index', :anchor => "e#{exc.id}", :only_path => false, :skip_relative_url_root => false)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,68 @@
1
+ <% page_title t(".title") %>
2
+
3
+ <div data-controller="faultline" class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
4
+ <div class="mb-8 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
5
+ <div>
6
+ <p class="text-sm font-semibold uppercase tracking-widest text-indigo-600">Faultline</p>
7
+ <h1 class="mt-2 text-3xl font-bold tracking-tight text-slate-950"><%= t(".title") %></h1>
8
+ <p class="mt-2 text-slate-600"><%= t(".subtitle") %></p>
9
+ </div>
10
+ <%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
11
+ class: "rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700",
12
+ form: { data: { turbo_stream: true } },
13
+ data: { turbo_confirm: t(".confirm_clear") } %>
14
+ </div>
15
+
16
+ <div class="grid gap-6 lg:grid-cols-[16rem_minmax(0,1fr)]">
17
+ <aside class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
18
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500"><%= t(".filters") %></h2>
19
+
20
+ <nav class="mt-4 space-y-1" aria-label="<%= t(".filters") %>">
21
+ <%= link_to t(".latest_exceptions"), logged_exceptions_path,
22
+ class: "block rounded-lg px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100",
23
+ data: { turbo_frame: "exceptions" } %>
24
+ </nav>
25
+
26
+ <%= render "filter_group", title: t(".exception"), values: @exception_names, parameter: :exception_names_filter %>
27
+ <%= render "filter_group", title: t(".controller_action"), values: @controller_actions, parameter: :controller_actions_filter %>
28
+
29
+ <div class="mt-6">
30
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".dates") %></h3>
31
+ <nav class="mt-2 space-y-1">
32
+ <% [[t(".today"), 1], [t(".last_few_days"), 3], [t(".last_7_days"), 7], [t(".last_30_days"), 30]].each do |label, days| %>
33
+ <%= link_to label, query_logged_exceptions_path(date_ranges_filter: days),
34
+ class: "block rounded-lg px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900",
35
+ data: { turbo_frame: "exceptions" } %>
36
+ <% end %>
37
+ </nav>
38
+ </div>
39
+
40
+ <div class="mt-6 border-t border-slate-200 pt-5">
41
+ <%= form_with url: query_logged_exceptions_path, method: :get,
42
+ data: { turbo_frame: "exceptions", action: "submit->faultline#submit" } do |form| %>
43
+ <%= form.label :query, t(".search"), class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
44
+ <div class="mt-2 flex gap-2">
45
+ <%= form.search_field :query, placeholder: t(".search_placeholder"), class: "min-w-0 flex-1 rounded-lg border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
46
+ <%= form.submit t(".find"), class: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-semibold text-white hover:bg-slate-700" %>
47
+ </div>
48
+ <% end %>
49
+ </div>
50
+
51
+ <%= render "feed" %>
52
+ </aside>
53
+
54
+ <div class="space-y-6">
55
+ <div id="activity" data-faultline-target="activity" class="hidden rounded-lg bg-indigo-50 px-4 py-3 text-sm text-indigo-700" role="status" aria-live="polite">
56
+ <%= t(".loading") %>
57
+ </div>
58
+
59
+ <%= turbo_frame_tag "exception-details", class: "block rounded-xl border border-slate-200 bg-white p-5 shadow-sm" do %>
60
+ <p class="py-8 text-center text-sm text-slate-500"><%= t(".select_exception") %></p>
61
+ <% end %>
62
+
63
+ <%= turbo_frame_tag "exceptions" do %>
64
+ <%= render "exceptions" %>
65
+ <% end %>
66
+ </div>
67
+ </div>
68
+ </div>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "exceptions" do %>
2
+ <%= render "exceptions" %>
3
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.replace "exceptions" do %>
2
+ <%= turbo_frame_tag "exceptions" do %>
3
+ <%= render "exceptions" %>
4
+ <% end %>
5
+ <% end %>
6
+ <%= turbo_stream.update "exception-details", "" %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "exception-details", class: "block rounded-xl border border-slate-200 bg-white p-5 shadow-sm" do %>
2
+ <%= render "show" %>
3
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= turbo_stream.replace "exception-details" do %>
2
+ <%= turbo_frame_tag "exception-details", class: "block rounded-xl border border-slate-200 bg-white p-5 shadow-sm" do %>
3
+ <%= render "show" %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title><%= content_for?(:title) ? yield(:title) : "Faultline" %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <% host_assets = Rails.application.assets %>
9
+ <% host_stylesheet = if host_assets.respond_to?(:find_asset)
10
+ host_assets.find_asset("application.css")
11
+ elsif host_assets.respond_to?(:load_path)
12
+ host_assets.load_path.find("application.css")
13
+ end %>
14
+ <% host_javascript = if host_assets.respond_to?(:find_asset)
15
+ host_assets.find_asset("application.js")
16
+ elsif host_assets.respond_to?(:load_path)
17
+ host_assets.load_path.find("application.js")
18
+ end %>
19
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" if host_stylesheet %>
20
+ <%= stylesheet_link_tag "faultline/application", "data-turbo-track": "reload" %>
21
+ <%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
22
+ <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" if host_javascript %>
23
+ <%= javascript_include_tag "faultline/application", "data-turbo-track": "reload", type: "module" %>
24
+ </head>
25
+ <body class="min-h-screen bg-slate-50 text-slate-900 antialiased">
26
+ <main>
27
+ <%= yield %>
28
+ </main>
29
+ </body>
30
+ </html>
@@ -0,0 +1,5 @@
1
+ Time::DATE_FORMATS.merge!(
2
+ :exc_full => "%A, %b %d, %Y at %l:%M %p",
3
+ :exc_date => "%b %d, %Y",
4
+ :exc_time => "%l:%M %p"
5
+ )
@@ -0,0 +1,47 @@
1
+ en:
2
+ faultline:
3
+ logged_exceptions:
4
+ index:
5
+ title: Logged Exceptions
6
+ subtitle: Review, filter, and remove application exceptions.
7
+ filters: Filters
8
+ latest_exceptions: Latest Exceptions
9
+ exception: Exception
10
+ controller_action: Controller / Action
11
+ dates: Dates
12
+ today: Today
13
+ last_few_days: Last few days
14
+ last_7_days: Last 7 days
15
+ last_30_days: Last 30 days
16
+ clear_history: Clear history
17
+ confirm_clear: Clear the complete exception history?
18
+ search: Search
19
+ search_placeholder: Search messages
20
+ find: Find
21
+ loading: Loading exceptions…
22
+ select_exception: Select an exception to inspect its details.
23
+ _exceptions:
24
+ heading: Exceptions
25
+ filtered: filtered
26
+ count: "%{count} total"
27
+ empty: No exceptions match these filters.
28
+ exception: Exception
29
+ date: Date
30
+ actions: Actions
31
+ delete: Delete
32
+ delete_visible: Delete visible
33
+ confirm_delete: Delete this exception?
34
+ confirm_delete_visible: Delete all visible exceptions?
35
+ _show:
36
+ close: Close
37
+ request: Request
38
+ message: Message
39
+ backtrace: Backtrace
40
+ environment: Environment
41
+ delete: Delete exception
42
+ confirm_delete: Delete this exception?
43
+ _feed:
44
+ rss_feed: Subscribe to the RSS feed
45
+ show:
46
+ title: Logged Exception
47
+
data/config/routes.rb ADDED
@@ -0,0 +1,13 @@
1
+
2
+ Faultline::Engine.routes.draw do
3
+ resources :logged_exceptions do
4
+ collection do
5
+ post :clear
6
+ match :query, via: %i[get post]
7
+ post :destroy_all
8
+ get :feed
9
+ end
10
+ end
11
+
12
+ root :to => 'logged_exceptions#index'
13
+ end
@@ -0,0 +1,18 @@
1
+ class CreateFaultlineLoggedExceptions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :faultline_logged_exceptions do |t|
4
+ t.string :exception_class
5
+ t.string :controller_name
6
+ t.string :action_name
7
+ t.text :message
8
+ t.text :backtrace
9
+ t.text :environment
10
+ t.text :request
11
+ t.string :user_info
12
+ t.string :user_agent
13
+ t.string :remote_ip
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "lib/faultline/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "faultline-rails"
5
+ spec.version = Faultline::VERSION
6
+ spec.authors = ["Tamiru Hailu"]
7
+ spec.email = ["tamiruhailu@gmail.com"]
8
+ spec.homepage = "https://github.com/tamiru/faultline-rails"
9
+ spec.summary = "A Rails 8 exception dashboard powered by Hotwire."
10
+ spec.description = "Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible dashboard."
11
+ spec.license = "MIT"
12
+
13
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/tamiru/faultline-rails/tree/main"
16
+ spec.metadata["changelog_uri"] = "https://github.com/tamiru/faultline-rails/blob/main/CHANGELOG.md"
17
+
18
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
+ Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "faultline-rails.gemspec"]
20
+ end
21
+
22
+ spec.required_ruby_version = ">= 3.2.0"
23
+
24
+ spec.add_dependency "rails", "~> 8.0", ">= 8.0.0"
25
+ spec.add_dependency "stimulus-rails", "~> 1.3"
26
+ spec.add_dependency "turbo-rails", "~> 2.0"
27
+ spec.add_dependency "will_paginate", "~> 4.0"
28
+ end
@@ -0,0 +1,21 @@
1
+ module Faultline
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Faultline
4
+
5
+ engine_name "faultline"
6
+
7
+ initializer "faultline.assets", group: :all do |app|
8
+ next unless app.config.respond_to?(:assets) && app.config.assets
9
+
10
+ asset_root = root.join("app/assets")
11
+
12
+ if defined?(::Propshaft)
13
+ app.config.assets.paths.concat(
14
+ Dir[asset_root.join("*")].select { |path| File.directory?(path) }
15
+ )
16
+ else
17
+ app.config.assets.paths << asset_root
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Faultline
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1 @@
1
+ require "faultline"
data/lib/faultline.rb ADDED
@@ -0,0 +1,94 @@
1
+ require "rails"
2
+ require "turbo-rails"
3
+ require "stimulus-rails"
4
+ require "will_paginate"
5
+ require "ipaddr"
6
+ require "socket"
7
+ require "faultline/version"
8
+ require "faultline/engine"
9
+
10
+ module Faultline
11
+ # Copyright (c) 2005 Jamis Buck
12
+ #
13
+ # Permission is hereby granted, free of charge, to any person obtaining
14
+ # a copy of this software and associated documentation files (the
15
+ # "Software"), to deal in the Software without restriction, including
16
+ # without limitation the rights to use, copy, modify, merge, publish,
17
+ # distribute, sublicense, and/or sell copies of the Software, and to
18
+ # permit persons to whom the Software is furnished to do so, subject to
19
+ # the following conditions:
20
+ #
21
+ # The above copyright notice and this permission notice shall be
22
+ # included in all copies or substantial portions of the Software.
23
+ #
24
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31
+ module ExceptionLoggable
32
+ def self.included(target)
33
+ target.extend(ClassMethods)
34
+ target.class_attribute :local_addresses, :exception_data
35
+
36
+ target.local_addresses = [IPAddr.new("127.0.0.1")]
37
+ end
38
+
39
+ module ClassMethods
40
+ def consider_local(*args)
41
+ local_addresses.concat(args.flatten.map { |a| IPAddr.new(a) })
42
+ end
43
+ end
44
+
45
+ def local_request?
46
+ remote = IPAddr.new(request.remote_ip)
47
+ !self.class.local_addresses.detect { |addr| addr.include?(remote) }.nil?
48
+ end
49
+
50
+ # we log the exception and raise it again, for the normal handling.
51
+ def log_exception_handler(exception)
52
+ log_exception(exception)
53
+ raise exception
54
+ end
55
+
56
+ def rescue_action(exception)
57
+ status = response_code_for_rescue(exception)
58
+ log_exception(exception) if status != :not_found
59
+ super
60
+ end
61
+
62
+ def filter_sub_parameters(params, rails_filter_parameters)
63
+ params.each do |key, value|
64
+ if(value.class != Hash and value.class != ActiveSupport::HashWithIndifferentAccess)
65
+ params[key] = '[FILTERED]' if rails_filter_parameters.include?(key.to_sym)
66
+ else
67
+ params[key] = filter_sub_parameters(value, rails_filter_parameters)
68
+ end
69
+ end
70
+ params
71
+ end
72
+
73
+ def log_exception(exception)
74
+ deliverer = self.class.exception_data
75
+ data = case deliverer
76
+ when nil then {}
77
+ when Symbol then send(deliverer)
78
+ when Proc then deliverer.call(self)
79
+ end
80
+
81
+ rails_filter_parameters = defined?(::Rails) ? ::Rails.application.config.filter_parameters : []
82
+
83
+ self.request.parameters.each do |key, value|
84
+ if(value.class != Hash and value.class != ActiveSupport::HashWithIndifferentAccess)
85
+ self.request.parameters[key] = '[FILTERED]' if rails_filter_parameters.include?(key.to_sym)
86
+ else
87
+ self.request.parameters[key] = filter_sub_parameters(value, rails_filter_parameters)
88
+ end
89
+ end if rails_filter_parameters.any?
90
+
91
+ LoggedException.create_from_exception(self, exception, data)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :faultline do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faultline-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Tamiru Hailu
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 8.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '8.0'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 8.0.0
32
+ - !ruby/object:Gem::Dependency
33
+ name: stimulus-rails
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.3'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: turbo-rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '2.0'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: will_paginate
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '4.0'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '4.0'
74
+ description: Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible
75
+ dashboard.
76
+ email:
77
+ - tamiruhailu@gmail.com
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - MIT-LICENSE
83
+ - README.md
84
+ - Rakefile
85
+ - app/assets/config/faultline_manifest.js
86
+ - app/assets/javascripts/faultline/application.js
87
+ - app/assets/stylesheets/faultline/application.css
88
+ - app/controllers/faultline/application_controller.rb
89
+ - app/controllers/faultline/logged_exceptions_controller.rb
90
+ - app/helpers/faultline/application_helper.rb
91
+ - app/helpers/faultline/logged_exceptions_helper.rb
92
+ - app/javascript/controllers/faultline_controller.js
93
+ - app/jobs/faultline/application_job.rb
94
+ - app/mailers/faultline/application_mailer.rb
95
+ - app/models/faultline/application_record.rb
96
+ - app/models/faultline/logged_exception.rb
97
+ - app/views/faultline/logged_exceptions/_exceptions.html.erb
98
+ - app/views/faultline/logged_exceptions/_feed.html.erb
99
+ - app/views/faultline/logged_exceptions/_filter_group.html.erb
100
+ - app/views/faultline/logged_exceptions/_show.html.erb
101
+ - app/views/faultline/logged_exceptions/clear.turbo_stream.erb
102
+ - app/views/faultline/logged_exceptions/destroy.turbo_stream.erb
103
+ - app/views/faultline/logged_exceptions/destroy_all.turbo_stream.erb
104
+ - app/views/faultline/logged_exceptions/feed.rss.builder
105
+ - app/views/faultline/logged_exceptions/index.html.erb
106
+ - app/views/faultline/logged_exceptions/query.html.erb
107
+ - app/views/faultline/logged_exceptions/query.turbo_stream.erb
108
+ - app/views/faultline/logged_exceptions/show.html.erb
109
+ - app/views/faultline/logged_exceptions/show.turbo_stream.erb
110
+ - app/views/layouts/faultline/application.html.erb
111
+ - config/initializers/date_formats.rb
112
+ - config/locales/en.yml
113
+ - config/routes.rb
114
+ - db/migrate/20240330122311_create_faultline_logged_exceptions.rb
115
+ - faultline-rails.gemspec
116
+ - lib/faultline-rails.rb
117
+ - lib/faultline.rb
118
+ - lib/faultline/engine.rb
119
+ - lib/faultline/version.rb
120
+ - lib/tasks/faultline_tasks.rake
121
+ homepage: https://github.com/tamiru/faultline-rails
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ allowed_push_host: https://rubygems.org
126
+ homepage_uri: https://github.com/tamiru/faultline-rails
127
+ source_code_uri: https://github.com/tamiru/faultline-rails/tree/main
128
+ changelog_uri: https://github.com/tamiru/faultline-rails/blob/main/CHANGELOG.md
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 3.2.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 4.0.10
144
+ specification_version: 4
145
+ summary: A Rails 8 exception dashboard powered by Hotwire.
146
+ test_files: []