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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +179 -0
- data/Rakefile +8 -0
- data/app/assets/config/faultline_manifest.js +2 -0
- data/app/assets/javascripts/faultline/application.js +42 -0
- data/app/assets/stylesheets/faultline/application.css +33 -0
- data/app/controllers/faultline/application_controller.rb +4 -0
- data/app/controllers/faultline/logged_exceptions_controller.rb +105 -0
- data/app/helpers/faultline/application_helper.rb +4 -0
- data/app/helpers/faultline/logged_exceptions_helper.rb +34 -0
- data/app/javascript/controllers/faultline_controller.js +38 -0
- data/app/jobs/faultline/application_job.rb +4 -0
- data/app/mailers/faultline/application_mailer.rb +6 -0
- data/app/models/faultline/application_record.rb +5 -0
- data/app/models/faultline/logged_exception.rb +88 -0
- data/app/views/faultline/logged_exceptions/_exceptions.html.erb +55 -0
- data/app/views/faultline/logged_exceptions/_feed.html.erb +5 -0
- data/app/views/faultline/logged_exceptions/_filter_group.html.erb +10 -0
- data/app/views/faultline/logged_exceptions/_show.html.erb +37 -0
- data/app/views/faultline/logged_exceptions/clear.turbo_stream.erb +6 -0
- data/app/views/faultline/logged_exceptions/destroy.turbo_stream.erb +2 -0
- data/app/views/faultline/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
- data/app/views/faultline/logged_exceptions/feed.rss.builder +20 -0
- data/app/views/faultline/logged_exceptions/index.html.erb +68 -0
- data/app/views/faultline/logged_exceptions/query.html.erb +3 -0
- data/app/views/faultline/logged_exceptions/query.turbo_stream.erb +6 -0
- data/app/views/faultline/logged_exceptions/show.html.erb +3 -0
- data/app/views/faultline/logged_exceptions/show.turbo_stream.erb +5 -0
- data/app/views/layouts/faultline/application.html.erb +30 -0
- data/config/initializers/date_formats.rb +5 -0
- data/config/locales/en.yml +47 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20240330122311_create_faultline_logged_exceptions.rb +18 -0
- data/faultline-rails.gemspec +28 -0
- data/lib/faultline/engine.rb +21 -0
- data/lib/faultline/version.rb +3 -0
- data/lib/faultline-rails.rb +1 -0
- data/lib/faultline.rb +94 -0
- data/lib/tasks/faultline_tasks.rake +4 -0
- 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,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,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,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,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,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,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,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,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,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 @@
|
|
|
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
|
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: []
|