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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 220502edbdb1e99c681f01b719d8f2987c6c18fef9e45bccd9a67bb98a17483f
4
- data.tar.gz: a20654cfb53fec6a6778e9e0f8ca7c698ab210fc0aa07b4a78e1950f9ec0a270
3
+ metadata.gz: f1f99904f339aa70945293928829f5ecb86d755a41203d9ad70dbc681e2b5213
4
+ data.tar.gz: '09d8929d8fc7823186707312eccb2be742e38c0e13167f6a62cdefa801e5d4f1'
5
5
  SHA512:
6
- metadata.gz: 061df31c39e8a80be6ce09f3039d432ca44ba66470ca5c91c2c83a12a006332792eab3e088de0ab56b1e6ede23c0077f883a5195d0f3b50c3d2e7cace7143b0f
7
- data.tar.gz: 5e23e10e12fea737bf89934a5a2bcf09cf11ca0fd0241cbcd7c64bef5ef7a58f7537e29faf1b55ac6c749fc182bd696393decd9219ca6051cc510e6fafbd81f5
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
- - Tailwind-compatible utility classes for a responsive dashboard UI.
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
- Install the dependencies and copy the engine migration:
27
+ Run the install generator to set up everything in one step:
28
28
 
29
29
  ```bash
30
30
  bundle install
31
- bin/rails app:faultline:install:migrations
31
+ bin/rails generate faultline:install
32
32
  bin/rails db:migrate
33
33
  ```
34
34
 
35
- Mount the dashboard in your application:
36
-
37
- ```ruby
38
- # config/routes.rb
39
- Rails.application.routes.draw do
40
- mount Faultline::Engine => "/faultline"
41
- end
42
- ```
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
- Attach your application's authorization callback:
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::LoggedExceptionsController.before_action :require_admin!
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
- Replace `require_admin!` with the authentication method provided by your application. You can also configure a policy, basic authentication, or an internal-only route at the host application level.
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
- ## Rails 8 frontend setup
125
+ ### Hotwire (default)
75
126
 
76
- Faultline includes `turbo-rails` and `stimulus-rails` as dependencies. Install the host application's Hotwire entry points when needed:
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
- The default Rails 8 import-map setup will load Faultline's Stimulus controller through the engine asset pipeline. If your application uses a JavaScript bundler, register the controller from `app/javascript/controllers/faultline_controller.js` in your Stimulus application:
136
+ ### Styling
84
137
 
