faultline-rails 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +78 -11
- data/app/assets/stylesheets/faultline/application.css +51 -4
- data/app/controllers/faultline/logged_exceptions_controller.rb +29 -9
- data/app/helpers/faultline/logged_exceptions_helper.rb +15 -1
- data/app/views/faultline/logged_exceptions/_exceptions.html.erb +78 -27
- data/app/views/faultline/logged_exceptions/_filter_group.html.erb +5 -4
- data/app/views/faultline/logged_exceptions/_show.html.erb +69 -30
- data/app/views/faultline/logged_exceptions/index.html.erb +126 -44
- data/app/views/faultline/logged_exceptions/show.html.erb +1 -1
- data/app/views/layouts/faultline/application.html.erb +79 -13
- data/config/initializers/faultline.rb +18 -0
- data/faultline-rails.gemspec +4 -1
- data/lib/faultline/engine.rb +10 -0
- data/lib/faultline/ransack_config.rb +26 -0
- data/lib/faultline/version.rb +1 -1
- data/lib/generators/faultline/install_generator.rb +2 -1
- data/lib/generators/faultline/templates/faultline.rb +8 -0
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz: '
|
|
3
|
+
metadata.gz: effc17275c134ae8cff03dbcb56c9599ef226c5ef95cbeef465ce545355d583d
|
|
4
|
+
data.tar.gz: '086260474f100cf16be33eeaa478e0d7552106ae73b96fde52dc713ef6106c67'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e29aacc40843e06de209d838f83e52ce96b0e4f761b519b4668e27f31b7418fe7e10aedbf15b5fe79dc4c672a3255133d6aa01845f485e08f38ae4ec17ff4587
|
|
7
|
+
data.tar.gz: b8790e34a7620ed17e2960071558d92099fbf0f698c70521915088542ec97e0c4e3d72bbe6a3ea3bde625174ec779546d859035f8f91b99f6c98b5c74854e980
|
data/README.md
CHANGED
|
@@ -4,10 +4,11 @@ Faultline is a production-friendly exception dashboard for Rails 8. It records u
|
|
|
4
4
|
|
|
5
5
|
The dashboard is server-rendered and progressively enhanced with:
|
|
6
6
|
|
|
7
|
+
- **Tailwind CSS** layout that works with or without Tailwind installed.
|
|
8
|
+
- **Ransack** for advanced search and sortable columns.
|
|
7
9
|
- Turbo Frames for filtering and opening exception details without full-page navigation.
|
|
8
10
|
- Turbo Streams for deleting one, many, or all exceptions.
|
|
9
11
|
- Stimulus for loading state and small interaction behavior.
|
|
10
|
-
- Built-in CSS for a responsive dashboard UI that works out of the box.
|
|
11
12
|
|
|
12
13
|
## Requirements
|
|
13
14
|
|
|
@@ -36,6 +37,7 @@ This will:
|
|
|
36
37
|
1. Copy the database migration with proper indexes.
|
|
37
38
|
2. Create a configuration initializer at `config/initializers/faultline.rb`.
|
|
38
39
|
3. Mount the engine in your routes.
|
|
40
|
+
4. Add `rescue_from Exception, with: :log_exception_handler` to your `ApplicationController` for Rails 8 compatibility.
|
|
39
41
|
|
|
40
42
|
The dashboard is now available at `/faultline`.
|
|
41
43
|
|
|
@@ -54,6 +56,18 @@ end
|
|
|
54
56
|
|
|
55
57
|
Faultline logs the exception and then re-raises it so Rails keeps its normal error handling, status codes, and error pages.
|
|
56
58
|
|
|
59
|
+
### Rails 8+ compatibility
|
|
60
|
+
|
|
61
|
+
Faultline's initializer automatically adds `rescue_from Exception, with: :log_exception_handler` to your `ApplicationController`. This ensures exceptions are logged even when Rails' built-in `rescue_action` pattern is no longer used.
|
|
62
|
+
|
|
63
|
+
If you need to handle this manually:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class ApplicationController < ActionController::Base
|
|
67
|
+
rescue_from Exception, with: :log_exception_handler
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
57
71
|
## Protect the dashboard
|
|
58
72
|
|
|
59
73
|
The dashboard contains sensitive information, including request parameters, environment variables, and source paths. **Do not expose it to unauthenticated public users.**
|
|
@@ -100,10 +114,12 @@ end
|
|
|
100
114
|
You can also attach additional application data to each recorded exception:
|
|
101
115
|
|
|
102
116
|
```ruby
|
|
103
|
-
|
|
117
|
+
config.exception_data = lambda do |controller|
|
|
104
118
|
{
|
|
105
119
|
request_id: controller.request.request_id,
|
|
106
|
-
user_id: controller.current_user&.id
|
|
120
|
+
user_id: controller.current_user&.id,
|
|
121
|
+
user_email: controller.current_user&.email,
|
|
122
|
+
environment: Rails.env
|
|
107
123
|
}
|
|
108
124
|
end
|
|
109
125
|
```
|
|
@@ -120,6 +136,33 @@ end
|
|
|
120
136
|
|
|
121
137
|
Rails' `filter_parameters` configuration is respected before request parameters are stored.
|
|
122
138
|
|
|
139
|
+
## Search & Filter (Ransack)
|
|
140
|
+
|
|
141
|
+
Faultline includes [Ransack](https://github.com/activerecord-hackery/ransack) for advanced search and filtering. The dashboard provides:
|
|
142
|
+
|
|
143
|
+
- **Search form** — Search by exception class, controller name, and message text.
|
|
144
|
+
- **Sortable columns** — Click column headers to sort by exception class, controller, action, or date.
|
|
145
|
+
- **Sidebar filters** — Quick filter by exception class, controller/action, and time range (today, 3 days, 7 days, 30 days).
|
|
146
|
+
|
|
147
|
+
If your app doesn't include Ransack, Faultline falls back to the built-in sidebar filters automatically.
|
|
148
|
+
|
|
149
|
+
### Searching
|
|
150
|
+
|
|
151
|
+
Type in the search form fields and click "Search" to filter exceptions:
|
|
152
|
+
|
|
153
|
+
- **Exception Class** — Search by exception type (e.g., `RuntimeError`, `ActiveRecord::RecordNotFound`)
|
|
154
|
+
- **Controller** — Search by controller name (e.g., `users`, `posts`)
|
|
155
|
+
- **Message** — Full-text search across exception messages
|
|
156
|
+
|
|
157
|
+
### Sorting
|
|
158
|
+
|
|
159
|
+
Click any column header in the exceptions table to sort:
|
|
160
|
+
|
|
161
|
+
- **Exception** — Sort alphabetically by exception class
|
|
162
|
+
- **Controller** — Sort by controller name
|
|
163
|
+
- **Action** — Sort by action name
|
|
164
|
+
- **Date** — Sort by creation date (newest/oldest first)
|
|
165
|
+
|
|
123
166
|
## Frontend setup
|
|
124
167
|
|
|
125
168
|
### Hotwire (default)
|
|
@@ -137,7 +180,11 @@ bin/rails stimulus:install
|
|
|
137
180
|
|
|
138
181
|
Faultline ships with its own built-in CSS stylesheet that works out of the box. No Tailwind configuration is required.
|
|
139
182
|
|
|
140
|
-
|
|
183
|
+
#### Tailwind CSS
|
|
184
|
+
|
|
185
|
+
If your application uses Tailwind CSS, Faultline's views are already Tailwind-styled and will automatically use your Tailwind theme.
|
|
186
|
+
|
|
187
|
+
If you want to configure the gem's view directory as a Tailwind source, use the absolute path returned by Bundler:
|
|
141
188
|
|
|
142
189
|
```bash
|
|
143
190
|
bundle show faultline-rails
|
|
@@ -163,15 +210,35 @@ module.exports = {
|
|
|
163
210
|
}
|
|
164
211
|
```
|
|
165
212
|
|
|
213
|
+
#### Non-importmap projects
|
|
214
|
+
|
|
215
|
+
Faultline works with **importmap**, **sprockets**, **propshaft**, or **no JS pipeline** at all. The layout adapts to your asset pipeline:
|
|
216
|
+
|
|
217
|
+
- If `javascript_importmap_tags` is available, it uses importmap.
|
|
218
|
+
- If `turbo_refreshes_with` is available, it enables Turbo morph scrolling.
|
|
219
|
+
- If neither is available, the dashboard works without JavaScript.
|
|
220
|
+
|
|
221
|
+
### Bootstrap / Other CSS frameworks
|
|
222
|
+
|
|
223
|
+
Faultline's views use Tailwind CSS utility classes. To use Bootstrap or another framework:
|
|
224
|
+
|
|
225
|
+
1. Override the views by copying them to your app:
|
|
226
|
+
```bash
|
|
227
|
+
cp -r $(bundle show faultline-rails)/app/views/faultline app/views/faultline
|
|
228
|
+
```
|
|
229
|
+
2. Rewrite the Tailwind classes with your framework's classes.
|
|
230
|
+
3. The built-in CSS in `faultline/application.css` provides fallback styles.
|
|
231
|
+
|
|
166
232
|
## Dashboard features
|
|
167
233
|
|
|
168
|
-
- Search exception messages.
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
- Delete
|
|
173
|
-
-
|
|
174
|
-
-
|
|
234
|
+
- **Search** — Full-text search across exception messages, classes, and controllers.
|
|
235
|
+
- **Sortable columns** — Sort by exception class, controller, action, or date.
|
|
236
|
+
- **Filter** — Filter by exception class, controller/action, or age.
|
|
237
|
+
- **Exception details** — Open full exception details in a Turbo Frame.
|
|
238
|
+
- **Delete** — Delete individual exceptions without leaving the page.
|
|
239
|
+
- **Bulk delete** — Delete the currently visible result set.
|
|
240
|
+
- **Clear history** — Clear the complete exception history.
|
|
241
|
+
- **RSS feed** — Subscribe to `/faultline/logged_exceptions/feed.rss`.
|
|
175
242
|
|
|
176
243
|
## Data storage
|
|
177
244
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
/* Faultline Dashboard -
|
|
2
|
-
/*
|
|
1
|
+
/* Faultline Dashboard - Tailwind CSS with fallbacks */
|
|
2
|
+
/* If Tailwind is loaded, these utilities take precedence. */
|
|
3
|
+
/* If Tailwind is NOT loaded, these fallback styles provide the UI. */
|
|
4
|
+
|
|
5
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
6
|
+
FALLBACK STYLES (used when Tailwind CSS is NOT available)
|
|
7
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
3
8
|
|
|
4
9
|
:root {
|
|
5
10
|
--fl-primary: #4f46e5;
|
|
@@ -31,7 +36,7 @@ table { border-collapse: collapse; width: 100%; }
|
|
|
31
36
|
@media (max-width: 1024px) { .fl-grid { grid-template-columns: 1fr; } }
|
|
32
37
|
|
|
33
38
|
/* Card */
|
|
34
|
-
.fl-card { background: var(--fl-surface); border: 1px solid var(--fl-border); border-radius: var(--fl-radius); box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
|
39
|
+
.fl-card { background: var(--fl-surface); border: 1px solid var(--fl-border); border-radius: var(--fl-radius); box-shadow: 0 1px 3px rgba(0,0,0,0.06); padding: 1.25rem; }
|
|
35
40
|
|
|
36
41
|
/* Header */
|
|
37
42
|
.fl-header { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 2rem; }
|
|
@@ -48,7 +53,6 @@ table { border-collapse: collapse; width: 100%; }
|
|
|
48
53
|
.fl-nav a { display: block; padding: 0.5rem 0.75rem; border-radius: var(--fl-radius-sm); font-size: 0.875rem; color: var(--fl-text-secondary); }
|
|
49
54
|
.fl-nav a:hover { background: #f1f5f9; color: var(--fl-text); text-decoration: none; }
|
|
50
55
|
.fl-nav-scroll { max-height: 10rem; overflow-y: auto; }
|
|
51
|
-
.fl-sidebar hr { border: none; border-top: 1px solid var(--fl-border); margin: 1.5rem 0; padding-top: 1.25rem; }
|
|
52
56
|
|
|
53
57
|
/* Search */
|
|
54
58
|
.fl-search { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
|
|
@@ -119,3 +123,46 @@ table { border-collapse: collapse; width: 100%; }
|
|
|
119
123
|
/* Utility */
|
|
120
124
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
|
121
125
|
.sr-only-focusable:focus { position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; border: 0; }
|
|
126
|
+
|
|
127
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
128
|
+
TAILWIND OVERRIDES (used when Tailwind CSS IS available)
|
|
129
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
130
|
+
|
|
131
|
+
/* Tailwind-compatible pagination */
|
|
132
|
+
.fl-pagination .pagination {
|
|
133
|
+
@apply flex flex-wrap gap-2 items-center list-none m-0 p-0;
|
|
134
|
+
}
|
|
135
|
+
.fl-pagination .pagination a {
|
|
136
|
+
@apply px-3 py-1.5 rounded-md text-indigo-600 hover:bg-indigo-50 no-underline text-sm;
|
|
137
|
+
}
|
|
138
|
+
.fl-pagination .pagination span.current {
|
|
139
|
+
@apply px-3 py-1.5 rounded-md bg-indigo-600 text-white font-semibold text-sm;
|
|
140
|
+
}
|
|
141
|
+
.fl-pagination .pagination span.gap {
|
|
142
|
+
@apply px-2 py-1.5 text-gray-400 text-sm;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Tailwind-compatible search input */
|
|
146
|
+
.fl-search input[type="search"] {
|
|
147
|
+
@apply flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Tailwind-compatible buttons */
|
|
151
|
+
.fl-btn {
|
|
152
|
+
@apply inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md text-sm font-semibold cursor-pointer no-underline;
|
|
153
|
+
}
|
|
154
|
+
.fl-btn-primary {
|
|
155
|
+
@apply bg-indigo-600 text-white hover:bg-indigo-700;
|
|
156
|
+
}
|
|
157
|
+
.fl-btn-danger {
|
|
158
|
+
@apply bg-red-600 text-white hover:bg-red-700;
|
|
159
|
+
}
|
|
160
|
+
.fl-btn-outline {
|
|
161
|
+
@apply bg-transparent border border-gray-300 text-red-700 hover:bg-red-50;
|
|
162
|
+
}
|
|
163
|
+
.fl-btn-sm {
|
|
164
|
+
@apply px-3 py-1.5 text-xs;
|
|
165
|
+
}
|
|
166
|
+
.fl-btn-ghost {
|
|
167
|
+
@apply bg-transparent text-gray-500 hover:bg-gray-100;
|
|
168
|
+
}
|
|
@@ -9,11 +9,15 @@ module Faultline
|
|
|
9
9
|
def index
|
|
10
10
|
@exception_names = LoggedException.class_names
|
|
11
11
|
@controller_actions = LoggedException.controller_actions
|
|
12
|
-
@
|
|
12
|
+
@q = ransack_search
|
|
13
|
+
@exceptions = @q.result(distinct: true)
|
|
14
|
+
.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def query
|
|
16
|
-
@
|
|
18
|
+
@q = ransack_search
|
|
19
|
+
@exceptions = @q.result(distinct: true)
|
|
20
|
+
.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
|
|
17
21
|
|
|
18
22
|
respond_to do |format|
|
|
19
23
|
format.turbo_stream
|
|
@@ -55,7 +59,10 @@ module Faultline
|
|
|
55
59
|
filtered_scope
|
|
56
60
|
end
|
|
57
61
|
exceptions.delete_all
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
@q = ransack_search
|
|
64
|
+
@exceptions = @q.result(distinct: true)
|
|
65
|
+
.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
|
|
59
66
|
|
|
60
67
|
respond_to do |format|
|
|
61
68
|
format.turbo_stream
|
|
@@ -65,11 +72,14 @@ module Faultline
|
|
|
65
72
|
|
|
66
73
|
def clear
|
|
67
74
|
LoggedException.delete_all
|
|
68
|
-
|
|
75
|
+
|
|
76
|
+
@q = ransack_search
|
|
77
|
+
@exceptions = @q.result(distinct: true)
|
|
78
|
+
.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
|
|
69
79
|
|
|
70
80
|
respond_to do |format|
|
|
71
81
|
format.turbo_stream
|
|
72
|
-
format.html { redirect_back fallback_location:
|
|
82
|
+
format.html { redirect_back fallback_location: faultline_root_path }
|
|
73
83
|
end
|
|
74
84
|
end
|
|
75
85
|
|
|
@@ -82,8 +92,22 @@ module Faultline
|
|
|
82
92
|
head :forbidden
|
|
83
93
|
end
|
|
84
94
|
|
|
95
|
+
def faultline_root_path
|
|
96
|
+
main_app.respond_to?(:root_path) ? main_app.root_path : "/"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ransack_search
|
|
100
|
+
if defined?(Ransack)
|
|
101
|
+
LoggedException.ransack(params[:q])
|
|
102
|
+
else
|
|
103
|
+
scope = filtered_scope
|
|
104
|
+
Struct.new(:result).new(scope)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
85
108
|
def params_filters
|
|
86
109
|
{
|
|
110
|
+
q: params[:q],
|
|
87
111
|
query: params[:query],
|
|
88
112
|
date_ranges_filter: params[:date_ranges_filter],
|
|
89
113
|
exception_names_filter: params[:exception_names_filter],
|
|
@@ -91,10 +115,6 @@ module Faultline
|
|
|
91
115
|
}.compact
|
|
92
116
|
end
|
|
93
117
|
|
|
94
|
-
def filtered_exceptions
|
|
95
|
-
filtered_scope.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
118
|
def filtered_scope
|
|
99
119
|
exceptions = LoggedException.sorted
|
|
100
120
|
exceptions = exceptions.where(id: params[:id]) if params[:id].present?
|
|
@@ -11,7 +11,8 @@ module Faultline
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def filtered?
|
|
14
|
-
[:query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].any? { |p| params[p] }
|
|
14
|
+
[:query, :date_ranges_filter, :exception_names_filter, :controller_actions_filter].any? { |p| params[p] } ||
|
|
15
|
+
params[:q].present?
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def listify(text)
|
|
@@ -32,5 +33,18 @@ module Faultline
|
|
|
32
33
|
simple_format(text).html_safe
|
|
33
34
|
end
|
|
34
35
|
end
|
|
36
|
+
|
|
37
|
+
# Sort link helper for Ransack-compatible table headers.
|
|
38
|
+
# Falls back to plain link if Ransack is not available.
|
|
39
|
+
def sort_link(search, attribute, name = nil, **options, &block)
|
|
40
|
+
name ||= attribute.to_s.humanize
|
|
41
|
+
|
|
42
|
+
if defined?(Ransack) && search.respond_to?(:result)
|
|
43
|
+
# Delegate to Ransack's built-in sort_link helper
|
|
44
|
+
Ransack::Helpers::FormHelper.instance_method(:sort_link).bind(self).call(search, attribute, name, **options, &block)
|
|
45
|
+
else
|
|
46
|
+
link_to name, "#", **options, &block
|
|
47
|
+
end
|
|
48
|
+
end
|
|
35
49
|
end
|
|
36
50
|
end
|
|
@@ -1,45 +1,79 @@
|
|
|
1
|
-
<div id="exceptions-content" class="
|
|
2
|
-
|
|
1
|
+
<div id="exceptions-content" class="bg-white shadow rounded-lg overflow-hidden">
|
|
2
|
+
<%# Header %>
|
|
3
|
+
<div class="px-6 py-4 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
|
3
4
|
<div>
|
|
4
|
-
<h2>
|
|
5
|
+
<h2 class="text-lg font-medium text-gray-900">
|
|
5
6
|
<%= t(".heading") %>
|
|
6
7
|
<% if filtered? %>
|
|
7
|
-
<span
|
|
8
|
+
<span class="text-sm font-normal text-gray-500">(<%= t(".filtered") %>)</span>
|
|
8
9
|
<% end %>
|
|
9
10
|
</h2>
|
|
10
|
-
<p class="
|
|
11
|
+
<p class="mt-1 text-sm text-gray-500"><%= t(".count", count: @exceptions.total_entries) %></p>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="mt-4 sm:mt-0">
|
|
14
|
+
<%= link_to t(".delete_visible"), destroy_all_logged_exceptions_path(params_filters),
|
|
15
|
+
class: "inline-flex items-center px-3 py-2 border border-red-300 text-sm leading-4 font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
|
|
16
|
+
data: { turbo_method: :post, turbo_confirm: t(".confirm_delete_visible"), turbo_stream: true } %>
|
|
11
17
|
</div>
|
|
12
|
-
|
|
13
|
-
<%= link_to t(".delete_visible"), destroy_all_logged_exceptions_path(params_filters),
|
|
14
|
-
class: "fl-btn fl-btn-outline fl-btn-sm",
|
|
15
|
-
data: { turbo_method: :post, turbo_confirm: t(".confirm_delete_visible"), turbo_stream: true } %>
|
|
16
18
|
</div>
|
|
17
19
|
|
|
18
20
|
<% if @exceptions.empty? %>
|
|
19
|
-
<div class="
|
|
21
|
+
<div class="px-6 py-12 text-center">
|
|
22
|
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
23
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
24
|
+
</svg>
|
|
25
|
+
<p class="mt-2 text-sm text-gray-500"><%= t(".empty") %></p>
|
|
26
|
+
</div>
|
|
20
27
|
<% else %>
|
|
21
|
-
<div class="
|
|
22
|
-
<table class="
|
|
23
|
-
<thead>
|
|
28
|
+
<div class="overflow-x-auto">
|
|
29
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
30
|
+
<thead class="bg-gray-50">
|
|
24
31
|
<tr>
|
|
25
|
-
<th
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
33
|
+
<%= sort_link @q, :exception_class, "Exception", data: { turbo_frame: "exceptions" } %>
|
|
34
|
+
</th>
|
|
35
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
36
|
+
<%= sort_link @q, :controller_name, "Controller", data: { turbo_frame: "exceptions" } %>
|
|
37
|
+
</th>
|
|
38
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
39
|
+
<%= sort_link @q, :action_name, "Action", data: { turbo_frame: "exceptions" } %>
|
|
40
|
+
</th>
|
|
41
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
42
|
+
Message
|
|
43
|
+
</th>
|
|
44
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
45
|
+
<%= sort_link @q, :created_at, "Date", data: { turbo_frame: "exceptions" } %>
|
|
46
|
+
</th>
|
|
47
|
+
<th class="relative px-6 py-3">
|
|
48
|
+
<span class="sr-only"><%= t(".actions") %></span>
|
|
49
|
+
</th>
|
|
28
50
|
</tr>
|
|
29
51
|
</thead>
|
|
30
|
-
<tbody>
|
|
52
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
31
53
|
<% @exceptions.each do |exception| %>
|
|
32
|
-
<tr id="<%= dom_id(exception) %>">
|
|
33
|
-
<td>
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
<tr id="<%= dom_id(exception) %>" class="hover:bg-gray-50">
|
|
55
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
56
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
57
|
+
<%= exception.exception_class %>
|
|
58
|
+
</span>
|
|
59
|
+
</td>
|
|
60
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
61
|
+
<%= exception.controller_name %>
|
|
62
|
+
</td>
|
|
63
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
64
|
+
<%= exception.action_name %>
|
|
65
|
+
</td>
|
|
66
|
+
<td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">
|
|
67
|
+
<%= link_to exception.message&.truncate(100), logged_exception_path(exception),
|
|
68
|
+
class: "text-indigo-600 hover:text-indigo-900",
|
|
36
69
|
data: { turbo_frame: "exception-details" } %>
|
|
37
|
-
<p class="fl-msg"><%= exception.message %></p>
|
|
38
70
|
</td>
|
|
39
|
-
<td class="
|
|
40
|
-
|
|
71
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
72
|
+
<%= pretty_exception_date(exception) %>
|
|
73
|
+
</td>
|
|
74
|
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
41
75
|
<%= link_to t(".delete"), logged_exception_path(exception),
|
|
42
|
-
class: "
|
|
76
|
+
class: "text-red-600 hover:text-red-900",
|
|
43
77
|
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
|
|
44
78
|
</td>
|
|
45
79
|
</tr>
|
|
@@ -49,7 +83,24 @@
|
|
|
49
83
|
</div>
|
|
50
84
|
<% end %>
|
|
51
85
|
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
<%# Pagination %>
|
|
87
|
+
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
|
88
|
+
<div class="flex-1 flex justify-between sm:hidden">
|
|
89
|
+
<%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters),
|
|
90
|
+
inner_window: 1, outer_window: 0, previous_label: "Previous", next_label: "Next" %>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
|
93
|
+
<div>
|
|
94
|
+
<p class="text-sm text-gray-700">
|
|
95
|
+
Showing <span class="font-medium"><%= @exceptions.offset + 1 %></span>
|
|
96
|
+
to <span class="font-medium"><%= @exceptions.offset + @exceptions.length %></span>
|
|
97
|
+
of <span class="font-medium"><%= @exceptions.total_entries %></span> exceptions
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters),
|
|
102
|
+
inner_window: 2, outer_window: 1, previous_label: "«", next_label: "»" %>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
54
105
|
</div>
|
|
55
106
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<div>
|
|
2
|
-
<h3><%= title %></h3>
|
|
3
|
-
<nav class="
|
|
2
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider"><%= title %></h3>
|
|
3
|
+
<nav class="mt-2 space-y-1 max-h-48 overflow-y-auto">
|
|
4
4
|
<% values.each do |value| %>
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
<%= link_to value, query_logged_exceptions_path(parameter => value),
|
|
6
|
+
class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
|
7
|
+
data: { turbo_frame: "exceptions" } %>
|
|
7
8
|
<% end %>
|
|
8
9
|
</nav>
|
|
9
10
|
</div>
|
|
@@ -1,37 +1,76 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<%# Header %>
|
|
3
|
+
<div class="flex items-start justify-between">
|
|
4
|
+
<div>
|
|
5
|
+
<p class="text-sm text-gray-500"><%= @exception.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>
|
|
6
|
+
<h2 class="mt-1 text-xl font-semibold text-gray-900"><%= @exception.name %></h2>
|
|
7
|
+
<div class="mt-2 flex items-center space-x-2">
|
|
8
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
9
|
+
<%= @exception.exception_class %>
|
|
10
|
+
</span>
|
|
11
|
+
<span class="text-sm text-gray-500">•</span>
|
|
12
|
+
<span class="text-sm text-gray-500"><%= @exception.controller_name %>#<%= @exception.action_name %></span>
|
|
13
|
+
<span class="text-sm text-gray-500">•</span>
|
|
14
|
+
<span class="text-sm text-gray-500"><%= @exception.remote_ip %></span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<button type="button"
|
|
18
|
+
class="inline-flex items-center p-2 border border-transparent rounded-full shadow-sm text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
19
|
+
data-action="click->faultline#closeDetails">
|
|
20
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
21
|
+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
22
|
+
</svg>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<%# Request Info %>
|
|
27
|
+
<div class="bg-gray-50 rounded-lg p-4">
|
|
28
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Request</h3>
|
|
29
|
+
<pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= pretty_format(@exception.request) %></pre>
|
|
5
30
|
</div>
|
|
6
|
-
<button type="button" class="fl-btn fl-btn-ghost fl-btn-sm" data-action="click->faultline#closeDetails">
|
|
7
|
-
<%= t(".close") %>
|
|
8
|
-
</button>
|
|
9
|
-
</div>
|
|
10
31
|
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
<h3
|
|
14
|
-
<div class="
|
|
15
|
-
|
|
32
|
+
<%# Message %>
|
|
33
|
+
<div>
|
|
34
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Message</h3>
|
|
35
|
+
<div class="bg-gray-50 rounded-lg p-4">
|
|
36
|
+
<pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= @exception.message %></pre>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
40
|
+
<%# Backtrace %>
|
|
41
|
+
<div>
|
|
42
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Backtrace</h3>
|
|
43
|
+
<div class="bg-gray-900 rounded-lg p-4 overflow-auto max-h-96">
|
|
44
|
+
<pre class="text-sm text-gray-300 whitespace-pre-wrap font-mono leading-relaxed"><%= @exception.backtrace %></pre>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
21
47
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
48
|
+
<%# Environment %>
|
|
49
|
+
<div>
|
|
50
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Environment</h3>
|
|
51
|
+
<div class="bg-gray-50 rounded-lg p-4 overflow-auto max-h-64">
|
|
52
|
+
<pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= pretty_format(@exception.environment) %></pre>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
26
55
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<div
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
<%# User Info %>
|
|
57
|
+
<% if @exception.user_info.present? %>
|
|
58
|
+
<div>
|
|
59
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">User Info</h3>
|
|
60
|
+
<div class="bg-gray-50 rounded-lg p-4">
|
|
61
|
+
<pre class="text-sm text-gray-700 whitespace-pre-wrap font-mono"><%= pretty_format(@exception.user_info) %></pre>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
32
65
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class
|
|
36
|
-
|
|
66
|
+
<%# Actions %>
|
|
67
|
+
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
|
68
|
+
<p class="text-sm text-gray-500">
|
|
69
|
+
ID: <%= @exception.id %> •
|
|
70
|
+
Created: <%= @exception.created_at.iso8601 %>
|
|
71
|
+
</p>
|
|
72
|
+
<%= link_to t(".delete"), logged_exception_path(@exception),
|
|
73
|
+
class: "inline-flex items-center px-3 py-2 border border-red-300 text-sm leading-4 font-medium rounded-md text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
|
|
74
|
+
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
|
|
75
|
+
</div>
|
|
37
76
|
</div>
|
|
@@ -1,63 +1,145 @@
|
|
|
1
1
|
<% page_title t(".title") %>
|
|
2
2
|
|
|
3
|
-
<div data-controller="faultline"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<h1
|
|
8
|
-
|
|
3
|
+
<div class="space-y-6" data-controller="faultline">
|
|
4
|
+
<%# Header %>
|
|
5
|
+
<div class="md:flex md:items-center md:justify-between">
|
|
6
|
+
<div class="flex-1 min-w-0">
|
|
7
|
+
<h1 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
|
8
|
+
<%= t(".title") %>
|
|
9
|
+
</h1>
|
|
10
|
+
<p class="mt-1 text-sm text-gray-500"><%= t(".subtitle") %></p>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="mt-4 flex md:mt-0 md:ml-4">
|
|
13
|
+
<%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
|
|
14
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500",
|
|
15
|
+
form: { data: { turbo_stream: true } },
|
|
16
|
+
data: { turbo_confirm: t(".confirm_clear") } %>
|
|
9
17
|
</div>
|
|
10
|
-
<%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
|
|
11
|
-
class: "fl-btn fl-btn-danger",
|
|
12
|
-
form: { data: { turbo_stream: true } },
|
|
13
|
-
data: { turbo_confirm: t(".confirm_clear") } %>
|
|
14
18
|
</div>
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
<%# Ransack Search Form %>
|
|
21
|
+
<div class="bg-white shadow rounded-lg p-4">
|
|
22
|
+
<%= search_form_for @q, url: query_logged_exceptions_path, data: { turbo_frame: "exceptions" } do |f| %>
|
|
23
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
24
|
+
<div>
|
|
25
|
+
<%= f.label :exception_class_cont, "Exception Class", class: "block text-sm font-medium text-gray-700" %>
|
|
26
|
+
<%= f.search_field :exception_class_cont,
|
|
27
|
+
class: "mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
|
|
28
|
+
placeholder: "e.g. RuntimeError" %>
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<%= f.label :controller_name_cont, "Controller", class: "block text-sm font-medium text-gray-700" %>
|
|
32
|
+
<%= f.search_field :controller_name_cont,
|
|
33
|
+
class: "mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
|
|
34
|
+
placeholder: "e.g. users" %>
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<%= f.label :message_cont, "Message", class: "block text-sm font-medium text-gray-700" %>
|
|
38
|
+
<%= f.search_field :message_cont,
|
|
39
|
+
class: "mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm",
|
|
40
|
+
placeholder: "Search messages..." %>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="flex items-end">
|
|
43
|
+
<%= f.submit "Search", class: "w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
19
48
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
49
|
+
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
50
|
+
<%# Sidebar %>
|
|
51
|
+
<div class="lg:col-span-1">
|
|
52
|
+
<div class="bg-white shadow rounded-lg p-4 space-y-6">
|
|
53
|
+
<%# Exception Class Filter %>
|
|
54
|
+
<div>
|
|
55
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Exception Class</h3>
|
|
56
|
+
<nav class="mt-2 space-y-1 max-h-48 overflow-y-auto">
|
|
57
|
+
<% @exception_names.each do |name| %>
|
|
58
|
+
<%= link_to name,
|
|
59
|
+
query_logged_exceptions_path(exception_names_filter: name),
|
|
60
|
+
class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
|
61
|
+
data: { turbo_frame: "exceptions" } %>
|
|
62
|
+
<% end %>
|
|
63
|
+
</nav>
|
|
64
|
+
</div>
|
|
24
65
|
|
|
25
|
-
|
|
26
|
-
|
|
66
|
+
<%# Controller/Action Filter %>
|
|
67
|
+
<div>
|
|
68
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Controller</h3>
|
|
69
|
+
<nav class="mt-2 space-y-1 max-h-48 overflow-y-auto">
|
|
70
|
+
<% @controller_actions.each do |action| %>
|
|
71
|
+
<%= link_to action,
|
|
72
|
+
query_logged_exceptions_path(controller_actions_filter: action),
|
|
73
|
+
class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
|
74
|
+
data: { turbo_frame: "exceptions" } %>
|
|
75
|
+
<% end %>
|
|
76
|
+
</nav>
|
|
77
|
+
</div>
|
|
27
78
|
|
|
28
|
-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
79
|
+
<%# Date Filter %>
|
|
80
|
+
<div>
|
|
81
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Time Range</h3>
|
|
82
|
+
<nav class="mt-2 space-y-1">
|
|
83
|
+
<% [[t(".today"), 1], [t(".last_few_days"), 3], [t(".last_7_days"), 7], [t(".last_30_days"), 30]].each do |label, days| %>
|
|
84
|
+
<%= link_to label,
|
|
85
|
+
query_logged_exceptions_path(date_ranges_filter: days),
|
|
86
|
+
class: "block px-3 py-2 text-sm rounded-md text-gray-700 hover:bg-gray-100 hover:text-gray-900",
|
|
87
|
+
data: { turbo_frame: "exceptions" } %>
|
|
88
|
+
<% end %>
|
|
89
|
+
</nav>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<%# Text Search %>
|
|
93
|
+
<div>
|
|
94
|
+
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Quick Search</h3>
|
|
95
|
+
<%= form_with url: query_logged_exceptions_path, method: :get,
|
|
96
|
+
data: { turbo_frame: "exceptions", action: "submit->faultline#submit" } do |form| %>
|
|
97
|
+
<div class="mt-2 flex gap-2">
|
|
98
|
+
<%= form.search_field :query,
|
|
99
|
+
placeholder: "Search...",
|
|
100
|
+
class: "flex-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" %>
|
|
101
|
+
<%= form.submit "Go",
|
|
102
|
+
class: "inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
|
|
103
|
+
</div>
|
|
34
104
|
<% end %>
|
|
35
|
-
</
|
|
36
|
-
</div>
|
|
105
|
+
</div>
|
|
37
106
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<%= form.search_field :query, placeholder: t(".search_placeholder") %>
|
|
44
|
-
<%= form.submit t(".find"), class: "fl-btn fl-btn-primary fl-btn-sm" %>
|
|
45
|
-
</div>
|
|
46
|
-
<% end %>
|
|
107
|
+
<%# RSS Feed %>
|
|
108
|
+
<div class="pt-4 border-t border-gray-200">
|
|
109
|
+
<%= link_to "📡 RSS Feed", feed_logged_exceptions_path(format: :rss),
|
|
110
|
+
class: "text-sm text-gray-500 hover:text-indigo-600" %>
|
|
111
|
+
</div>
|
|
47
112
|
</div>
|
|
113
|
+
</div>
|
|
48
114
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
115
|
+
<%# Main Content %>
|
|
116
|
+
<div class="lg:col-span-3 space-y-6">
|
|
117
|
+
<%# Activity indicator %>
|
|
118
|
+
<div id="activity" data-faultline-target="activity" class="rounded-md bg-blue-50 p-4" role="status" aria-live="polite" hidden>
|
|
119
|
+
<div class="flex">
|
|
120
|
+
<div class="flex-shrink-0">
|
|
121
|
+
<svg class="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
122
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
123
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
124
|
+
</svg>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="ml-3">
|
|
127
|
+
<p class="text-sm font-medium text-blue-800"><%= t(".loading") %></p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
55
130
|
</div>
|
|
56
131
|
|
|
57
|
-
|
|
58
|
-
|
|
132
|
+
<%# Exception Details Panel %>
|
|
133
|
+
<%= turbo_frame_tag "exception-details", class: "bg-white shadow rounded-lg p-6" do %>
|
|
134
|
+
<div class="text-center py-12 text-gray-500">
|
|
135
|
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
136
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
137
|
+
</svg>
|
|
138
|
+
<p class="mt-2 text-sm"><%= t(".select_exception") %></p>
|
|
139
|
+
</div>
|
|
59
140
|
<% end %>
|
|
60
141
|
|
|
142
|
+
<%# Exceptions List %>
|
|
61
143
|
<%= turbo_frame_tag "exceptions" do %>
|
|
62
144
|
<%= render "exceptions" %>
|
|
63
145
|
<% end %>
|
|
@@ -1,17 +1,83 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
<html lang="en" class="h-full bg-gray-50">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= content_for?(:title) ? yield(:title) : "Faultline" %></title>
|
|
7
|
+
|
|
8
|
+
<%= stylesheet_link_tag "faultline/application", "data-turbo-track": "reload" %>
|
|
9
|
+
|
|
10
|
+
<% if respond_to?(:javascript_importmap_tags) %>
|
|
11
|
+
<%= javascript_importmap_tags %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% if respond_to?(:turbo_refreshes_with) %>
|
|
15
|
+
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= csrf_meta_tags %>
|
|
18
|
+
</head>
|
|
19
|
+
<body class="h-full">
|
|
20
|
+
<div class="min-h-full">
|
|
21
|
+
<nav class="bg-white shadow-sm border-b border-gray-200">
|
|
22
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
23
|
+
<div class="flex justify-between h-16">
|
|
24
|
+
<div class="flex items-center">
|
|
25
|
+
<div class="flex-shrink-0 flex items-center">
|
|
26
|
+
<span class="text-indigo-600 font-bold text-xl">⚡</span>
|
|
27
|
+
<span class="ml-2 text-lg font-semibold text-gray-900">Faultline</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="flex items-center space-x-4">
|
|
31
|
+
<% if main_app.respond_to?(:root_path) %>
|
|
32
|
+
<%= link_to "Dashboard", main_app.root_path, class: "text-gray-500 hover:text-gray-700 text-sm font-medium" %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
40
|
+
<% if notice.present? %>
|
|
41
|
+
<div class="mb-4 rounded-md bg-green-50 p-4">
|
|
42
|
+
<div class="flex">
|
|
43
|
+
<div class="flex-shrink-0">
|
|
44
|
+
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
|
45
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
|
46
|
+
</svg>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="ml-3">
|
|
49
|
+
<p class="text-sm font-medium text-green-800"><%= notice %></p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<% if alert.present? %>
|
|
56
|
+
<div class="mb-4 rounded-md bg-red-50 p-4">
|
|
57
|
+
<div class="flex">
|
|
58
|
+
<div class="flex-shrink-0">
|
|
59
|
+
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
60
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
61
|
+
</svg>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="ml-3">
|
|
64
|
+
<p class="text-sm font-medium text-red-800"><%= alert %></p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
14
70
|
<%= yield %>
|
|
15
71
|
</main>
|
|
16
|
-
|
|
72
|
+
|
|
73
|
+
<footer class="border-t border-gray-200 mt-8">
|
|
74
|
+
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
|
75
|
+
<p class="text-center text-xs text-gray-500">
|
|
76
|
+
Faultline v<%= Faultline::VERSION %> •
|
|
77
|
+
<%= link_to "RSS Feed", feed_logged_exceptions_path(format: :rss), class: "text-indigo-600 hover:text-indigo-500" %>
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</footer>
|
|
81
|
+
</div>
|
|
82
|
+
</body>
|
|
17
83
|
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rails.application.config.to_prepare do
|
|
4
|
+
Faultline.configure do |config|
|
|
5
|
+
# Protect the dashboard - only admin users can access
|
|
6
|
+
config.auth_block = lambda do |controller|
|
|
7
|
+
controller.current_user&.admin?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Attach additional data to each exception record
|
|
11
|
+
config.exception_data = lambda do |controller|
|
|
12
|
+
{
|
|
13
|
+
user_id: controller.current_user&.id,
|
|
14
|
+
request_id: controller.request.request_id
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/faultline-rails.gemspec
CHANGED
|
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.email = ["tamiruhailu@gmail.com"]
|
|
8
8
|
spec.homepage = "https://github.com/tamiru/faultline-rails"
|
|
9
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."
|
|
10
|
+
spec.description = "Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible dashboard with Ransack search."
|
|
11
11
|
spec.license = "MIT"
|
|
12
12
|
|
|
13
13
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
@@ -25,4 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.add_dependency "stimulus-rails", "~> 1.3"
|
|
26
26
|
spec.add_dependency "turbo-rails", "~> 2.0"
|
|
27
27
|
spec.add_dependency "will_paginate", "~> 4.0"
|
|
28
|
+
|
|
29
|
+
# Optional: Ransack provides advanced search/filter UI
|
|
30
|
+
spec.add_dependency "ransack", "~> 4.0"
|
|
28
31
|
end
|
data/lib/faultline/engine.rb
CHANGED
|
@@ -19,5 +19,15 @@ module Faultline
|
|
|
19
19
|
app.config.assets.paths << asset_root
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Configure Ransack for Faultline models
|
|
24
|
+
initializer "faultline.ransack" do
|
|
25
|
+
ActiveSupport.on_load(:active_record) do
|
|
26
|
+
if defined?(Ransack)
|
|
27
|
+
# Enable Ransack search on LoggedException
|
|
28
|
+
require "faultline/ransack_config"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
22
32
|
end
|
|
23
33
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Configure Ransack for Faultline LoggedException model.
|
|
4
|
+
# Ransack 4.x automatically adds `.ransack` to ActiveRecord::Relation via its railtie.
|
|
5
|
+
# We just need to declare which attributes are searchable.
|
|
6
|
+
module Faultline
|
|
7
|
+
module RansackConfig
|
|
8
|
+
def self.apply!
|
|
9
|
+
return unless defined?(Ransack)
|
|
10
|
+
|
|
11
|
+
Faultline::LoggedException.class_eval do
|
|
12
|
+
def self.ransackable_attributes(auth_object = nil)
|
|
13
|
+
%w[action_name backtrace controller_name created_at environment
|
|
14
|
+
exception_class id message remote_ip request
|
|
15
|
+
updated_at user_agent user_info]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.ransackable_associations(auth_object = nil)
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Faultline::RansackConfig.apply!
|
data/lib/faultline/version.rb
CHANGED
|
@@ -14,7 +14,8 @@ module Faultline
|
|
|
14
14
|
desc "Install Faultline: creates migration, initializer, and mounts routes."
|
|
15
15
|
|
|
16
16
|
def self.next_migration_number(path)
|
|
17
|
-
|
|
17
|
+
# Generate timestamp-based migration number for Rails 8.1+ compatibility
|
|
18
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def create_migration
|
|
@@ -39,3 +39,11 @@ Rails.application.config.to_prepare do
|
|
|
39
39
|
# config.per_page = 30
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Rails 8+ compatibility: Faultline's old rescue_action pattern doesn't work
|
|
44
|
+
# anymore. This registers rescue_from to log exceptions via log_exception_handler.
|
|
45
|
+
#
|
|
46
|
+
# Remove this block if you have your own global exception logging.
|
|
47
|
+
ApplicationController.class_eval do
|
|
48
|
+
rescue_from Exception, with: :log_exception_handler
|
|
49
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: faultline-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tamiru Hailu
|
|
@@ -71,8 +71,22 @@ dependencies:
|
|
|
71
71
|
- - "~>"
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '4.0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: ransack
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '4.0'
|
|
81
|
+
type: :runtime
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '4.0'
|
|
74
88
|
description: Faultline logs Rails exceptions and provides a Turbo, Stimulus, and Tailwind-compatible
|
|
75
|
-
dashboard.
|
|
89
|
+
dashboard with Ransack search.
|
|
76
90
|
email:
|
|
77
91
|
- tamiruhailu@gmail.com
|
|
78
92
|
executables: []
|
|
@@ -109,6 +123,7 @@ files:
|
|
|
109
123
|
- app/views/faultline/logged_exceptions/show.turbo_stream.erb
|
|
110
124
|
- app/views/layouts/faultline/application.html.erb
|
|
111
125
|
- config/initializers/date_formats.rb
|
|
126
|
+
- config/initializers/faultline.rb
|
|
112
127
|
- config/locales/en.yml
|
|
113
128
|
- config/routes.rb
|
|
114
129
|
- db/migrate/20240330122311_create_faultline_logged_exceptions.rb
|
|
@@ -117,6 +132,7 @@ files:
|
|
|
117
132
|
- lib/faultline.rb
|
|
118
133
|
- lib/faultline/configuration.rb
|
|
119
134
|
- lib/faultline/engine.rb
|
|
135
|
+
- lib/faultline/ransack_config.rb
|
|
120
136
|
- lib/faultline/version.rb
|
|
121
137
|
- lib/generators/faultline/install_generator.rb
|
|
122
138
|
- lib/generators/faultline/templates/faultline.rb
|
|
@@ -144,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
144
160
|
- !ruby/object:Gem::Version
|
|
145
161
|
version: '0'
|
|
146
162
|
requirements: []
|
|
147
|
-
rubygems_version: 4.0.
|
|
163
|
+
rubygems_version: 4.0.10
|
|
148
164
|
specification_version: 4
|
|
149
165
|
summary: A Rails 8 exception dashboard powered by Hotwire.
|
|
150
166
|
test_files: []
|