rails_exception_log 1.0.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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +173 -0
- data/app/assets/config/tailwind.config.js +34 -0
- data/app/assets/stylesheets/rails_exception_log/application.css +12 -0
- data/app/controllers/rails_exception_log/application_controller.rb +17 -0
- data/app/controllers/rails_exception_log/logged_exceptions_controller.rb +158 -0
- data/app/helpers/rails_exception_log/application_helper.rb +52 -0
- data/app/javascript/controllers/dropdown_controller.js +22 -0
- data/app/javascript/index.js +4 -0
- data/app/models/rails_exception_log/logged_exception.rb +149 -0
- data/app/views/rails_exception_log/logged_exceptions/index.html.erb +278 -0
- data/app/views/rails_exception_log/logged_exceptions/show.html.erb +214 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20240101000000_create_rails_exception_log_logged_exceptions.rb +36 -0
- data/lib/generators/rails_exception_log/install_generator.rb +54 -0
- data/lib/rails_exception_log/engine.rb +26 -0
- data/lib/rails_exception_log/exception_loggable.rb +89 -0
- data/lib/rails_exception_log/version.rb +3 -0
- data/lib/rails_exception_log.rb +23 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f74b304353846663f7ce71910b4551a2ff998add94dcbfc1ec367cf8fd6a0cc1
|
|
4
|
+
data.tar.gz: f8f6dc75de6d73b215d47aa4dad59c07d0f91a2613b54c7f3d3bf050633b10fb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ac73adab5b4b0f3bd6000c0054e468081bd305f9543f36e00d2047e1d051af1312c1f4dbc7d61b33c47d8a2da3a32ab0e7d894ec8bf23d182d495e8779b1bdfd
|
|
7
|
+
data.tar.gz: 6ce8da2534567b8a8308af48eb6fe4db59777a02f702417fafba96f6ac61c85ff565ded94e0e141b3f26abdb0c6133aca5931afe5143bb2fd40e93ea0066b12b
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Rails Exception Log
|
|
2
|
+
|
|
3
|
+
A modern exception logging gem for Rails 7 and 8 with Tailwind UI, inspired by Honeybadger.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Error Grouping**: Automatically groups similar exceptions using fingerprinting
|
|
8
|
+
- **Status Tracking**: Open, Resolved, Reopened, Ignored states
|
|
9
|
+
- **Occurrence Count**: Track how many times each error occurs
|
|
10
|
+
- **User Tracking**: Link exceptions to affected users
|
|
11
|
+
- **Comments**: Add notes to exceptions for team collaboration
|
|
12
|
+
- **Rate Limiting**: Prevent database flooding from repeated errors
|
|
13
|
+
- **Modern UI**: Beautiful Tailwind-styled dashboard
|
|
14
|
+
- **Stimulus JS**: Lightweight JavaScript interactions
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Ruby >= 3.0
|
|
19
|
+
- Rails >= 7.0, < 9.0
|
|
20
|
+
- Bun (for CSS bundling)
|
|
21
|
+
- Tailwind CSS via css-bundling-rails
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add to your Gemfile:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem "rails_exception_log"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run the install generator:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
rails generate rails_exception_log:install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will:
|
|
38
|
+
- Copy migrations to your app
|
|
39
|
+
- Add route to config/routes.rb
|
|
40
|
+
|
|
41
|
+
Then run migrations:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rails db:migrate
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
### Application Controller
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class ApplicationController < ActionController::Base
|
|
53
|
+
include RailsExceptionLog::ExceptionLoggable
|
|
54
|
+
rescue_from Exception, with: :log_exception_handler
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Routes
|
|
59
|
+
|
|
60
|
+
The generator adds this route:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
mount RailsExceptionLog::Engine => "/exceptions"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Optional Configuration
|
|
67
|
+
|
|
68
|
+
Create an initializer `config/initializers/rails_exception_log.rb`:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
RailsExceptionLog.configure do |config|
|
|
72
|
+
config.application_name = "My App"
|
|
73
|
+
config.max_requests_per_minute = 100
|
|
74
|
+
config.enable_user_tracking = true
|
|
75
|
+
config.before_log_exception = ->(controller) {
|
|
76
|
+
# Add custom authentication
|
|
77
|
+
true
|
|
78
|
+
}
|
|
79
|
+
config.after_log_exception = ->(exception) {
|
|
80
|
+
# Send notifications, etc.
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Custom Exception Data
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
class ApplicationController < ActionController::Base
|
|
89
|
+
# Add custom data to exceptions
|
|
90
|
+
self.exception_data = Proc.new { |controller|
|
|
91
|
+
{ user_id: controller.current_user&.id }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Or use a method
|
|
95
|
+
self.exception_data = :extra_exception_data
|
|
96
|
+
|
|
97
|
+
def extra_exception_data
|
|
98
|
+
{ version: "1.0", environment: Rails.env }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Authentication
|
|
104
|
+
|
|
105
|
+
In your environment config:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
config.after_initialize do
|
|
109
|
+
RailsExceptionLog::LoggedExceptionsController.class_eval do
|
|
110
|
+
before_action :authenticate_admin!
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
### Dashboard
|
|
118
|
+
|
|
119
|
+
Visit `/exceptions` to view the exception dashboard.
|
|
120
|
+
|
|
121
|
+
### Actions
|
|
122
|
+
|
|
123
|
+
- **View**: Click on an exception to see details
|
|
124
|
+
- **Resolve**: Mark an exception as resolved
|
|
125
|
+
- **Reopen**: Reopen a resolved exception
|
|
126
|
+
- **Ignore**: Ignore an exception
|
|
127
|
+
- **Delete**: Delete an exception
|
|
128
|
+
- **Export**: Export exceptions as CSV
|
|
129
|
+
|
|
130
|
+
### Filtering
|
|
131
|
+
|
|
132
|
+
- By date range (Today, 7 days, 30 days)
|
|
133
|
+
- By status (Open, Resolved, Reopened, Ignored)
|
|
134
|
+
- By exception type
|
|
135
|
+
- By controller
|
|
136
|
+
|
|
137
|
+
## JavaScript Setup
|
|
138
|
+
|
|
139
|
+
Ensure your app has Stimulus configured. The gem includes a dropdown controller.
|
|
140
|
+
|
|
141
|
+
In your application layout, include the bundled assets:
|
|
142
|
+
|
|
143
|
+
```erb
|
|
144
|
+
<%= stylesheet_link_tag "rails_exception_log/application" %>
|
|
145
|
+
<%= javascript_include_tag "rails_exception_log/application" %>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Rake Tasks
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Cleanup exceptions older than 30 days
|
|
152
|
+
rails rails_exception_log:cleanup
|
|
153
|
+
|
|
154
|
+
# Cleanup with custom days
|
|
155
|
+
rails rails_exception_log:cleanup[60]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Install dependencies
|
|
162
|
+
bundle install
|
|
163
|
+
|
|
164
|
+
# Run tests
|
|
165
|
+
bundle exec rake test
|
|
166
|
+
|
|
167
|
+
# Build
|
|
168
|
+
bundle exec rake build
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT License
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const defaultTheme = require("tailwindcss/defaultTheme");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
content: [
|
|
5
|
+
"./app/views/**/*.{erb,haml,html}",
|
|
6
|
+
"./app/helpers/**/*.rb",
|
|
7
|
+
"./app/controllers/**/*.rb",
|
|
8
|
+
],
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {
|
|
11
|
+
fontFamily: {
|
|
12
|
+
sans: ["Inter", ...defaultTheme.fontFamily.sans],
|
|
13
|
+
},
|
|
14
|
+
colors: {
|
|
15
|
+
primary: {
|
|
16
|
+
50: "#EEF2FF",
|
|
17
|
+
100: "#E0E7FF",
|
|
18
|
+
200: "#C7D2FE",
|
|
19
|
+
300: "#A5B4FC",
|
|
20
|
+
400: "#818CF8",
|
|
21
|
+
500: "#6366F1",
|
|
22
|
+
600: "#4F46E5",
|
|
23
|
+
700: "#4338CA",
|
|
24
|
+
800: "#3730A3",
|
|
25
|
+
900: "#312E81",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
plugins: [],
|
|
31
|
+
corePlugins: {
|
|
32
|
+
container: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@import "tailwindcss/base";
|
|
2
|
+
@import "tailwindcss/components";
|
|
3
|
+
@import "tailwindcss/utilities";
|
|
4
|
+
|
|
5
|
+
/* Custom styles */
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.font-mono {
|
|
11
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module RailsExceptionLog
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :exception
|
|
4
|
+
|
|
5
|
+
before_action :ensure_exception_log_enabled
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def ensure_exception_log_enabled
|
|
10
|
+
return if Rails.env.development? || Rails.env.test?
|
|
11
|
+
|
|
12
|
+
return if RailsExceptionLog.before_log_exception.call(self)
|
|
13
|
+
|
|
14
|
+
render plain: 'Unauthorized', status: 401
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
module RailsExceptionLog
|
|
4
|
+
class LoggedExceptionsController < ApplicationController
|
|
5
|
+
before_action :load_exception, only: %i[show destroy resolve reopen ignore add_comment]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@exceptions = filtered_exceptions
|
|
9
|
+
.select(:id, :exception_class, :message, :controller_name,
|
|
10
|
+
:action_name, :request_method, :request_path,
|
|
11
|
+
:environment, :created_at, :status, :occurrence_count,
|
|
12
|
+
:last_occurred_at, :resolved_at)
|
|
13
|
+
.order(last_occurred_at: :desc)
|
|
14
|
+
.page(params[:page])
|
|
15
|
+
|
|
16
|
+
@exception_classes = RailsExceptionLog::LoggedException
|
|
17
|
+
.select(:exception_class)
|
|
18
|
+
.distinct
|
|
19
|
+
.pluck(:exception_class)
|
|
20
|
+
.sort
|
|
21
|
+
|
|
22
|
+
@controllers = RailsExceptionLog::LoggedException
|
|
23
|
+
.select(:controller_name)
|
|
24
|
+
.distinct
|
|
25
|
+
.pluck(:controller_name)
|
|
26
|
+
.compact
|
|
27
|
+
.sort
|
|
28
|
+
|
|
29
|
+
@status_counts = {
|
|
30
|
+
open: RailsExceptionLog::LoggedException.status_open.count,
|
|
31
|
+
resolved: RailsExceptionLog::LoggedException.status_resolved.count,
|
|
32
|
+
reopened: RailsExceptionLog::LoggedException.status_reopened.count,
|
|
33
|
+
ignored: RailsExceptionLog::LoggedException.status_ignored.count
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@stats = {
|
|
37
|
+
total: RailsExceptionLog::LoggedException.count,
|
|
38
|
+
unresolved: RailsExceptionLog::LoggedException.unresolved.count,
|
|
39
|
+
last_24h: RailsExceptionLog::LoggedException.where('created_at >= ?', 24.hours.ago).count
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
respond_to do |format|
|
|
43
|
+
format.html
|
|
44
|
+
format.json { render json: @exceptions }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def show
|
|
49
|
+
@exception_data = @exception.request_params_display
|
|
50
|
+
@session_data = @exception.session_data_display
|
|
51
|
+
@headers = @exception.request_headers || {}
|
|
52
|
+
@comments = @exception.comments.present? ? JSON.parse(@exception.comments) : []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def destroy
|
|
56
|
+
@exception.destroy
|
|
57
|
+
redirect_to railsexceptionlog_exceptions_path,
|
|
58
|
+
notice: 'Exception deleted successfully'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve
|
|
62
|
+
@exception.mark_resolved!
|
|
63
|
+
redirect_to railsexceptionlog_exception_path(@exception),
|
|
64
|
+
notice: 'Exception marked as resolved'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def reopen
|
|
68
|
+
@exception.mark_open!
|
|
69
|
+
redirect_to railsexceptionlog_exception_path(@exception),
|
|
70
|
+
notice: 'Exception reopened'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ignore
|
|
74
|
+
@exception.update!(status: :ignored)
|
|
75
|
+
redirect_to railsexceptionlog_exception_path(@exception),
|
|
76
|
+
notice: 'Exception ignored'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_comment
|
|
80
|
+
@exception.add_comment!(params[:comment], author: current_user_email)
|
|
81
|
+
redirect_to railsexceptionlog_exception_path(@exception),
|
|
82
|
+
notice: 'Comment added'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def destroy_all
|
|
86
|
+
if params[:status].present?
|
|
87
|
+
filtered_exceptions.destroy_all
|
|
88
|
+
else
|
|
89
|
+
RailsExceptionLog::LoggedException.delete_all
|
|
90
|
+
end
|
|
91
|
+
redirect_to railsexceptionlog_exceptions_path,
|
|
92
|
+
notice: 'Exceptions cleared'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def export
|
|
96
|
+
@exceptions = filtered_exceptions.order(last_occurred_at: :desc)
|
|
97
|
+
respond_to do |format|
|
|
98
|
+
format.csv { send_data export_to_csv(@exceptions), filename: "exceptions-#{Date.today}.csv" }
|
|
99
|
+
format.json { render json: @exceptions }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def load_exception
|
|
106
|
+
@exception = RailsExceptionLog::LoggedException.find(params[:id])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def filtered_exceptions
|
|
110
|
+
exceptions = RailsExceptionLog::LoggedException.all
|
|
111
|
+
|
|
112
|
+
exceptions = exceptions.filter_by_date(params[:date_range]) if params[:date_range].present?
|
|
113
|
+
|
|
114
|
+
exceptions = exceptions.by_class(params[:exception_class]) if params[:exception_class].present?
|
|
115
|
+
|
|
116
|
+
exceptions = exceptions.by_controller(params[:controller]) if params[:controller].present?
|
|
117
|
+
|
|
118
|
+
exceptions = exceptions.by_status(params[:status]) if params[:status].present?
|
|
119
|
+
|
|
120
|
+
if params[:search].present?
|
|
121
|
+
search_term = "%#{params[:search]}%"
|
|
122
|
+
exceptions = exceptions.where(
|
|
123
|
+
'exception_class LIKE ? OR message LIKE ?',
|
|
124
|
+
search_term, search_term
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
exceptions
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def export_to_csv(exceptions)
|
|
132
|
+
CSV.generate(headers: true) do |csv|
|
|
133
|
+
csv << ['ID', 'Exception Class', 'Message', 'Controller', 'Action', 'Path', 'Method', 'Environment', 'Status',
|
|
134
|
+
'Occurrences', 'Created At']
|
|
135
|
+
exceptions.each do |exc|
|
|
136
|
+
csv << [
|
|
137
|
+
exc.id,
|
|
138
|
+
exc.exception_class,
|
|
139
|
+
exc.message,
|
|
140
|
+
exc.controller_name,
|
|
141
|
+
exc.action_name,
|
|
142
|
+
exc.request_path,
|
|
143
|
+
exc.request_method,
|
|
144
|
+
exc.environment,
|
|
145
|
+
exc.status_label,
|
|
146
|
+
exc.occurrence_count,
|
|
147
|
+
exc.created_at
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def current_user_email
|
|
154
|
+
# Override this method to return the current user's email
|
|
155
|
+
'anonymous'
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module RailsExceptionLog
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
def exception_status_class(exception)
|
|
4
|
+
case exception.environment
|
|
5
|
+
when 'production' then 'bg-red-100 text-red-800'
|
|
6
|
+
when 'staging' then 'bg-amber-100 text-amber-800'
|
|
7
|
+
else 'bg-slate-100 text-slate-800'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def exception_status_badge(exception)
|
|
12
|
+
case exception.status.to_s
|
|
13
|
+
when 'open' then 'bg-red-100 text-red-800'
|
|
14
|
+
when 'resolved' then 'bg-green-100 text-green-800'
|
|
15
|
+
when 'reopened' then 'bg-orange-100 text-orange-800'
|
|
16
|
+
when 'ignored' then 'bg-slate-100 text-slate-600'
|
|
17
|
+
else 'bg-slate-100 text-slate-800'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def severity_badge(severity)
|
|
22
|
+
case severity.to_i
|
|
23
|
+
when 3 then 'bg-red-600 text-white'
|
|
24
|
+
when 2 then 'bg-orange-500 text-white'
|
|
25
|
+
when 1 then 'bg-yellow-500 text-white'
|
|
26
|
+
else 'bg-blue-500 text-white'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def format_datetime(datetime)
|
|
31
|
+
return '-' unless datetime
|
|
32
|
+
|
|
33
|
+
datetime.strftime('%Y-%m-%d %H:%M:%S')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format_datetime_relative(datetime)
|
|
37
|
+
return '-' unless datetime
|
|
38
|
+
|
|
39
|
+
time_ago_in_words(datetime)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def request_method_class(method)
|
|
43
|
+
case method&.upcase
|
|
44
|
+
when 'GET' then 'bg-green-100 text-green-800'
|
|
45
|
+
when 'POST' then 'bg-blue-100 text-blue-800'
|
|
46
|
+
when 'PUT', 'PATCH' then 'bg-amber-100 text-amber-800'
|
|
47
|
+
when 'DELETE' then 'bg-red-100 text-red-800'
|
|
48
|
+
else 'bg-slate-100 text-slate-800'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class DropdownController extends Controller {
|
|
4
|
+
static targets = ["menu"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.isOpen = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toggle(event) {
|
|
11
|
+
event.preventDefault()
|
|
12
|
+
this.isOpen = !this.isOpen
|
|
13
|
+
this.menuTarget.classList.toggle("hidden", !this.isOpen)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
close(event) {
|
|
17
|
+
if (!this.element.contains(event.target)) {
|
|
18
|
+
this.isOpen = false
|
|
19
|
+
this.menuTarget.classList.add("hidden")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module RailsExceptionLog
|
|
2
|
+
class LoggedException < ::ActiveRecord::Base
|
|
3
|
+
self.table_name = 'rails_exception_log_logged_exceptions'
|
|
4
|
+
|
|
5
|
+
enum :status, { open: 0, resolved: 1, reopened: 2, ignored: 3 }, prefix: true
|
|
6
|
+
enum :severity, { info: 0, warning: 1, error: 2, critical: 3 }, prefix: true
|
|
7
|
+
|
|
8
|
+
validates :exception_class, presence: true
|
|
9
|
+
|
|
10
|
+
scope :recent, -> { order(last_occurred_at: :desc) }
|
|
11
|
+
scope :today, -> { where('created_at >= ?', Date.today) }
|
|
12
|
+
scope :last_days, ->(days) { where('created_at >= ?', days.days.ago) }
|
|
13
|
+
scope :by_class, ->(klass) { where(exception_class: klass) }
|
|
14
|
+
scope :by_controller, ->(controller) { where(controller_name: controller) }
|
|
15
|
+
scope :by_status, ->(status) { where(status: statuses[status]) }
|
|
16
|
+
scope :unresolved, -> { where(status: [statuses[:open], statuses[:reopened]]) }
|
|
17
|
+
scope :high_severity, -> { where('severity >= ?', severities[:error]) }
|
|
18
|
+
|
|
19
|
+
def self.filter_by_date(range)
|
|
20
|
+
case range
|
|
21
|
+
when 'today'
|
|
22
|
+
today
|
|
23
|
+
when '7days'
|
|
24
|
+
last_days(7)
|
|
25
|
+
when '30days'
|
|
26
|
+
last_days(30)
|
|
27
|
+
else
|
|
28
|
+
all
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.generate_fingerprint(exception, request = nil)
|
|
33
|
+
fingerprint_parts = [
|
|
34
|
+
exception.class.name,
|
|
35
|
+
exception.message.to_s.split("\n").first[0..200],
|
|
36
|
+
request&.path
|
|
37
|
+
].compact
|
|
38
|
+
|
|
39
|
+
Digest::SHA256.hexdigest(fingerprint_parts.join('|'))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.create_or_update_with_fingerprint!(exception, request = nil, user: nil)
|
|
43
|
+
fp = generate_fingerprint(exception, request)
|
|
44
|
+
existing = where(fingerprint: fp, status: [statuses[:open], statuses[:reopened]]).first
|
|
45
|
+
|
|
46
|
+
if existing
|
|
47
|
+
updates = {
|
|
48
|
+
occurrence_count: existing.occurrence_count + 1,
|
|
49
|
+
last_occurred_at: Time.current
|
|
50
|
+
}
|
|
51
|
+
updates[:backtrace] = exception.backtrace.join("\n") if exception.backtrace
|
|
52
|
+
existing.update!(updates)
|
|
53
|
+
existing
|
|
54
|
+
else
|
|
55
|
+
create!(
|
|
56
|
+
exception_class: exception.class.name,
|
|
57
|
+
message: exception.message,
|
|
58
|
+
backtrace: exception.backtrace&.join("\n"),
|
|
59
|
+
controller_name: request&.controller_name,
|
|
60
|
+
action_name: request&.action_name,
|
|
61
|
+
request_method: request&.method&.to_s,
|
|
62
|
+
request_path: request&.path,
|
|
63
|
+
request_params: request&.params,
|
|
64
|
+
request_headers: request_headers(request),
|
|
65
|
+
session_data: session_data(request),
|
|
66
|
+
environment: Rails.env,
|
|
67
|
+
fingerprint: fp,
|
|
68
|
+
user_id: user&.id,
|
|
69
|
+
user_email: user&.email,
|
|
70
|
+
last_occurred_at: Time.current
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.request_headers(request)
|
|
76
|
+
return {} unless request&.headers
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
'HTTP_HOST' => request.headers['HTTP_HOST'],
|
|
80
|
+
'HTTP_USER_AGENT' => request.headers['HTTP_USER_AGENT'],
|
|
81
|
+
'REMOTE_ADDR' => request.remote_ip
|
|
82
|
+
}.compact
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.session_data(request)
|
|
86
|
+
return {} unless request&.session
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
request.session.to_hash
|
|
90
|
+
rescue StandardError
|
|
91
|
+
{}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def formatted_backtrace
|
|
96
|
+
return [] unless backtrace
|
|
97
|
+
|
|
98
|
+
backtrace.split("\n").reject(&:blank?)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def request_params_display
|
|
102
|
+
return {} unless request_params
|
|
103
|
+
|
|
104
|
+
request_params.with_indifferent_access
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def session_data_display
|
|
108
|
+
return {} unless session_data
|
|
109
|
+
|
|
110
|
+
session_data.with_indifferent_access
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def mark_resolved!
|
|
114
|
+
update!(status: :resolved, resolved_at: Time.current)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def mark_open!
|
|
118
|
+
update!(status: :reopened, resolved_at: nil)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def add_comment!(comment, author: nil)
|
|
122
|
+
comment_data = {
|
|
123
|
+
body: comment,
|
|
124
|
+
author: author || 'System',
|
|
125
|
+
created_at: Time.current.iso8601
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
existing_comments = comments.present? ? JSON.parse(comments) : []
|
|
129
|
+
existing_comments << comment_data
|
|
130
|
+
update!(comments: existing_comments.to_json)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def reopen_if_needed!
|
|
134
|
+
return unless resolved? && last_occurred_at && last_occurred_at > resolved_at
|
|
135
|
+
|
|
136
|
+
update!(status: :reopened, resolved_at: nil)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def status_label
|
|
140
|
+
case status
|
|
141
|
+
when 'open' then 'Open'
|
|
142
|
+
when 'resolved' then 'Resolved'
|
|
143
|
+
when 'reopened' then 'Reopened'
|
|
144
|
+
when 'ignored' then 'Ignored'
|
|
145
|
+
else 'Unknown'
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|