85
- ```javascript
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.register("faultline", FaultlineController)
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
- ## Tailwind setup
92
-
93
- Faultline's dashboard uses Tailwind utility classes. Add the gem's view directory to the sources scanned by your Tailwind build; otherwise the host application will purge the dashboard classes.
142
+ ```bash
143
+ bundle show faultline-rails
144
+ ```
94
145
 
95
- For Tailwind CSS v4, add a source entry to the application's Tailwind stylesheet. Use the absolute path returned by Bundler for the installed gem:
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
- * Faultline's views use Tailwind utility classes. Add the gem's view path to
3
- * the host application's Tailwind sources so those classes are compiled.
4
- *
5
- * This small fallback keeps will_paginate usable when a host app has not yet
6
- * added Tailwind's pagination styles.
7
- */
8
- .faultline-pagination .pagination {
9
- display: flex;
10
- flex-wrap: wrap;
11
- gap: 0.5rem;
12
- align-items: center;
13
- }
1
+ /* Faultline Dashboard - Self-contained styles */
2
+ /* Works without Tailwind CSS. If you have Tailwind, these serve as fallbacks. */
14
3
 
15
- .faultline-pagination .pagination a,
16
- .faultline-pagination .pagination span {
17
- padding: 0.35rem 0.65rem;
18
- border-radius: 0.5rem;
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
- .faultline-pagination .pagination a {
22
- color: #4338ca;
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
- .faultline-pagination .pagination a:hover {
26
- background: #eef2ff;
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
- .faultline-pagination .pagination .current {
30
- background: #4f46e5;
31
- color: white;
32
- font-weight: 600;
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
- cattr_accessor :application_name
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 = [controller.application_name.presence, text].compact.join(" :: ")
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 = "#{exception.message.inspect}\n* Extra Data\n\n#{data}" unless data.blank?
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) * "\n" unless trace.is_a?(String)
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 |env, key|
49
- env << '* ' + ("%-*s: %s" % [max.length, key, request.env[key].to_s.strip])
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}") * "\n")
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:#{" #{request.method.to_s.upcase}" unless request.get?} #{request.protocol}#{request.env["HTTP_HOST"]}#{request.fullpath}",
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
- ] * "\n")
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) }.collect { |line| Pathname.new(line.gsub(@@backtrace_regex, "[RAILS_ROOT]")).cleanpath.to_s }
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="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
2
- <div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-5 py-4">
1
+ <div id="exceptions-content" class="fl-card">
2
+ <div class="fl-detail-header">
3
3
  <div>
4
- <h2 class="text-lg font-semibold text-slate-900">
4
+ <h2>
5
5
  <%= t(".heading") %>
6
6
  <% if filtered? %>
7
- <span class="font-normal text-slate-500">(<%= t(".filtered") %>)</span>
7
+ <span style="font-weight: 400; color: var(--fl-text-secondary);">(<%= t(".filtered") %>)</span>
8
8
  <% end %>
9
9
  </h2>
10
- <p class="mt-1 text-sm text-slate-500"><%= t(".count", count: @exceptions.total_entries) %></p>
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: "rounded-lg border border-red-200 px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50",
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="px-5 py-16 text-center text-sm text-slate-500"><%= t(".empty") %></div>
19
+ <div class="fl-empty"><%= t(".empty") %></div>
20
20
  <% else %>
21
- <div class="overflow-x-auto">
22
- <table class="min-w-full divide-y divide-slate-200">
23
- <thead class="bg-slate-50">
21
+ <div class="fl-table-wrap">
22
+ <table class="fl-table">
23
+ <thead>
24
24
  <tr>
25
- <th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".exception") %></th>
26
- <th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".date") %></th>
27
- <th class="px-5 py-3"><span class="sr-only"><%= t(".actions") %></span></th>
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 class="divide-y divide-slate-100">
30
+ <tbody>
31
31
  <% @exceptions.each do |exception| %>
32
- <tr id="<%= dom_id(exception) %>" class="group hover:bg-slate-50">
33
- <td class="max-w-2xl px-5 py-4 align-top">
32
+ <tr id="<%= dom_id(exception) %>">
33
+ <td>
34
34
  <%= link_to exception.name, logged_exception_path(exception),
35
- class: "font-medium text-indigo-700 hover:text-indigo-900 hover:underline",
35
+ class: "fl-name",
36
36
  data: { turbo_frame: "exception-details" } %>
37
- <p class="mt-1 truncate text-sm text-slate-600"><%= exception.message %></p>
37
+ <p class="fl-msg"><%= exception.message %></p>
38
38
  </td>
39
- <td class="whitespace-nowrap px-5 py-4 align-top text-sm text-slate-500"><%= pretty_exception_date(exception) %></td>
40
- <td class="whitespace-nowrap px-5 py-4 text-right align-top">
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: "text-sm font-medium text-red-600 hover:text-red-800 hover:underline",
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="faultline-pagination border-t border-slate-200 px-5 py-4 text-sm">
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="mt-6 border-t border-slate-200 pt-5">
2
- <%= link_to feed_logged_exceptions_path(format: :rss), class: "text-sm font-medium text-indigo-700 hover:underline" do %>
3
- <%= t(".rss_feed") %>
4
- <% end %>
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 class="mt-6">
2
- <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500"><%= title %></h3>
3
- <nav class="mt-2 max-h-40 space-y-1 overflow-y-auto">
1
+ <div>
2
+ <h3><%= title %></h3>
3
+ <nav class="fl-nav fl-nav-scroll">
4
4
  <% values.each do |value| %>
5
- <%= link_to value, query_logged_exceptions_path(parameter => value),
6
- class: "block truncate rounded-lg px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900",
7
- data: { turbo_frame: "exceptions" } %>
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="flex items-center justify-between gap-4 border-b border-slate-200 pb-4">
1
+ <div class="fl-detail-header">
2
2
  <div>
3
- <p class="text-sm text-slate-500"><%= @exception.created_at.strftime(Time::DATE_FORMATS[:exc_full]) %></p>
4
- <h2 class="mt-1 break-words text-xl font-semibold text-slate-900"><%= @exception.name %></h2>
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="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100" data-action="click->faultline#closeDetails">
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="mt-5 space-y-5 text-sm text-slate-700">
12
- <section>
13
- <h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".request") %></h3>
14
- <div class="overflow-x-auto rounded-lg bg-slate-50 p-4"><%= pretty_format(@exception.request) %></div>
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 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".message") %></h3>
19
- <div class="rounded-lg bg-slate-50 p-4"><%= simple_format(@exception.message) %></div>
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 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".backtrace") %></h3>
24
- <pre class="max-h-96 overflow-auto rounded-lg bg-slate-950 p-4 text-xs leading-5 text-slate-100"><%= @exception.backtrace %></pre>
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 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".environment") %></h3>
29
- <div class="overflow-x-auto rounded-lg bg-slate-50 p-4"><%= pretty_format(@exception.environment) %></div>
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="mt-6 border-t border-slate-200 pt-4">
33
+ <div class="fl-detail-footer">
34
34
  <%= link_to t(".delete"), logged_exception_path(@exception),
35
- class: "text-sm font-medium text-red-600 hover:text-red-800 hover:underline",
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 "#{Faultline::LoggedExceptionsController.application_name}"
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="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
4
- <div class="mb-8 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
3
+ <div data-controller="faultline" class="fl-container">
4
+ <div class="fl-header">
5
5
  <div>
6
- <p class="text-sm font-semibold uppercase tracking-widest text-indigo-600">Faultline</p>
7
- <h1 class="mt-2 text-3xl font-bold tracking-tight text-slate-950"><%= t(".title") %></h1>
8
- <p class="mt-2 text-slate-600"><%= t(".subtitle") %></p>
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: "rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700",
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="grid gap-6 lg:grid-cols-[16rem_minmax(0,1fr)]">
17
- <aside class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
18
- <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500"><%= t(".filters") %></h2>
16
+ <div class="fl-grid">
17
+ <aside class="fl-card fl-sidebar">
18
+ <h2><%= t(".filters") %></h2>
19
19
 
20
- <nav class="mt-4 space-y-1" aria-label="<%= t(".filters") %>">
21
- <%= link_to t(".latest_exceptions"), logged_exceptions_path,
22
- class: "block rounded-lg px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100",
23
- data: { turbo_frame: "exceptions" } %>
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 class="mt-6">
30
- <h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500"><%= t(".dates") %></h3>
31
- <nav class="mt-2 space-y-1">
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
- <%= link_to label, query_logged_exceptions_path(date_ranges_filter: days),
34
- class: "block rounded-lg px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900",
35
- data: { turbo_frame: "exceptions" } %>
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 class="mt-6 border-t border-slate-200 pt-5">
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
- <%= form.label :query, t(".search"), class: "text-xs font-semibold uppercase tracking-wide text-slate-500" %>
44
- <div class="mt-2 flex gap-2">
45
- <%= form.search_field :query, placeholder: t(".search_placeholder"), class: "min-w-0 flex-1 rounded-lg border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
46
- <%= form.submit t(".find"), class: "rounded-lg bg-slate-900 px-3 py-2 text-sm font-semibold text-white hover:bg-slate-700" %>
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 class="space-y-6">
55
- <div id="activity" data-faultline-target="activity" class="hidden rounded-lg bg-indigo-50 px-4 py-3 text-sm text-indigo-700" role="status" aria-live="polite">
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: "block rounded-xl border border-slate-200 bg-white p-5 shadow-sm" do %>
60
- <p class="py-8 text-center text-sm text-slate-500"><%= t(".select_exception") %></p>
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Faultline
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace Faultline
@@ -1,3 +1,3 @@
1
1
  module Faultline
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
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
- deliverer = self.class.exception_data
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
- # desc "Explaining what the task does"
2
- # task :faultline do
3
- # # Task goes here
4
- # end
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.1.1
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.10
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: []