faultline-rails 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +77 -62
- data/app/assets/stylesheets/faultline/application.css +116 -28
- data/app/controllers/faultline/logged_exceptions_controller.rb +11 -2
- data/app/helpers/faultline/logged_exceptions_helper.rb +3 -1
- data/app/models/faultline/logged_exception.rb +17 -8
- data/app/views/faultline/logged_exceptions/_exceptions.html.erb +22 -22
- data/app/views/faultline/logged_exceptions/_feed.html.erb +2 -4
- data/app/views/faultline/logged_exceptions/_filter_group.html.erb +5 -6
- data/app/views/faultline/logged_exceptions/_show.html.erb +19 -19
- data/app/views/faultline/logged_exceptions/feed.rss.builder +1 -1
- data/app/views/faultline/logged_exceptions/index.html.erb +26 -28
- data/app/views/layouts/faultline/application.html.erb +0 -13
- data/db/migrate/20240330122311_create_faultline_logged_exceptions.rb +4 -0
- data/lib/faultline/configuration.rb +33 -0
- data/lib/faultline/engine.rb +2 -0
- data/lib/faultline/version.rb +1 -1
- data/lib/faultline.rb +29 -1
- data/lib/generators/faultline/install_generator.rb +62 -0
- data/lib/generators/faultline/templates/faultline.rb +41 -0
- data/lib/generators/faultline/templates/migration.rb +24 -0
- data/lib/tasks/faultline_tasks.rake +37 -4
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1f99904f339aa70945293928829f5ecb86d755a41203d9ad70dbc681e2b5213
|
|
4
|
+
data.tar.gz: '09d8929d8fc7823186707312eccb2be742e38c0e13167f6a62cdefa801e5d4f1'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef04fc828fda367fd3eca26f1e776163846397f610835497bfd6177a72c862a51270d2ef1e6d1c5bf24a33f4ee115d8ac240ebe8582fb0ccfb3c2fab6b83cc0c
|
|
7
|
+
data.tar.gz: 8967bf842e86fbc7aa90d91d8e8988dc9e8a982ba4a1393ff309f268ce4287f570796857638d0fdc3102fc3c984a8e4717fc728ba9c2f5cbead2f8195df50efa
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@ The dashboard is server-rendered and progressively enhanced with:
|
|
|
7
7
|
- Turbo Frames for filtering and opening exception details without full-page navigation.
|
|
8
8
|
- Turbo Streams for deleting one, many, or all exceptions.
|
|
9
9
|
- Stimulus for loading state and small interaction behavior.
|
|
10
|
-
-
|
|
10
|
+
- Built-in CSS for a responsive dashboard UI that works out of the box.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
@@ -24,25 +24,23 @@ Add Faultline to your application:
|
|
|
24
24
|
gem "faultline-rails"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Run the install generator to set up everything in one step:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
bundle install
|
|
31
|
-
bin/rails
|
|
31
|
+
bin/rails generate faultline:install
|
|
32
32
|
bin/rails db:migrate
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Rails.application.routes.draw do
|
|
40
|
-
mount Faultline::Engine => "/faultline"
|
|
41
|
-
end
|
|
42
|
-
```
|
|
35
|
+
This will:
|
|
36
|
+
1. Copy the database migration with proper indexes.
|
|
37
|
+
2. Create a configuration initializer at `config/initializers/faultline.rb`.
|
|
38
|
+
3. Mount the engine in your routes.
|
|
43
39
|
|
|
44
40
|
The dashboard is now available at `/faultline`.
|
|
45
41
|
|
|
42
|
+
> **Note:** The generator mounts the engine at `/faultline` by default. You can change the mount path by editing `config/routes.rb`.
|
|
43
|
+
|
|
46
44
|
## Start logging exceptions
|
|
47
45
|
|
|
48
46
|
Include `Faultline::ExceptionLoggable` in your application controller:
|
|
@@ -58,41 +56,94 @@ Faultline logs the exception and then re-raises it so Rails keeps its normal err
|
|
|
58
56
|
|
|
59
57
|
## Protect the dashboard
|
|
60
58
|
|
|
61
|
-
The dashboard contains sensitive information, including request parameters, environment variables, and source paths. Do not expose it to unauthenticated public users
|
|
59
|
+
The dashboard contains sensitive information, including request parameters, environment variables, and source paths. **Do not expose it to unauthenticated public users.**
|
|
60
|
+
|
|
61
|
+
By default, the dashboard returns `403 Forbidden` for all requests. Configure authentication in your initializer:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# config/initializers/faultline.rb
|
|
65
|
+
Rails.application.config.to_prepare do
|
|
66
|
+
Faultline.configure do |config|
|
|
67
|
+
config.auth_block = lambda do |controller|
|
|
68
|
+
# Return true if the user is authorized to view the dashboard.
|
|
69
|
+
# Examples:
|
|
70
|
+
controller.authenticate_user! # Devise
|
|
71
|
+
# controller.current_user&.admin? # Custom auth
|
|
72
|
+
# false # Block everyone (default)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
Configure Faultline through the block-style DSL in your initializer:
|
|
64
81
|
|
|
65
82
|
```ruby
|
|
66
83
|
# config/initializers/faultline.rb
|
|
67
84
|
Rails.application.config.to_prepare do
|
|
68
|
-
Faultline
|
|
85
|
+
Faultline.configure do |config|
|
|
86
|
+
# Dashboard title
|
|
87
|
+
config.application_name = "Acme"
|
|
88
|
+
|
|
89
|
+
# Items per page (default: 30)
|
|
90
|
+
config.per_page = 50
|
|
91
|
+
|
|
92
|
+
# Authentication block (see "Protect the dashboard" above)
|
|
93
|
+
config.auth_block = lambda do |controller|
|
|
94
|
+
controller.current_user&.admin?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
69
97
|
end
|
|
70
98
|
```
|
|
71
99
|
|
|
72
|
-
|
|
100
|
+
You can also attach additional application data to each recorded exception:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
ApplicationController.exception_data = lambda do |controller|
|
|
104
|
+
{
|
|
105
|
+
request_id: controller.request.request_id,
|
|
106
|
+
user_id: controller.current_user&.id
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Exclude trusted private networks from the dashboard's local-request handling:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class ApplicationController < ActionController::Base
|
|
115
|
+
include Faultline::ExceptionLoggable
|
|
116
|
+
|
|
117
|
+
consider_local "10.0.0.0/8", "192.168.0.0/16"
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Rails' `filter_parameters` configuration is respected before request parameters are stored.
|
|
122
|
+
|
|
123
|
+
## Frontend setup
|
|
73
124
|
|
|
74
|
-
|
|
125
|
+
### Hotwire (default)
|
|
75
126
|
|
|
76
|
-
Faultline includes `turbo-rails` and `stimulus-rails` as dependencies.
|
|
127
|
+
Faultline includes `turbo-rails` and `stimulus-rails` as dependencies. If your Rails app already has Hotwire installed (the default for Rails 8), no extra setup is needed.
|
|
128
|
+
|
|
129
|
+
If you need to install Hotwire:
|
|
77
130
|
|
|
78
131
|
```bash
|
|
79
132
|
bin/rails turbo:install
|
|
80
133
|
bin/rails stimulus:install
|
|
81
134
|
```
|
|
82
135
|
|
|
83
|
-
|
|
136
|
+
### Styling
|
|
84
137
|
|
|
85
|
-
|
|
86
|
-
import FaultlineController from "./faultline_controller"
|
|
138
|
+
Faultline ships with its own built-in CSS stylesheet that works out of the box. No Tailwind configuration is required.
|
|
87
139
|
|
|
88
|
-
application
|
|
89
|
-
```
|
|
140
|
+
If your application uses Tailwind CSS and you want Faultline to use your Tailwind theme instead, you can configure the gem's view directory as a Tailwind source. Use the absolute path returned by Bundler:
|
|
90
141
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
142
|
+
```bash
|
|
143
|
+
bundle show faultline-rails
|
|
144
|
+
```
|
|
94
145
|
|
|
95
|
-
For Tailwind CSS v4, add a source entry
|
|
146
|
+
For Tailwind CSS v4, add a source entry:
|
|
96
147
|
|
|
97
148
|
```css
|
|
98
149
|
@import "tailwindcss";
|
|
@@ -112,42 +163,6 @@ module.exports = {
|
|
|
112
163
|
}
|
|
113
164
|
```
|
|
114
165
|
|
|
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
166
|
## Dashboard features
|
|
152
167
|
|
|
153
168
|
- Search exception messages.
|
|
@@ -1,33 +1,121 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
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
|
-
}
|
|
1
|
+
/* Faultline Dashboard - Self-contained styles */
|
|
2
|
+
/* Works without Tailwind CSS. If you have Tailwind, these serve as fallbacks. */
|
|
14
3
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
4
|
+
:root {
|
|
5
|
+
--fl-primary: #4f46e5;
|
|
6
|
+
--fl-primary-hover: #4338ca;
|
|
7
|
+
--fl-bg: #f8fafc;
|
|
8
|
+
--fl-surface: #ffffff;
|
|
9
|
+
--fl-border: #e2e8f0;
|
|
10
|
+
--fl-text: #0f172a;
|
|
11
|
+
--fl-text-secondary: #64748b;
|
|
12
|
+
--fl-text-muted: #94a3b8;
|
|
13
|
+
--fl-danger: #dc2626;
|
|
14
|
+
--fl-danger-hover: #b91c1c;
|
|
15
|
+
--fl-danger-bg: #fef2f2;
|
|
16
|
+
--fl-radius: 0.75rem;
|
|
17
|
+
--fl-radius-sm: 0.5rem;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
20
|
+
/* Base reset */
|
|
21
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
22
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.5; color: var(--fl-text); background: var(--fl-bg); }
|
|
23
|
+
a { color: var(--fl-primary); text-decoration: none; }
|
|
24
|
+
a:hover { text-decoration: underline; }
|
|
25
|
+
h1, h2, h3, h4, p { margin: 0; }
|
|
26
|
+
table { border-collapse: collapse; width: 100%; }
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
+
/* Layout */
|
|
29
|
+
.fl-container { max-width: 80rem; margin: 0 auto; padding: 2rem 1rem; }
|
|
30
|
+
.fl-grid { display: grid; gap: 1.5rem; grid-template-columns: 16rem minmax(0, 1fr); }
|
|
31
|
+
@media (max-width: 1024px) { .fl-grid { grid-template-columns: 1fr; } }
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
33
|
+
/* 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); }
|
|
35
|
+
|
|
36
|
+
/* Header */
|
|
37
|
+
.fl-header { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 2rem; }
|
|
38
|
+
.fl-header h1 { font-size: 1.875rem; font-weight: 700; letter-spacing: -0.025em; margin-top: 0.5rem; }
|
|
39
|
+
.fl-brand { font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--fl-primary); }
|
|
40
|
+
.fl-subtitle { color: var(--fl-text-secondary); margin-top: 0.5rem; }
|
|
41
|
+
|
|
42
|
+
/* Sidebar */
|
|
43
|
+
.fl-sidebar { padding: 1.25rem; }
|
|
44
|
+
.fl-sidebar h2 { font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); }
|
|
45
|
+
.fl-sidebar h3 { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); margin-top: 1.5rem; }
|
|
46
|
+
.fl-nav { list-style: none; padding: 0; margin: 0.5rem 0 0; }
|
|
47
|
+
.fl-nav li { margin: 0.25rem 0; }
|
|
48
|
+
.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
|
+
.fl-nav a:hover { background: #f1f5f9; color: var(--fl-text); text-decoration: none; }
|
|
50
|
+
.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
|
+
|
|
53
|
+
/* Search */
|
|
54
|
+
.fl-search { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
|
|
55
|
+
.fl-search input[type="search"] { flex: 1; min-width: 0; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: var(--fl-radius-sm); font-size: 0.875rem; }
|
|
56
|
+
.fl-search input[type="search"]:focus { outline: none; border-color: var(--fl-primary); box-shadow: 0 0 0 2px rgba(79,70,229,0.2); }
|
|
57
|
+
|
|
58
|
+
/* Buttons */
|
|
59
|
+
.fl-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.5rem 1rem; border: none; border-radius: var(--fl-radius-sm); font-size: 0.875rem; font-weight: 600; cursor: pointer; text-decoration: none; }
|
|
60
|
+
.fl-btn-primary { background: var(--fl-primary); color: white; }
|
|
61
|
+
.fl-btn-primary:hover { background: var(--fl-primary-hover); text-decoration: none; }
|
|
62
|
+
.fl-btn-danger { background: var(--fl-danger); color: white; }
|
|
63
|
+
.fl-btn-danger:hover { background: var(--fl-danger-hover); text-decoration: none; }
|
|
64
|
+
.fl-btn-outline { background: transparent; border: 1px solid #e2e8f0; color: #b91c1c; }
|
|
65
|
+
.fl-btn-outline:hover { background: var(--fl-danger-bg); text-decoration: none; }
|
|
66
|
+
.fl-btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
|
|
67
|
+
.fl-btn-ghost { background: transparent; color: var(--fl-text-secondary); }
|
|
68
|
+
.fl-btn-ghost:hover { background: #f1f5f9; text-decoration: none; }
|
|
69
|
+
|
|
70
|
+
/* Table */
|
|
71
|
+
.fl-table-wrap { overflow-x: auto; }
|
|
72
|
+
.fl-table { min-width: 100%; }
|
|
73
|
+
.fl-table thead { background: #f8fafc; }
|
|
74
|
+
.fl-table th { padding: 0.75rem 1.25rem; text-align: left; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); }
|
|
75
|
+
.fl-table td { padding: 1rem 1.25rem; vertical-align: top; border-top: 1px solid #f1f5f9; }
|
|
76
|
+
.fl-table tr:hover { background: #f8fafc; }
|
|
77
|
+
.fl-table .fl-name { font-weight: 500; color: var(--fl-primary); }
|
|
78
|
+
.fl-table .fl-name:hover { text-decoration: underline; }
|
|
79
|
+
.fl-table .fl-msg { margin-top: 0.25rem; font-size: 0.875rem; color: var(--fl-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40rem; }
|
|
80
|
+
.fl-table .fl-date { font-size: 0.875rem; color: var(--fl-text-secondary); white-space: nowrap; }
|
|
81
|
+
|
|
82
|
+
/* Actions */
|
|
83
|
+
.fl-actions { text-align: right; white-space: nowrap; }
|
|
84
|
+
.fl-delete-link { font-size: 0.875rem; font-weight: 500; color: var(--fl-danger); }
|
|
85
|
+
.fl-delete-link:hover { color: var(--fl-danger-hover); text-decoration: underline; }
|
|
86
|
+
|
|
87
|
+
/* Detail panel */
|
|
88
|
+
.fl-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--fl-border); }
|
|
89
|
+
.fl-detail-header h2 { font-size: 1.25rem; font-weight: 600; word-break: break-word; margin-top: 0.25rem; }
|
|
90
|
+
.fl-detail-time { font-size: 0.875rem; color: var(--fl-text-secondary); }
|
|
91
|
+
.fl-detail-body { margin-top: 1.25rem; }
|
|
92
|
+
.fl-detail-section { margin-bottom: 1.25rem; }
|
|
93
|
+
.fl-detail-section h3 { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fl-text-secondary); margin-bottom: 0.5rem; }
|
|
94
|
+
.fl-detail-code { background: #f8fafc; padding: 1rem; border-radius: var(--fl-radius-sm); overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-size: 0.875rem; }
|
|
95
|
+
.fl-detail-pre { background: #0f172a; color: #e2e8f0; padding: 1rem; border-radius: var(--fl-radius-sm); overflow: auto; max-height: 24rem; font-size: 0.75rem; line-height: 1.625; }
|
|
96
|
+
.fl-detail-footer { margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--fl-border); }
|
|
97
|
+
|
|
98
|
+
/* Empty state */
|
|
99
|
+
.fl-empty { padding: 4rem 1.25rem; text-align: center; color: var(--fl-text-secondary); font-size: 0.875rem; }
|
|
100
|
+
|
|
101
|
+
/* Status bar */
|
|
102
|
+
.fl-status { background: #eef2ff; color: #4338ca; padding: 0.75rem 1rem; border-radius: var(--fl-radius-sm); font-size: 0.875rem; margin-bottom: 1rem; }
|
|
103
|
+
.fl-status[hidden] { display: none; }
|
|
104
|
+
|
|
105
|
+
/* Pagination */
|
|
106
|
+
.fl-pagination { border-top: 1px solid var(--fl-border); padding: 1rem 1.25rem; font-size: 0.875rem; }
|
|
107
|
+
.fl-pagination .pagination { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; list-style: none; margin: 0; padding: 0; }
|
|
108
|
+
.fl-pagination .pagination a,
|
|
109
|
+
.fl-pagination .pagination span { padding: 0.375rem 0.75rem; border-radius: var(--fl-radius-sm); }
|
|
110
|
+
.fl-pagination .pagination a { color: var(--fl-primary); }
|
|
111
|
+
.fl-pagination .pagination a:hover { background: #eef2ff; text-decoration: none; }
|
|
112
|
+
.fl-pagination .pagination .current { background: var(--fl-primary); color: white; font-weight: 600; }
|
|
113
|
+
|
|
114
|
+
/* Feed link */
|
|
115
|
+
.fl-feed { margin-top: 1rem; }
|
|
116
|
+
.fl-feed a { font-size: 0.8125rem; color: var(--fl-text-muted); }
|
|
117
|
+
.fl-feed a:hover { color: var(--fl-primary); }
|
|
118
|
+
|
|
119
|
+
/* Utility */
|
|
120
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
|
121
|
+
.sr-only-focusable:focus { position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; border: 0; }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Faultline
|
|
2
4
|
class LoggedExceptionsController < ApplicationController
|
|
3
|
-
|
|
5
|
+
before_action :faultline_require_auth!
|
|
4
6
|
|
|
5
7
|
helper_method :params_filters
|
|
6
8
|
|
|
@@ -73,6 +75,13 @@ module Faultline
|
|
|
73
75
|
|
|
74
76
|
private
|
|
75
77
|
|
|
78
|
+
def faultline_require_auth!
|
|
79
|
+
auth_block = Faultline.configuration.auth_block
|
|
80
|
+
return if auth_block&.call(self)
|
|
81
|
+
|
|
82
|
+
head :forbidden
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
def params_filters
|
|
77
86
|
{
|
|
78
87
|
query: params[:query],
|
|
@@ -83,7 +92,7 @@ module Faultline
|
|
|
83
92
|
end
|
|
84
93
|
|
|
85
94
|
def filtered_exceptions
|
|
86
|
-
filtered_scope.paginate(page: params[:page], per_page: 30)
|
|
95
|
+
filtered_scope.paginate(page: params[:page], per_page: Faultline.configuration.per_page || 30)
|
|
87
96
|
end
|
|
88
97
|
|
|
89
98
|
def filtered_scope
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Faultline
|
|
2
4
|
module LoggedExceptionsHelper
|
|
3
5
|
def pretty_exception_date(exception)
|
|
@@ -18,7 +20,7 @@ module Faultline
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def page_title(text)
|
|
21
|
-
title = [
|
|
23
|
+
title = [Faultline.application_name.presence, text].compact.join(" :: ")
|
|
22
24
|
content_for(:title, title)
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Faultline
|
|
2
4
|
class LoggedException < ApplicationRecord
|
|
3
5
|
self.table_name = "faultline_logged_exceptions"
|
|
@@ -5,7 +7,8 @@ module Faultline
|
|
|
5
7
|
|
|
6
8
|
class << self
|
|
7
9
|
def create_from_exception(controller, exception, data)
|
|
8
|
-
message =
|
|
10
|
+
message = exception.message.to_s
|
|
11
|
+
message += "\n* Extra Data\n\n#{data}" unless data.blank?
|
|
9
12
|
create!(
|
|
10
13
|
exception_class: exception.class.name,
|
|
11
14
|
controller_name: controller.controller_path,
|
|
@@ -36,7 +39,7 @@ module Faultline
|
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
def backtrace=(trace)
|
|
39
|
-
trace = sanitize_backtrace(trace)
|
|
42
|
+
trace = sanitize_backtrace(trace) unless trace.is_a?(String)
|
|
40
43
|
write_attribute :backtrace, trace
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -45,17 +48,18 @@ module Faultline
|
|
|
45
48
|
write_attribute :request, request
|
|
46
49
|
else
|
|
47
50
|
max = request.env.keys.max { |a, b| a.length <=> b.length }
|
|
48
|
-
env = request.env.keys.sort.inject [] do |
|
|
49
|
-
|
|
51
|
+
env = request.env.keys.sort.inject [] do |memo, key|
|
|
52
|
+
memo << "* %-*s: %s" % [max.length, key, request.env[key].to_s.strip]
|
|
50
53
|
end
|
|
51
|
-
write_attribute(:environment, (env << "* Process: #{$$}" << "* Server : #{self.class.host_name}")
|
|
54
|
+
write_attribute(:environment, (env << "* Process: #{$$}" << "* Server : #{self.class.host_name}").join("\n"))
|
|
52
55
|
|
|
56
|
+
method_str = request.get? ? "" : " #{request.method.to_s.upcase}"
|
|
53
57
|
write_attribute(:request, [
|
|
54
|
-
"* URL:#{
|
|
58
|
+
"* URL:#{method_str} #{request.protocol}#{request.env["HTTP_HOST"]}#{request.fullpath}",
|
|
55
59
|
"* Format: #{request.format.to_s}",
|
|
56
60
|
"* Parameters: #{request.parameters.inspect}",
|
|
57
61
|
"* Rails Root: #{rails_root}"
|
|
58
|
-
]
|
|
62
|
+
].join("\n"))
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -77,8 +81,13 @@ module Faultline
|
|
|
77
81
|
@@backtrace_regex = /^#{Regexp.escape(@@rails_root)}/
|
|
78
82
|
|
|
79
83
|
def sanitize_backtrace(trace)
|
|
84
|
+
return "" if trace.nil?
|
|
85
|
+
return trace unless trace.respond_to?(:reject)
|
|
86
|
+
|
|
80
87
|
gem_path = Bundler.bundle_path.to_s
|
|
81
|
-
trace.reject { |line| line.include?(gem_path) }
|
|
88
|
+
trace.reject { |line| line.include?(gem_path) }
|
|
89
|
+
.collect { |line| Pathname.new(line.gsub(@@backtrace_regex, "[RAILS_ROOT]")).cleanpath.to_s }
|
|
90
|
+
.join("\n")
|
|
82
91
|
end
|
|
83
92
|
|
|
84
93
|
def rails_root
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
<div id="exceptions-content" class="
|
|
2
|
-
<div class="
|
|
1
|
+
<div id="exceptions-content" class="fl-card">
|
|
2
|
+
<div class="fl-detail-header">
|
|
3
3
|
<div>
|
|
4
|
-
<h2
|
|
4
|
+
<h2>
|
|
5
5
|
<%= t(".heading") %>
|
|
6
6
|
<% if filtered? %>
|
|
7
|
-
<span
|
|
7
|
+
<span style="font-weight: 400; color: var(--fl-text-secondary);">(<%= t(".filtered") %>)</span>
|
|
8
8
|
<% end %>
|
|
9
9
|
</h2>
|
|
10
|
-
<p class="
|
|
10
|
+
<p class="fl-detail-time" style="margin-top: 0.25rem;"><%= t(".count", count: @exceptions.total_entries) %></p>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
<%= link_to t(".delete_visible"), destroy_all_logged_exceptions_path(params_filters),
|
|
14
|
-
class: "
|
|
14
|
+
class: "fl-btn fl-btn-outline fl-btn-sm",
|
|
15
15
|
data: { turbo_method: :post, turbo_confirm: t(".confirm_delete_visible"), turbo_stream: true } %>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<% if @exceptions.empty? %>
|
|
19
|
-
<div class="
|
|
19
|
+
<div class="fl-empty"><%= t(".empty") %></div>
|
|
20
20
|
<% else %>
|
|
21
|
-
<div class="
|
|
22
|
-
<table class="
|
|
23
|
-
<thead
|
|
21
|
+
<div class="fl-table-wrap">
|
|
22
|
+
<table class="fl-table">
|
|
23
|
+
<thead>
|
|
24
24
|
<tr>
|
|
25
|
-
<th
|
|
26
|
-
<th
|
|
27
|
-
<th
|
|
25
|
+
<th><%= t(".exception") %></th>
|
|
26
|
+
<th><%= t(".date") %></th>
|
|
27
|
+
<th><span class="sr-only"><%= t(".actions") %></span></th>
|
|
28
28
|
</tr>
|
|
29
29
|
</thead>
|
|
30
|
-
<tbody
|
|
30
|
+
<tbody>
|
|
31
31
|
<% @exceptions.each do |exception| %>
|
|
32
|
-
<tr id="<%= dom_id(exception) %>"
|
|
33
|
-
<td
|
|
32
|
+
<tr id="<%= dom_id(exception) %>">
|
|
33
|
+
<td>
|
|
34
34
|
<%= link_to exception.name, logged_exception_path(exception),
|
|
35
|
-
class: "
|
|
35
|
+
class: "fl-name",
|
|
36
36
|
data: { turbo_frame: "exception-details" } %>
|
|
37
|
-
<p class="
|
|
37
|
+
<p class="fl-msg"><%= exception.message %></p>
|
|
38
38
|
</td>
|
|
39
|
-
<td class="
|
|
40
|
-
<td class="
|
|
39
|
+
<td class="fl-date"><%= pretty_exception_date(exception) %></td>
|
|
40
|
+
<td class="fl-actions">
|
|
41
41
|
<%= link_to t(".delete"), logged_exception_path(exception),
|
|
42
|
-
class: "
|
|
42
|
+
class: "fl-delete-link",
|
|
43
43
|
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
|
|
44
44
|
</td>
|
|
45
45
|
</tr>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
</div>
|
|
50
50
|
<% end %>
|
|
51
51
|
|
|
52
|
-
<div class="
|
|
52
|
+
<div class="fl-pagination">
|
|
53
53
|
<%= will_paginate @exceptions, params: { controller: "logged_exceptions", action: "index" }.merge(params_filters) %>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
<%= link_to feed_logged_exceptions_path(format: :rss)
|
|
3
|
-
<%= t(".rss_feed") %>
|
|
4
|
-
<% end %>
|
|
1
|
+
<div class="fl-feed">
|
|
2
|
+
<%= link_to t(".rss_feed"), feed_logged_exceptions_path(format: :rss) %>
|
|
5
3
|
</div>
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
<div
|
|
2
|
-
<h3
|
|
3
|
-
<nav class="
|
|
1
|
+
<div>
|
|
2
|
+
<h3><%= title %></h3>
|
|
3
|
+
<nav class="fl-nav fl-nav-scroll">
|
|
4
4
|
<% values.each do |value| %>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
data: { turbo_frame: "exceptions" } %>
|
|
5
|
+
<li><%= link_to value, query_logged_exceptions_path(parameter => value),
|
|
6
|
+
data: { turbo_frame: "exceptions" } %></li>
|
|
8
7
|
<% end %>
|
|
9
8
|
</nav>
|
|
10
9
|
</div>
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
<div class="
|
|
1
|
+
<div class="fl-detail-header">
|
|
2
2
|
<div>
|
|
3
|
-
<p class="
|
|
4
|
-
<h2
|
|
3
|
+
<p class="fl-detail-time"><%= @exception.created_at.strftime(Time::DATE_FORMATS[:exc_full]) %></p>
|
|
4
|
+
<h2><%= @exception.name %></h2>
|
|
5
5
|
</div>
|
|
6
|
-
<button type="button" class="
|
|
6
|
+
<button type="button" class="fl-btn fl-btn-ghost fl-btn-sm" data-action="click->faultline#closeDetails">
|
|
7
7
|
<%= t(".close") %>
|
|
8
8
|
</button>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
<div class="
|
|
12
|
-
<section>
|
|
13
|
-
<h3
|
|
14
|
-
<div class="
|
|
11
|
+
<div class="fl-detail-body">
|
|
12
|
+
<section class="fl-detail-section">
|
|
13
|
+
<h3><%= t(".request") %></h3>
|
|
14
|
+
<div class="fl-detail-code"><%= pretty_format(@exception.request) %></div>
|
|
15
15
|
</section>
|
|
16
16
|
|
|
17
|
-
<section>
|
|
18
|
-
<h3
|
|
19
|
-
<div class="
|
|
17
|
+
<section class="fl-detail-section">
|
|
18
|
+
<h3><%= t(".message") %></h3>
|
|
19
|
+
<div class="fl-detail-code"><%= simple_format(@exception.message) %></div>
|
|
20
20
|
</section>
|
|
21
21
|
|
|
22
|
-
<section>
|
|
23
|
-
<h3
|
|
24
|
-
<pre class="
|
|
22
|
+
<section class="fl-detail-section">
|
|
23
|
+
<h3><%= t(".backtrace") %></h3>
|
|
24
|
+
<pre class="fl-detail-pre"><%= @exception.backtrace %></pre>
|
|
25
25
|
</section>
|
|
26
26
|
|
|
27
|
-
<section>
|
|
28
|
-
<h3
|
|
29
|
-
<div class="
|
|
27
|
+
<section class="fl-detail-section">
|
|
28
|
+
<h3><%= t(".environment") %></h3>
|
|
29
|
+
<div class="fl-detail-code"><%= pretty_format(@exception.environment) %></div>
|
|
30
30
|
</section>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
<div class="
|
|
33
|
+
<div class="fl-detail-footer">
|
|
34
34
|
<%= link_to t(".delete"), logged_exception_path(@exception),
|
|
35
|
-
class: "
|
|
35
|
+
class: "fl-delete-link",
|
|
36
36
|
data: { turbo_method: :delete, turbo_confirm: t(".confirm_delete"), turbo_stream: true } %>
|
|
37
37
|
</div>
|
|
@@ -2,7 +2,7 @@ xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
|
|
|
2
2
|
|
|
3
3
|
xml.rss "version" => "2.0" do
|
|
4
4
|
xml.channel do
|
|
5
|
-
xml.title
|
|
5
|
+
xml.title Faultline.application_name
|
|
6
6
|
xml.link url_for(:only_path => false, :skip_relative_url_root => false)
|
|
7
7
|
xml.language "en-us"
|
|
8
8
|
xml.ttl "60"
|
|
@@ -1,49 +1,47 @@
|
|
|
1
1
|
<% page_title t(".title") %>
|
|
2
2
|
|
|
3
|
-
<div data-controller="faultline" class="
|
|
4
|
-
<div class="
|
|
3
|
+
<div data-controller="faultline" class="fl-container">
|
|
4
|
+
<div class="fl-header">
|
|
5
5
|
<div>
|
|
6
|
-
<p class="
|
|
7
|
-
<h1
|
|
8
|
-
<p class="
|
|
6
|
+
<p class="fl-brand">Faultline</p>
|
|
7
|
+
<h1><%= t(".title") %></h1>
|
|
8
|
+
<p class="fl-subtitle"><%= t(".subtitle") %></p>
|
|
9
9
|
</div>
|
|
10
10
|
<%= button_to t(".clear_history"), clear_logged_exceptions_path, method: :post,
|
|
11
|
-
class: "
|
|
11
|
+
class: "fl-btn fl-btn-danger",
|
|
12
12
|
form: { data: { turbo_stream: true } },
|
|
13
13
|
data: { turbo_confirm: t(".confirm_clear") } %>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
<div class="
|
|
17
|
-
<aside class="
|
|
18
|
-
<h2
|
|
16
|
+
<div class="fl-grid">
|
|
17
|
+
<aside class="fl-card fl-sidebar">
|
|
18
|
+
<h2><%= t(".filters") %></h2>
|
|
19
19
|
|
|
20
|
-
<nav class="
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
data: { turbo_frame: "exceptions" } %>
|
|
20
|
+
<nav class="fl-nav" aria-label="<%= t(".filters") %>">
|
|
21
|
+
<li><%= link_to t(".latest_exceptions"), logged_exceptions_path,
|
|
22
|
+
data: { turbo_frame: "exceptions" } %></li>
|
|
24
23
|
</nav>
|
|
25
24
|
|
|
26
25
|
<%= render "filter_group", title: t(".exception"), values: @exception_names, parameter: :exception_names_filter %>
|
|
27
26
|
<%= render "filter_group", title: t(".controller_action"), values: @controller_actions, parameter: :controller_actions_filter %>
|
|
28
27
|
|
|
29
|
-
<div
|
|
30
|
-
<h3
|
|
31
|
-
<nav class="
|
|
28
|
+
<div>
|
|
29
|
+
<h3><%= t(".dates") %></h3>
|
|
30
|
+
<nav class="fl-nav">
|
|
32
31
|
<% [[t(".today"), 1], [t(".last_few_days"), 3], [t(".last_7_days"), 7], [t(".last_30_days"), 30]].each do |label, days| %>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
data: { turbo_frame: "exceptions" } %>
|
|
32
|
+
<li><%= link_to label, query_logged_exceptions_path(date_ranges_filter: days),
|
|
33
|
+
data: { turbo_frame: "exceptions" } %></li>
|
|
36
34
|
<% end %>
|
|
37
35
|
</nav>
|
|
38
36
|
</div>
|
|
39
37
|
|
|
40
|
-
<div
|
|
38
|
+
<div>
|
|
41
39
|
<%= form_with url: query_logged_exceptions_path, method: :get,
|
|
42
40
|
data: { turbo_frame: "exceptions", action: "submit->faultline#submit" } do |form| %>
|
|
43
|
-
|
|
44
|
-
<div class="
|
|
45
|
-
<%= form.search_field :query, placeholder: t(".search_placeholder")
|
|
46
|
-
<%= form.submit t(".find"), class: "
|
|
41
|
+
<h3><%= form.label :query, t(".search") %></h3>
|
|
42
|
+
<div class="fl-search">
|
|
43
|
+
<%= form.search_field :query, placeholder: t(".search_placeholder") %>
|
|
44
|
+
<%= form.submit t(".find"), class: "fl-btn fl-btn-primary fl-btn-sm" %>
|
|
47
45
|
</div>
|
|
48
46
|
<% end %>
|
|
49
47
|
</div>
|
|
@@ -51,13 +49,13 @@
|
|
|
51
49
|
<%= render "feed" %>
|
|
52
50
|
</aside>
|
|
53
51
|
|
|
54
|
-
<div
|
|
55
|
-
<div id="activity" data-faultline-target="activity" class="
|
|
52
|
+
<div>
|
|
53
|
+
<div id="activity" data-faultline-target="activity" class="fl-status" role="status" aria-live="polite" hidden>
|
|
56
54
|
<%= t(".loading") %>
|
|
57
55
|
</div>
|
|
58
56
|
|
|
59
|
-
<%= turbo_frame_tag "exception-details", class: "
|
|
60
|
-
<
|
|
57
|
+
<%= turbo_frame_tag "exception-details", class: "fl-card" do %>
|
|
58
|
+
<div class="fl-empty"><%= t(".select_exception") %></div>
|
|
61
59
|
<% end %>
|
|
62
60
|
|
|
63
61
|
<%= turbo_frame_tag "exceptions" do %>
|
|
@@ -5,21 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
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
8
|
<%= stylesheet_link_tag "faultline/application", "data-turbo-track": "reload" %>
|
|
21
9
|
<%= javascript_importmap_tags if respond_to?(:javascript_importmap_tags) %>
|
|
22
|
-
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" if host_javascript %>
|
|
23
10
|
<%= javascript_include_tag "faultline/application", "data-turbo-track": "reload", type: "module" %>
|
|
24
11
|
</head>
|
|
25
12
|
<body class="min-h-screen bg-slate-50 text-slate-900 antialiased">
|
|
@@ -14,5 +14,9 @@ class CreateFaultlineLoggedExceptions < ActiveRecord::Migration[8.0]
|
|
|
14
14
|
|
|
15
15
|
t.timestamps
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
add_index :faultline_logged_exceptions, :created_at
|
|
19
|
+
add_index :faultline_logged_exceptions, :exception_class
|
|
20
|
+
add_index :faultline_logged_exceptions, [:controller_name, :action_name]
|
|
17
21
|
end
|
|
18
22
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Faultline
|
|
4
|
+
class Configuration
|
|
5
|
+
# Dashboard title shown in the header. Set to nil to use "Faultline".
|
|
6
|
+
attr_accessor :application_name
|
|
7
|
+
|
|
8
|
+
# Proc called with the controller instance before each request.
|
|
9
|
+
# Return false/nil to deny access; return true to allow.
|
|
10
|
+
# Example:
|
|
11
|
+
# config.auth_block = ->(controller) { controller.current_user&.admin? }
|
|
12
|
+
attr_accessor :auth_block
|
|
13
|
+
|
|
14
|
+
# Proc called with the controller to attach extra data to each exception record.
|
|
15
|
+
# Example:
|
|
16
|
+
# config.exception_data = ->(controller) { { user_id: controller.current_user&.id } }
|
|
17
|
+
attr_accessor :exception_data
|
|
18
|
+
|
|
19
|
+
# Number of exceptions per page (default: 30).
|
|
20
|
+
attr_accessor :per_page
|
|
21
|
+
|
|
22
|
+
# Enable/disable the dashboard entirely (default: true).
|
|
23
|
+
attr_accessor :enabled
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@application_name = nil
|
|
27
|
+
@auth_block = nil
|
|
28
|
+
@exception_data = nil
|
|
29
|
+
@per_page = 30
|
|
30
|
+
@enabled = true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/faultline/engine.rb
CHANGED
data/lib/faultline/version.rb
CHANGED
data/lib/faultline.rb
CHANGED
|
@@ -5,9 +5,31 @@ require "will_paginate"
|
|
|
5
5
|
require "ipaddr"
|
|
6
6
|
require "socket"
|
|
7
7
|
require "faultline/version"
|
|
8
|
+
require "faultline/configuration"
|
|
8
9
|
require "faultline/engine"
|
|
9
10
|
|
|
10
11
|
module Faultline
|
|
12
|
+
class << self
|
|
13
|
+
attr_writer :configuration
|
|
14
|
+
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
yield(configuration)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Backward-compatible accessor for application_name
|
|
24
|
+
def application_name
|
|
25
|
+
configuration.application_name
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def application_name=(value)
|
|
29
|
+
configuration.application_name = value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
11
33
|
# Copyright (c) 2005 Jamis Buck
|
|
12
34
|
#
|
|
13
35
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
@@ -71,7 +93,13 @@ module Faultline
|
|
|
71
93
|
end
|
|
72
94
|
|
|
73
95
|
def log_exception(exception)
|
|
74
|
-
|
|
96
|
+
# Support both the legacy class_attribute and the new configuration DSL
|
|
97
|
+
deliverer = if self.class.exception_data
|
|
98
|
+
self.class.exception_data
|
|
99
|
+
elsif Faultline.configuration.exception_data
|
|
100
|
+
Faultline.configuration.exception_data
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
data = case deliverer
|
|
76
104
|
when nil then {}
|
|
77
105
|
when Symbol then send(deliverer)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Faultline
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
include ActiveRecord::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Install Faultline: creates migration, initializer, and mounts routes."
|
|
15
|
+
|
|
16
|
+
def self.next_migration_number(path)
|
|
17
|
+
ActiveRecord::Generators::MigrationGenerator.next_migration_number(path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_migration
|
|
21
|
+
migration_template "migration.rb",
|
|
22
|
+
"db/migrate/create_faultline_logged_exceptions.rb",
|
|
23
|
+
migration_version: migration_version
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_initializer
|
|
27
|
+
template "faultline.rb", "config/initializers/faultline.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def mount_routes
|
|
31
|
+
route "mount Faultline::Engine => \"/faultline\""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def include_loggable
|
|
35
|
+
inject_into_class "app/controllers/application_controller.rb", "ActionController::Base" do
|
|
36
|
+
" include Faultline::ExceptionLoggable\n"
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError
|
|
39
|
+
say "Could not auto-inject ExceptionLoggable into ApplicationController.", :yellow
|
|
40
|
+
say "Add the following to your ApplicationController:\n\n include Faultline::ExceptionLoggable\n"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def display_post_install
|
|
44
|
+
say ""
|
|
45
|
+
say "Faultline has been installed!", :green
|
|
46
|
+
say ""
|
|
47
|
+
say "Next steps:"
|
|
48
|
+
say " 1. Run: bin/rails db:migrate"
|
|
49
|
+
say " 2. Visit: /faultline"
|
|
50
|
+
say ""
|
|
51
|
+
say "Protect the dashboard by editing config/initializers/faultline.rb"
|
|
52
|
+
say ""
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def migration_version
|
|
58
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rails.application.config.to_prepare do
|
|
4
|
+
Faultline.configure do |config|
|
|
5
|
+
# Dashboard title shown in the header.
|
|
6
|
+
# config.application_name = "MyApp"
|
|
7
|
+
|
|
8
|
+
# Protect the dashboard. Return true to allow access, false to deny.
|
|
9
|
+
# Replace with your app's authentication method.
|
|
10
|
+
#
|
|
11
|
+
# Examples:
|
|
12
|
+
#
|
|
13
|
+
# # Require admin user (Devise / custom auth)
|
|
14
|
+
# config.auth_block = ->(controller) { controller.current_user&.admin? }
|
|
15
|
+
#
|
|
16
|
+
# # Require any logged-in user
|
|
17
|
+
# config.auth_block = ->(controller) { controller.current_user.present? }
|
|
18
|
+
#
|
|
19
|
+
# # HTTP Basic Auth (set FAULTLINE_USER / FAULTLINE_PASSWORD env vars)
|
|
20
|
+
# config.auth_block = ->(controller) {
|
|
21
|
+
# authenticate_or_request_with_http凭据("Faultline") do |username, password|
|
|
22
|
+
# username == ENV["FAULTLINE_USER"] && password == ENV["FAULTLINE_PASSWORD"]
|
|
23
|
+
# end
|
|
24
|
+
# }
|
|
25
|
+
#
|
|
26
|
+
# ⚠️ Without an auth_block, the dashboard is OPEN to anyone.
|
|
27
|
+
# Uncomment and configure one of the examples above before deploying.
|
|
28
|
+
config.auth_block = ->(controller) { true }
|
|
29
|
+
|
|
30
|
+
# Attach extra data to each exception record.
|
|
31
|
+
# config.exception_data = ->(controller) {
|
|
32
|
+
# {
|
|
33
|
+
# user_id: controller.current_user&.id,
|
|
34
|
+
# request_id: controller.request.request_id
|
|
35
|
+
# }
|
|
36
|
+
# }
|
|
37
|
+
|
|
38
|
+
# Number of exceptions per page.
|
|
39
|
+
# config.per_page = 30
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateFaultlineLoggedExceptions < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :faultline_logged_exceptions do |t|
|
|
6
|
+
t.string :exception_class
|
|
7
|
+
t.string :controller_name
|
|
8
|
+
t.string :action_name
|
|
9
|
+
t.text :message
|
|
10
|
+
t.text :backtrace
|
|
11
|
+
t.text :environment
|
|
12
|
+
t.text :request
|
|
13
|
+
t.string :user_info
|
|
14
|
+
t.string :user_agent
|
|
15
|
+
t.string :remote_ip
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :faultline_logged_exceptions, :created_at
|
|
21
|
+
add_index :faultline_logged_exceptions, :exception_class
|
|
22
|
+
add_index :faultline_logged_exceptions, [:controller_name, :action_name]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,4 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
namespace :faultline do
|
|
2
|
+
namespace :tailwind do
|
|
3
|
+
desc "Generate Tailwind v4 @source entries for Faultline views/helpers (OUT=app/assets/stylesheets/_faultline_sources.css ENTRY=app/assets/stylesheets/application.tailwind.css)"
|
|
4
|
+
task sources: :environment do
|
|
5
|
+
root = Faultline::Engine.root
|
|
6
|
+
out = ENV["OUT"] || "app/assets/stylesheets/_faultline_sources.css"
|
|
7
|
+
|
|
8
|
+
content = <<~CSS
|
|
9
|
+
/* Generated by bin/rails faultline:tailwind:sources - do not edit. Re-run after gem updates or on new machines/containers. */
|
|
10
|
+
@source "#{root}/app/views/**/*.erb";
|
|
11
|
+
@source "#{root}/app/helpers/**/*.rb";
|
|
12
|
+
CSS
|
|
13
|
+
|
|
14
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
15
|
+
File.write(out, content)
|
|
16
|
+
puts "Wrote #{out}"
|
|
17
|
+
|
|
18
|
+
entry = ENV["ENTRY"]
|
|
19
|
+
entry ||= "app/assets/stylesheets/application.tailwind.css" if File.exist?("app/assets/stylesheets/application.tailwind.css")
|
|
20
|
+
|
|
21
|
+
if entry && File.file?(entry)
|
|
22
|
+
import_line = "@import \"./#{File.basename(out)}\";"
|
|
23
|
+
body = File.read(entry)
|
|
24
|
+
|
|
25
|
+
if body.include?(File.basename(out))
|
|
26
|
+
puts "#{entry} already imports #{File.basename(out)}"
|
|
27
|
+
else
|
|
28
|
+
File.write(entry, body.sub(/\A/, "#{import_line}\n"))
|
|
29
|
+
puts "Added #{import_line} to #{entry}"
|
|
30
|
+
end
|
|
31
|
+
elsif !entry
|
|
32
|
+
puts "Add this to your Tailwind entry stylesheet:"
|
|
33
|
+
puts " @import \"./#{File.basename(out)}\";"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tamiru Hailu
|
|
@@ -115,8 +115,12 @@ files:
|
|
|
115
115
|
- faultline-rails.gemspec
|
|
116
116
|
- lib/faultline-rails.rb
|
|
117
117
|
- lib/faultline.rb
|
|
118
|
+
- lib/faultline/configuration.rb
|
|
118
119
|
- lib/faultline/engine.rb
|
|
119
120
|
- lib/faultline/version.rb
|
|
121
|
+
- lib/generators/faultline/install_generator.rb
|
|
122
|
+
- lib/generators/faultline/templates/faultline.rb
|
|
123
|
+
- lib/generators/faultline/templates/migration.rb
|
|
120
124
|
- lib/tasks/faultline_tasks.rake
|
|
121
125
|
homepage: https://github.com/tamiru/faultline-rails
|
|
122
126
|
licenses:
|
|
@@ -140,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
144
|
- !ruby/object:Gem::Version
|
|
141
145
|
version: '0'
|
|
142
146
|
requirements: []
|
|
143
|
-
rubygems_version: 4.0.
|
|
147
|
+
rubygems_version: 4.0.9
|
|
144
148
|
specification_version: 4
|
|
145
149
|
summary: A Rails 8 exception dashboard powered by Hotwire.
|
|
146
150
|
test_files: []
|