rails_error_dashboard 0.1.23 → 0.1.25
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 +3 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +43 -38
- data/app/helpers/rails_error_dashboard/application_helper.rb +11 -2
- data/app/models/rails_error_dashboard/application.rb +18 -8
- data/app/models/rails_error_dashboard/error_log.rb +5 -5
- data/app/models/rails_error_dashboard/error_logs_record.rb +9 -10
- data/app/views/layouts/rails_error_dashboard.html.erb +19 -2
- data/app/views/rails_error_dashboard/errors/index.html.erb +1 -1
- data/db/migrate/20260106094233_add_application_to_error_logs.rb +2 -2
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +10 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +6 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +3 -3
- data/lib/rails_error_dashboard/engine.rb +14 -0
- data/lib/rails_error_dashboard/queries/error_correlation.rb +7 -3
- data/lib/rails_error_dashboard/queries/filter_options.rb +16 -4
- data/lib/rails_error_dashboard/queries/mttr_stats.rb +15 -7
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +35 -21
- data/lib/rails_error_dashboard/queries/recurring_issues.rb +7 -4
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb29048c8c0c29401f6295d9e2b1d797cd82908b81197f906a9df5a6f221a182
|
|
4
|
+
data.tar.gz: 63f26da41e171cc6956edb1bb5a0027a8c67a37bc94ff323665d4c34a2673d1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a12dfcad4a1f99763ed15bb861d3937ed577eaf2eaa89b66d99dda8fc50e63cf093d76c01dbfb226f18a4181a57352183f2ddfd8678009b02e247f0ae0d651d0
|
|
7
|
+
data.tar.gz: b213451cd2c971ab5c583f2301917a87f2c79d4b5c26526abaa72dfa59bbcc8db949cc4c7452a854966229648a7ae43f861484306e55701e780070ef72054b9b
|
data/README.md
CHANGED
|
@@ -16,6 +16,8 @@ gem 'rails_error_dashboard'
|
|
|
16
16
|
|
|
17
17
|
**5-minute setup** · **Works out-of-the-box** · **100% Rails + Postgres** · **No vendor lock-in**
|
|
18
18
|
|
|
19
|
+
📖 **[Full Documentation](https://anjanj.github.io/rails_error_dashboard/)** · 🎮 **[Live Demo](https://rails-error-dashboard.anjan.dev)** · 💎 **[RubyGems](https://rubygems.org/gems/rails_error_dashboard)**
|
|
20
|
+
|
|
19
21
|
### 🎮 Try the Live Demo
|
|
20
22
|
|
|
21
23
|
**See it in action:** [https://rails-error-dashboard.anjan.dev](https://rails-error-dashboard.anjan.dev)
|
|
@@ -586,6 +588,7 @@ config.webhook_urls = ['https://yourapp.com/hooks/errors']
|
|
|
586
588
|
- **[Error Trend Visualizations](docs/guides/ERROR_TREND_VISUALIZATIONS.md)** - Charts & analytics
|
|
587
589
|
|
|
588
590
|
### Advanced
|
|
591
|
+
- **[Multi-App Support](docs/MULTI_APP_PERFORMANCE.md)** - Track multiple applications
|
|
589
592
|
- **[Plugin System](docs/PLUGIN_SYSTEM.md)** - Build custom integrations
|
|
590
593
|
- **[API Reference](docs/API_REFERENCE.md)** - Complete API documentation
|
|
591
594
|
- **[Customization Guide](docs/CUSTOMIZATION.md)** - Customize everything
|
|
@@ -5,14 +5,32 @@ module RailsErrorDashboard
|
|
|
5
5
|
include Pagy::Backend
|
|
6
6
|
|
|
7
7
|
before_action :authenticate_dashboard_user!
|
|
8
|
+
before_action :set_application_context
|
|
9
|
+
|
|
10
|
+
FILTERABLE_PARAMS = %i[
|
|
11
|
+
error_type
|
|
12
|
+
unresolved
|
|
13
|
+
platform
|
|
14
|
+
application_id
|
|
15
|
+
search
|
|
16
|
+
severity
|
|
17
|
+
timeframe
|
|
18
|
+
frequency
|
|
19
|
+
status
|
|
20
|
+
assigned_to
|
|
21
|
+
priority_level
|
|
22
|
+
hide_snoozed
|
|
23
|
+
sort_by
|
|
24
|
+
sort_direction
|
|
25
|
+
].freeze
|
|
8
26
|
|
|
9
27
|
def overview
|
|
10
|
-
# Get dashboard stats using Query
|
|
11
|
-
@stats = Queries::DashboardStats.call
|
|
28
|
+
# Get dashboard stats using Query (pass application filter)
|
|
29
|
+
@stats = Queries::DashboardStats.call(application_id: @current_application_id)
|
|
12
30
|
|
|
13
|
-
# Get platform health summary (if enabled)
|
|
31
|
+
# Get platform health summary (if enabled, pass application filter)
|
|
14
32
|
if RailsErrorDashboard.configuration.enable_platform_comparison
|
|
15
|
-
comparison = Queries::PlatformComparison.new(days: 7)
|
|
33
|
+
comparison = Queries::PlatformComparison.new(days: 7, application_id: @current_application_id)
|
|
16
34
|
@platform_health = comparison.platform_health_summary
|
|
17
35
|
@platform_scores = comparison.platform_stability_scores
|
|
18
36
|
else
|
|
@@ -26,8 +44,8 @@ module RailsErrorDashboard
|
|
|
26
44
|
.where("occurred_at >= ?", 1.hour.ago)
|
|
27
45
|
.where(resolved_at: nil)
|
|
28
46
|
.where(priority_level: [ 3, 4 ]) # 3 = high, 4 = critical (based on severity enum)
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
@critical_alerts = @critical_alerts.where(application_id: @current_application_id) if @current_application_id.present?
|
|
48
|
+
@critical_alerts = @critical_alerts.order(occurred_at: :desc).limit(10)
|
|
31
49
|
end
|
|
32
50
|
|
|
33
51
|
def index
|
|
@@ -38,10 +56,10 @@ module RailsErrorDashboard
|
|
|
38
56
|
@pagy, @errors = pagy(errors_query, items: params[:per_page] || 25)
|
|
39
57
|
|
|
40
58
|
# Get dashboard stats using Query (pass application filter)
|
|
41
|
-
@stats = Queries::DashboardStats.call(application_id:
|
|
59
|
+
@stats = Queries::DashboardStats.call(application_id: @current_application_id)
|
|
42
60
|
|
|
43
|
-
# Get filter options using Query
|
|
44
|
-
filter_options = Queries::FilterOptions.call
|
|
61
|
+
# Get filter options using Query (pass application filter)
|
|
62
|
+
filter_options = Queries::FilterOptions.call(application_id: @current_application_id)
|
|
45
63
|
@error_types = filter_options[:error_types]
|
|
46
64
|
@platforms = filter_options[:platforms]
|
|
47
65
|
@applications = filter_options[:applications]
|
|
@@ -52,7 +70,7 @@ module RailsErrorDashboard
|
|
|
52
70
|
# - comments: Used in the comments section (@error.comments.count, @error.comments.recent_first)
|
|
53
71
|
# - parent_cascade_patterns/child_cascade_patterns: Used if cascade detection is enabled
|
|
54
72
|
@error = ErrorLog.includes(:comments, :parent_cascade_patterns, :child_cascade_patterns).find(params[:id])
|
|
55
|
-
@related_errors = @error.related_errors(limit: 5)
|
|
73
|
+
@related_errors = @error.related_errors(limit: 5, application_id: @current_application_id)
|
|
56
74
|
|
|
57
75
|
# Dispatch plugin event for error viewed
|
|
58
76
|
RailsErrorDashboard::PluginRegistry.dispatch(:on_error_viewed, @error)
|
|
@@ -139,7 +157,7 @@ module RailsErrorDashboard
|
|
|
139
157
|
@days = days
|
|
140
158
|
|
|
141
159
|
# Use Query to get analytics data (pass application filter)
|
|
142
|
-
analytics = Queries::AnalyticsStats.call(days, application_id:
|
|
160
|
+
analytics = Queries::AnalyticsStats.call(days, application_id: @current_application_id)
|
|
143
161
|
|
|
144
162
|
@error_stats = analytics[:error_stats]
|
|
145
163
|
@errors_over_time = analytics[:errors_over_time]
|
|
@@ -151,18 +169,18 @@ module RailsErrorDashboard
|
|
|
151
169
|
@mobile_errors = analytics[:mobile_errors]
|
|
152
170
|
@api_errors = analytics[:api_errors]
|
|
153
171
|
|
|
154
|
-
# Get recurring issues data
|
|
155
|
-
recurring = Queries::RecurringIssues.call(days)
|
|
172
|
+
# Get recurring issues data (pass application filter)
|
|
173
|
+
recurring = Queries::RecurringIssues.call(days, application_id: @current_application_id)
|
|
156
174
|
@recurring_data = recurring
|
|
157
175
|
|
|
158
|
-
# Get release correlation data
|
|
159
|
-
correlation = Queries::ErrorCorrelation.new(days: days)
|
|
176
|
+
# Get release correlation data (pass application filter)
|
|
177
|
+
correlation = Queries::ErrorCorrelation.new(days: days, application_id: @current_application_id)
|
|
160
178
|
@errors_by_version = correlation.errors_by_version
|
|
161
179
|
@problematic_releases = correlation.problematic_releases
|
|
162
180
|
@release_comparison = calculate_release_comparison
|
|
163
181
|
|
|
164
|
-
# Get MTTR data
|
|
165
|
-
mttr_data = Queries::MttrStats.call(days)
|
|
182
|
+
# Get MTTR data (pass application filter)
|
|
183
|
+
mttr_data = Queries::MttrStats.call(days, application_id: @current_application_id)
|
|
166
184
|
@mttr_stats = mttr_data
|
|
167
185
|
@overall_mttr = mttr_data[:overall_mttr]
|
|
168
186
|
@mttr_by_platform = mttr_data[:mttr_by_platform]
|
|
@@ -179,8 +197,8 @@ module RailsErrorDashboard
|
|
|
179
197
|
days = (params[:days] || 7).to_i
|
|
180
198
|
@days = days
|
|
181
199
|
|
|
182
|
-
# Use Query to get platform comparison data
|
|
183
|
-
comparison = Queries::PlatformComparison.new(days: days)
|
|
200
|
+
# Use Query to get platform comparison data (pass application filter)
|
|
201
|
+
comparison = Queries::PlatformComparison.new(days: days, application_id: @current_application_id)
|
|
184
202
|
|
|
185
203
|
@error_rate_by_platform = comparison.error_rate_by_platform
|
|
186
204
|
@severity_distribution = comparison.severity_distribution_by_platform
|
|
@@ -228,7 +246,7 @@ module RailsErrorDashboard
|
|
|
228
246
|
|
|
229
247
|
days = (params[:days] || 30).to_i
|
|
230
248
|
@days = days
|
|
231
|
-
correlation = Queries::ErrorCorrelation.new(days: days)
|
|
249
|
+
correlation = Queries::ErrorCorrelation.new(days: days, application_id: @current_application_id)
|
|
232
250
|
|
|
233
251
|
@errors_by_version = correlation.errors_by_version
|
|
234
252
|
@errors_by_git_sha = correlation.errors_by_git_sha
|
|
@@ -266,24 +284,11 @@ module RailsErrorDashboard
|
|
|
266
284
|
end
|
|
267
285
|
|
|
268
286
|
def filter_params
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
search: params[:search],
|
|
275
|
-
severity: params[:severity],
|
|
276
|
-
timeframe: params[:timeframe],
|
|
277
|
-
frequency: params[:frequency],
|
|
278
|
-
# Phase 3: Workflow filter params
|
|
279
|
-
status: params[:status],
|
|
280
|
-
assigned_to: params[:assigned_to],
|
|
281
|
-
priority_level: params[:priority_level],
|
|
282
|
-
hide_snoozed: params[:hide_snoozed],
|
|
283
|
-
# Sorting params
|
|
284
|
-
sort_by: params[:sort_by],
|
|
285
|
-
sort_direction: params[:sort_direction]
|
|
286
|
-
}
|
|
287
|
+
params.permit(*FILTERABLE_PARAMS).to_h.symbolize_keys
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def set_application_context
|
|
291
|
+
@current_application_id = params[:application_id].presence
|
|
287
292
|
end
|
|
288
293
|
|
|
289
294
|
def authenticate_dashboard_user!
|
|
@@ -81,6 +81,15 @@ module RailsErrorDashboard
|
|
|
81
81
|
RailsErrorDashboard.configuration.dashboard_username || ENV["USER"] || "unknown"
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Returns a sanitized hash of filter params safe for query links
|
|
85
|
+
# @param extra_keys [Array<Symbol>] Additional permitted keys for specific contexts
|
|
86
|
+
# @return [Hash] Whitelisted params for building URLs
|
|
87
|
+
def permitted_filter_params(extra_keys: [])
|
|
88
|
+
base_keys = RailsErrorDashboard::ErrorsController::FILTERABLE_PARAMS + %i[page per_page days]
|
|
89
|
+
allowed_keys = base_keys + Array(extra_keys)
|
|
90
|
+
params.permit(*allowed_keys).to_h.symbolize_keys
|
|
91
|
+
end
|
|
92
|
+
|
|
84
93
|
# Generates a sortable column header link
|
|
85
94
|
# @param label [String] The column label to display
|
|
86
95
|
# @param column [String] The column name to sort by
|
|
@@ -103,8 +112,8 @@ module RailsErrorDashboard
|
|
|
103
112
|
"⇅" # Unsorted indicator
|
|
104
113
|
end
|
|
105
114
|
|
|
106
|
-
# Preserve
|
|
107
|
-
link_params =
|
|
115
|
+
# Preserve whitelisted filter params while adding sort params
|
|
116
|
+
link_params = permitted_filter_params.merge(sort_by: column, sort_direction: new_direction)
|
|
108
117
|
|
|
109
118
|
link_to errors_path(link_params), class: "text-decoration-none" do
|
|
110
119
|
content_tag(:span, "#{label} ", class: current_sort == column ? "fw-bold" : "") +
|
|
@@ -12,22 +12,32 @@ module RailsErrorDashboard
|
|
|
12
12
|
scope :ordered_by_name, -> { order(:name) }
|
|
13
13
|
|
|
14
14
|
# Class method for finding or creating with caching
|
|
15
|
-
#
|
|
15
|
+
# Caches application IDs (not objects) to avoid stale ActiveRecord references
|
|
16
|
+
# This is safer with transactional fixtures and database rollbacks
|
|
16
17
|
def self.find_or_create_by_name(name)
|
|
17
|
-
# Try to find in cache
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# Try to find ID in cache
|
|
19
|
+
cached_id = Rails.cache.read("error_dashboard/application_id/#{name}")
|
|
20
|
+
if cached_id
|
|
21
|
+
# Verify the cached ID still exists in database
|
|
22
|
+
# (could have been deleted in tests with transactional rollback)
|
|
23
|
+
cached_record = find_by(id: cached_id)
|
|
24
|
+
return cached_record if cached_record
|
|
25
|
+
# Cache was stale, clear it
|
|
26
|
+
Rails.cache.delete("error_dashboard/application_id/#{name}")
|
|
27
|
+
end
|
|
20
28
|
|
|
21
|
-
# Try to find existing
|
|
29
|
+
# Try to find existing in database
|
|
22
30
|
found = find_by(name: name)
|
|
23
31
|
if found
|
|
24
|
-
|
|
32
|
+
# Cache the ID (not the object) for future lookups
|
|
33
|
+
Rails.cache.write("error_dashboard/application_id/#{name}", found.id, expires_in: 1.hour)
|
|
25
34
|
return found
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
# Create if not found
|
|
37
|
+
# Create if not found
|
|
29
38
|
created = create!(name: name)
|
|
30
|
-
|
|
39
|
+
# Cache the ID (not the object)
|
|
40
|
+
Rails.cache.write("error_dashboard/application_id/#{name}", created.id, expires_in: 1.hour)
|
|
31
41
|
created
|
|
32
42
|
end
|
|
33
43
|
|
|
@@ -390,11 +390,11 @@ module RailsErrorDashboard
|
|
|
390
390
|
end
|
|
391
391
|
|
|
392
392
|
# Find related errors of the same type
|
|
393
|
-
def related_errors(limit: 5)
|
|
394
|
-
self.class.where(error_type: error_type)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
393
|
+
def related_errors(limit: 5, application_id: nil)
|
|
394
|
+
scope = self.class.where(error_type: error_type)
|
|
395
|
+
.where.not(id: id)
|
|
396
|
+
scope = scope.where(application_id: application_id) if application_id.present?
|
|
397
|
+
scope.order(occurred_at: :desc).limit(limit)
|
|
398
398
|
end
|
|
399
399
|
|
|
400
400
|
# Extract backtrace frames for similarity comparison
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Abstract base class for models stored in the
|
|
3
|
+
# Abstract base class for models stored in the error dashboard database
|
|
4
4
|
#
|
|
5
5
|
# By default, this connects to the same database as the main application.
|
|
6
6
|
#
|
|
7
|
-
# To enable a separate error
|
|
7
|
+
# To enable a separate error dashboard database:
|
|
8
8
|
# 1. Set use_separate_database: true in the gem configuration
|
|
9
|
-
# 2.
|
|
10
|
-
# 3.
|
|
11
|
-
# 4. Run: rails db:
|
|
9
|
+
# 2. Set database: :error_dashboard (or your custom name) in the gem configuration
|
|
10
|
+
# 3. Configure error_dashboard settings in config/database.yml
|
|
11
|
+
# 4. Run: rails db:create
|
|
12
|
+
# 5. Run: rails db:migrate
|
|
12
13
|
#
|
|
13
14
|
# Benefits of separate database:
|
|
14
15
|
# - Performance isolation (error logging doesn't slow down user requests)
|
|
@@ -25,10 +26,8 @@ module RailsErrorDashboard
|
|
|
25
26
|
class ErrorLogsRecord < ActiveRecord::Base
|
|
26
27
|
self.abstract_class = true
|
|
27
28
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
connects_to database: { writing: :error_logs, reading: :error_logs }
|
|
32
|
-
end
|
|
29
|
+
# Database connection will be configured by the engine initializer
|
|
30
|
+
# after the user's configuration is loaded
|
|
31
|
+
# See lib/rails_error_dashboard/engine.rb
|
|
33
32
|
end
|
|
34
33
|
end
|
|
@@ -866,16 +866,20 @@
|
|
|
866
866
|
<% end %>
|
|
867
867
|
</button>
|
|
868
868
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
|
|
869
|
+
<%
|
|
870
|
+
base_route_params = request.path_parameters.slice(:controller, :action)
|
|
871
|
+
current_filter_params = permitted_filter_params
|
|
872
|
+
%>
|
|
869
873
|
<li>
|
|
870
874
|
<%= link_to "All Applications",
|
|
871
|
-
url_for(
|
|
875
|
+
url_for(base_route_params.merge(current_filter_params.except(:application_id))),
|
|
872
876
|
class: "dropdown-item #{'active' unless params[:application_id].present?}" %>
|
|
873
877
|
</li>
|
|
874
878
|
<li><hr class="dropdown-divider"></li>
|
|
875
879
|
<% @applications.each do |app_name, app_id| %>
|
|
876
880
|
<li>
|
|
877
881
|
<%= link_to app_name,
|
|
878
|
-
url_for(
|
|
882
|
+
url_for(base_route_params.merge(current_filter_params.merge(application_id: app_id))),
|
|
879
883
|
class: "dropdown-item #{params[:application_id].to_s == app_id.to_s ? 'active' : ''}" %>
|
|
880
884
|
</li>
|
|
881
885
|
<% end %>
|
|
@@ -996,6 +1000,19 @@
|
|
|
996
1000
|
</div>
|
|
997
1001
|
</div>
|
|
998
1002
|
|
|
1003
|
+
<!-- Footer -->
|
|
1004
|
+
<footer class="mt-5 py-4 text-center border-top">
|
|
1005
|
+
<div class="container">
|
|
1006
|
+
<p class="text-muted mb-1">
|
|
1007
|
+
<i class="bi bi-bug-fill text-primary"></i>
|
|
1008
|
+
Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank" class="text-decoration-none">Rails Error Dashboard</a>
|
|
1009
|
+
</p>
|
|
1010
|
+
<p class="text-muted small mb-0">
|
|
1011
|
+
Created by <a href="https://www.anjan.dev/" target="_blank" class="text-decoration-none">Anjan Jagirdar</a>
|
|
1012
|
+
</p>
|
|
1013
|
+
</div>
|
|
1014
|
+
</footer>
|
|
1015
|
+
|
|
999
1016
|
<!-- Bootstrap JS -->
|
|
1000
1017
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
1001
1018
|
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
<% active_filters.each do |filter| %>
|
|
159
159
|
<%
|
|
160
160
|
# Build URL without this specific filter
|
|
161
|
-
filter_params =
|
|
161
|
+
filter_params = permitted_filter_params.except(filter[:param])
|
|
162
162
|
%>
|
|
163
163
|
<%= link_to errors_path(filter_params), class: "badge bg-primary text-decoration-none filter-pill" do %>
|
|
164
164
|
<%= filter[:label] %>
|
|
@@ -7,11 +7,11 @@ class AddApplicationToErrorLogs < ActiveRecord::Migration[7.0]
|
|
|
7
7
|
add_index :rails_error_dashboard_error_logs, :application_id
|
|
8
8
|
|
|
9
9
|
add_index :rails_error_dashboard_error_logs,
|
|
10
|
-
[:application_id, :occurred_at],
|
|
10
|
+
[ :application_id, :occurred_at ],
|
|
11
11
|
name: 'index_error_logs_on_app_occurred'
|
|
12
12
|
|
|
13
13
|
add_index :rails_error_dashboard_error_logs,
|
|
14
|
-
[:application_id, :resolved],
|
|
14
|
+
[ :application_id, :resolved ],
|
|
15
15
|
name: 'index_error_logs_on_app_resolved'
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -18,6 +18,7 @@ module RailsErrorDashboard
|
|
|
18
18
|
class_option :async_logging, type: :boolean, default: false, desc: "Enable async error logging"
|
|
19
19
|
class_option :error_sampling, type: :boolean, default: false, desc: "Enable error sampling (reduce volume)"
|
|
20
20
|
class_option :separate_database, type: :boolean, default: false, desc: "Use separate database for errors"
|
|
21
|
+
class_option :database, type: :string, default: nil, desc: "Database name to use for errors (e.g., 'error_dashboard')"
|
|
21
22
|
# Advanced analytics options
|
|
22
23
|
class_option :baseline_alerts, type: :boolean, default: false, desc: "Enable baseline anomaly alerts"
|
|
23
24
|
class_option :similar_errors, type: :boolean, default: false, desc: "Enable fuzzy error matching"
|
|
@@ -185,6 +186,7 @@ module RailsErrorDashboard
|
|
|
185
186
|
@enable_async_logging = @selected_features&.dig(:async_logging) || options[:async_logging]
|
|
186
187
|
@enable_error_sampling = @selected_features&.dig(:error_sampling) || options[:error_sampling]
|
|
187
188
|
@enable_separate_database = @selected_features&.dig(:separate_database) || options[:separate_database]
|
|
189
|
+
@database_name = options[:database]
|
|
188
190
|
|
|
189
191
|
# Advanced Analytics
|
|
190
192
|
@enable_baseline_alerts = @selected_features&.dig(:baseline_alerts) || options[:baseline_alerts]
|
|
@@ -275,7 +277,14 @@ module RailsErrorDashboard
|
|
|
275
277
|
say " → Set PAGERDUTY_INTEGRATION_KEY in .env", :yellow if @enable_pagerduty
|
|
276
278
|
say " → Set WEBHOOK_URLS in .env", :yellow if @enable_webhooks
|
|
277
279
|
say " → Ensure Sidekiq/Solid Queue running", :yellow if @enable_async_logging
|
|
278
|
-
|
|
280
|
+
if @enable_separate_database
|
|
281
|
+
if @database_name
|
|
282
|
+
say " → Configure '#{@database_name}' database in database.yml", :yellow
|
|
283
|
+
else
|
|
284
|
+
say " → Configure database in database.yml and set config.database", :yellow
|
|
285
|
+
end
|
|
286
|
+
say " See docs/guides/DATABASE_OPTIONS.md for details", :yellow
|
|
287
|
+
end
|
|
279
288
|
|
|
280
289
|
say "\n"
|
|
281
290
|
say "Next Steps:", :cyan
|
|
@@ -153,6 +153,11 @@ RailsErrorDashboard.configure do |config|
|
|
|
153
153
|
# Errors will be stored in a dedicated database
|
|
154
154
|
# See docs/guides/DATABASE_OPTIONS.md for setup instructions
|
|
155
155
|
config.use_separate_database = true
|
|
156
|
+
<% if @database_name -%>
|
|
157
|
+
config.database = :<%= @database_name %>
|
|
158
|
+
<% else -%>
|
|
159
|
+
# config.database = :error_dashboard # Uncomment and set your database name
|
|
160
|
+
<% end -%>
|
|
156
161
|
# To disable: Set config.use_separate_database = false
|
|
157
162
|
|
|
158
163
|
<% else -%>
|
|
@@ -160,6 +165,7 @@ RailsErrorDashboard.configure do |config|
|
|
|
160
165
|
# Errors are stored in your main application database
|
|
161
166
|
# To enable: Set config.use_separate_database = true and configure database.yml
|
|
162
167
|
config.use_separate_database = false
|
|
168
|
+
# config.database = :error_dashboard # Database name when using separate database
|
|
163
169
|
|
|
164
170
|
<% end -%>
|
|
165
171
|
# ============================================================================
|
|
@@ -143,15 +143,15 @@ module RailsErrorDashboard
|
|
|
143
143
|
# Find or create application for multi-app support
|
|
144
144
|
def find_or_create_application
|
|
145
145
|
app_name = RailsErrorDashboard.configuration.application_name ||
|
|
146
|
-
ENV[
|
|
146
|
+
ENV["APPLICATION_NAME"] ||
|
|
147
147
|
(defined?(Rails) && Rails.application.class.module_parent_name) ||
|
|
148
|
-
|
|
148
|
+
"Rails Application"
|
|
149
149
|
|
|
150
150
|
Application.find_or_create_by_name(app_name)
|
|
151
151
|
rescue => e
|
|
152
152
|
RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Failed to find/create application: #{e.message}")
|
|
153
153
|
# Fallback: try to find any application or create default
|
|
154
|
-
Application.first || Application.create!(name:
|
|
154
|
+
Application.first || Application.create!(name: "Default Application")
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
# Trigger notification callbacks for error logging
|
|
@@ -2,6 +2,20 @@ module RailsErrorDashboard
|
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
isolate_namespace RailsErrorDashboard
|
|
4
4
|
|
|
5
|
+
# Configure database connection for error models
|
|
6
|
+
# This runs early, before middleware setup, but after database.yml is loaded
|
|
7
|
+
initializer "rails_error_dashboard.database", before: :load_config_initializers do
|
|
8
|
+
config.after_initialize do
|
|
9
|
+
if RailsErrorDashboard.configuration&.use_separate_database
|
|
10
|
+
database_name = RailsErrorDashboard.configuration&.database || :error_dashboard
|
|
11
|
+
|
|
12
|
+
RailsErrorDashboard::ErrorLogsRecord.connects_to(
|
|
13
|
+
database: { writing: database_name, reading: database_name }
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
5
19
|
# Initialize the engine
|
|
6
20
|
initializer "rails_error_dashboard.middleware" do |app|
|
|
7
21
|
# Enable Flash middleware for Error Dashboard routes in API-only apps
|
|
@@ -14,11 +14,13 @@ module RailsErrorDashboard
|
|
|
14
14
|
# correlation.errors_by_version
|
|
15
15
|
# # => { "1.0.0" => { count: 100, error_types: 15, critical_count: 5 } }
|
|
16
16
|
class ErrorCorrelation
|
|
17
|
-
attr_reader :days
|
|
17
|
+
attr_reader :days, :application_id
|
|
18
18
|
|
|
19
19
|
# @param days [Integer] Number of days to analyze (default: 30)
|
|
20
|
-
|
|
20
|
+
# @param application_id [Integer, nil] Optional application ID to filter by
|
|
21
|
+
def initialize(days: 30, application_id: nil)
|
|
21
22
|
@days = days
|
|
23
|
+
@application_id = application_id
|
|
22
24
|
@start_date = days.days.ago
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -285,7 +287,9 @@ module RailsErrorDashboard
|
|
|
285
287
|
private
|
|
286
288
|
|
|
287
289
|
def base_query
|
|
288
|
-
ErrorLog.where("occurred_at >= ?", @start_date)
|
|
290
|
+
scope = ErrorLog.where("occurred_at >= ?", @start_date)
|
|
291
|
+
scope = scope.where(application_id: @application_id) if @application_id.present?
|
|
292
|
+
scope
|
|
289
293
|
end
|
|
290
294
|
|
|
291
295
|
# Check if app_version column exists
|
|
@@ -5,17 +5,29 @@ module RailsErrorDashboard
|
|
|
5
5
|
# Query: Fetch available filter options
|
|
6
6
|
# This is a read operation that returns distinct values for filters
|
|
7
7
|
class FilterOptions
|
|
8
|
-
def self.call
|
|
9
|
-
new.call
|
|
8
|
+
def self.call(application_id: nil)
|
|
9
|
+
new(application_id: application_id).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(application_id: nil)
|
|
13
|
+
@application_id = application_id
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
def call
|
|
13
17
|
{
|
|
14
|
-
error_types:
|
|
15
|
-
platforms:
|
|
18
|
+
error_types: base_scope.distinct.pluck(:error_type).compact.sort,
|
|
19
|
+
platforms: base_scope.distinct.pluck(:platform).compact,
|
|
16
20
|
applications: Application.ordered_by_name.pluck(:name, :id)
|
|
17
21
|
}
|
|
18
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def base_scope
|
|
27
|
+
scope = ErrorLog.all
|
|
28
|
+
scope = scope.where(application_id: @application_id) if @application_id.present?
|
|
29
|
+
scope
|
|
30
|
+
end
|
|
19
31
|
end
|
|
20
32
|
end
|
|
21
33
|
end
|
|
@@ -5,12 +5,13 @@ module RailsErrorDashboard
|
|
|
5
5
|
# Query: Calculate Mean Time to Resolution (MTTR) statistics
|
|
6
6
|
# Provides metrics on how quickly errors are resolved
|
|
7
7
|
class MttrStats
|
|
8
|
-
def self.call(days = 30)
|
|
9
|
-
new(days).call
|
|
8
|
+
def self.call(days = 30, application_id: nil)
|
|
9
|
+
new(days, application_id: application_id).call
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def initialize(days = 30)
|
|
12
|
+
def initialize(days = 30, application_id: nil)
|
|
13
13
|
@days = days
|
|
14
|
+
@application_id = application_id
|
|
14
15
|
@start_date = days.days.ago
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -29,9 +30,13 @@ module RailsErrorDashboard
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
32
|
def resolved_errors
|
|
32
|
-
@resolved_errors ||=
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
@resolved_errors ||= begin
|
|
34
|
+
scope = ErrorLog
|
|
35
|
+
.where.not(resolved_at: nil)
|
|
36
|
+
.where("occurred_at >= ?", @start_date)
|
|
37
|
+
scope = scope.where(application_id: @application_id) if @application_id.present?
|
|
38
|
+
scope
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def calculate_overall_mttr
|
|
@@ -44,7 +49,9 @@ module RailsErrorDashboard
|
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def mttr_by_platform
|
|
47
|
-
|
|
52
|
+
platforms_scope = ErrorLog.all
|
|
53
|
+
platforms_scope = platforms_scope.where(application_id: @application_id) if @application_id.present?
|
|
54
|
+
platforms = platforms_scope.distinct.pluck(:platform).compact
|
|
48
55
|
|
|
49
56
|
platforms.each_with_object({}) do |platform, result|
|
|
50
57
|
platform_resolved = resolved_errors.where(platform: platform)
|
|
@@ -81,6 +88,7 @@ module RailsErrorDashboard
|
|
|
81
88
|
week_resolved = ErrorLog
|
|
82
89
|
.where.not(resolved_at: nil)
|
|
83
90
|
.where("occurred_at >= ? AND occurred_at < ?", current_date, week_end)
|
|
91
|
+
week_resolved = week_resolved.where(application_id: @application_id) if @application_id.present?
|
|
84
92
|
|
|
85
93
|
if week_resolved.any?
|
|
86
94
|
total_hours = week_resolved.sum { |e| ((e.resolved_at - e.occurred_at) / 3600.0) }
|
|
@@ -17,18 +17,32 @@ module RailsErrorDashboard
|
|
|
17
17
|
# comparison.error_rate_by_platform
|
|
18
18
|
# # => { "ios" => 150, "android" => 200, "api" => 50, "web" => 100 }
|
|
19
19
|
class PlatformComparison
|
|
20
|
-
attr_reader :days
|
|
20
|
+
attr_reader :days, :application_id
|
|
21
21
|
|
|
22
22
|
# @param days [Integer] Number of days to analyze (default: 7)
|
|
23
|
-
|
|
23
|
+
# @param application_id [Integer, nil] Optional application ID to filter by
|
|
24
|
+
def initialize(days: 7, application_id: nil)
|
|
24
25
|
@days = days
|
|
26
|
+
@application_id = application_id
|
|
25
27
|
@start_date = days.days.ago
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Base scope that filters by application when present
|
|
33
|
+
# @return [ActiveRecord::Relation]
|
|
34
|
+
def base_scope
|
|
35
|
+
scope = ErrorLog.all
|
|
36
|
+
scope = scope.where(application_id: @application_id) if @application_id.present?
|
|
37
|
+
scope
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
public
|
|
41
|
+
|
|
28
42
|
# Get error count by platform for the time period
|
|
29
43
|
# @return [Hash] Platform name => error count
|
|
30
44
|
def error_rate_by_platform
|
|
31
|
-
|
|
45
|
+
base_scope
|
|
32
46
|
.where("occurred_at >= ?", @start_date)
|
|
33
47
|
.group(:platform)
|
|
34
48
|
.count
|
|
@@ -37,10 +51,10 @@ module RailsErrorDashboard
|
|
|
37
51
|
# Get severity distribution by platform
|
|
38
52
|
# @return [Hash] Platform => { severity => count }
|
|
39
53
|
def severity_distribution_by_platform
|
|
40
|
-
platforms =
|
|
54
|
+
platforms = base_scope.distinct.pluck(:platform).compact
|
|
41
55
|
|
|
42
56
|
platforms.each_with_object({}) do |platform, result|
|
|
43
|
-
errors =
|
|
57
|
+
errors = base_scope
|
|
44
58
|
.where(platform: platform)
|
|
45
59
|
.where("occurred_at >= ?", @start_date)
|
|
46
60
|
|
|
@@ -57,10 +71,10 @@ module RailsErrorDashboard
|
|
|
57
71
|
# Get average resolution time by platform
|
|
58
72
|
# @return [Hash] Platform => average hours to resolve
|
|
59
73
|
def resolution_time_by_platform
|
|
60
|
-
platforms =
|
|
74
|
+
platforms = base_scope.distinct.pluck(:platform).compact
|
|
61
75
|
|
|
62
76
|
platforms.each_with_object({}) do |platform, result|
|
|
63
|
-
resolved_errors =
|
|
77
|
+
resolved_errors = base_scope
|
|
64
78
|
.where(platform: platform)
|
|
65
79
|
.where.not(resolved_at: nil)
|
|
66
80
|
.where("occurred_at >= ?", @start_date)
|
|
@@ -79,10 +93,10 @@ module RailsErrorDashboard
|
|
|
79
93
|
# Get top 10 errors for each platform
|
|
80
94
|
# @return [Hash] Platform => Array of error hashes
|
|
81
95
|
def top_errors_by_platform
|
|
82
|
-
platforms =
|
|
96
|
+
platforms = base_scope.distinct.pluck(:platform).compact
|
|
83
97
|
|
|
84
98
|
platforms.each_with_object({}) do |platform, result|
|
|
85
|
-
result[platform] =
|
|
99
|
+
result[platform] = base_scope
|
|
86
100
|
.where(platform: platform)
|
|
87
101
|
.where("occurred_at >= ?", @start_date)
|
|
88
102
|
.select(:id, :error_type, :message, :occurrence_count, :occurred_at)
|
|
@@ -105,7 +119,7 @@ module RailsErrorDashboard
|
|
|
105
119
|
# Higher score = more stable (fewer errors, faster resolution)
|
|
106
120
|
# @return [Hash] Platform => stability score
|
|
107
121
|
def platform_stability_scores
|
|
108
|
-
platforms =
|
|
122
|
+
platforms = base_scope.distinct.pluck(:platform).compact
|
|
109
123
|
error_rates = error_rate_by_platform
|
|
110
124
|
resolution_times = resolution_time_by_platform
|
|
111
125
|
|
|
@@ -131,7 +145,7 @@ module RailsErrorDashboard
|
|
|
131
145
|
# @return [Array<Hash>] Errors with their platforms
|
|
132
146
|
def cross_platform_errors
|
|
133
147
|
# Get error types that appear on 2+ platforms
|
|
134
|
-
error_types_with_platforms =
|
|
148
|
+
error_types_with_platforms = base_scope
|
|
135
149
|
.where("occurred_at >= ?", @start_date)
|
|
136
150
|
.group(:error_type, :platform)
|
|
137
151
|
.select(:error_type, :platform)
|
|
@@ -145,7 +159,7 @@ module RailsErrorDashboard
|
|
|
145
159
|
.select { |_, platforms| platforms.map(&:last).uniq.count > 1 }
|
|
146
160
|
.map do |error_type, platform_pairs|
|
|
147
161
|
platforms = platform_pairs.map(&:last).uniq
|
|
148
|
-
total_count =
|
|
162
|
+
total_count = base_scope
|
|
149
163
|
.where(error_type: error_type, platform: platforms)
|
|
150
164
|
.where("occurred_at >= ?", @start_date)
|
|
151
165
|
.sum(:occurrence_count)
|
|
@@ -155,7 +169,7 @@ module RailsErrorDashboard
|
|
|
155
169
|
platforms: platforms.sort,
|
|
156
170
|
total_occurrences: total_count,
|
|
157
171
|
platform_breakdown: platforms.each_with_object({}) do |platform, breakdown|
|
|
158
|
-
breakdown[platform] =
|
|
172
|
+
breakdown[platform] = base_scope
|
|
159
173
|
.where(error_type: error_type, platform: platform)
|
|
160
174
|
.where("occurred_at >= ?", @start_date)
|
|
161
175
|
.sum(:occurrence_count)
|
|
@@ -168,10 +182,10 @@ module RailsErrorDashboard
|
|
|
168
182
|
# Get daily error trend by platform
|
|
169
183
|
# @return [Hash] Platform => { date => count }
|
|
170
184
|
def daily_trend_by_platform
|
|
171
|
-
platforms =
|
|
185
|
+
platforms = base_scope.distinct.pluck(:platform).compact
|
|
172
186
|
|
|
173
187
|
platforms.each_with_object({}) do |platform, result|
|
|
174
|
-
result[platform] =
|
|
188
|
+
result[platform] = base_scope
|
|
175
189
|
.where(platform: platform)
|
|
176
190
|
.where("occurred_at >= ?", @start_date)
|
|
177
191
|
.group_by_day(:occurred_at, range: @start_date..Time.current)
|
|
@@ -182,7 +196,7 @@ module RailsErrorDashboard
|
|
|
182
196
|
# Get platform health summary
|
|
183
197
|
# @return [Hash] Platform => health metrics
|
|
184
198
|
def platform_health_summary
|
|
185
|
-
platforms =
|
|
199
|
+
platforms = base_scope.distinct.pluck(:platform).compact
|
|
186
200
|
error_rates = error_rate_by_platform
|
|
187
201
|
stability_scores = platform_stability_scores
|
|
188
202
|
|
|
@@ -190,18 +204,18 @@ module RailsErrorDashboard
|
|
|
190
204
|
total_errors = error_rates[platform] || 0
|
|
191
205
|
|
|
192
206
|
# Count critical errors by checking severity method
|
|
193
|
-
critical_errors =
|
|
207
|
+
critical_errors = base_scope
|
|
194
208
|
.where(platform: platform)
|
|
195
209
|
.where("occurred_at >= ?", @start_date)
|
|
196
210
|
.select { |error| error.severity == :critical }
|
|
197
211
|
.count
|
|
198
212
|
|
|
199
|
-
unresolved_errors =
|
|
213
|
+
unresolved_errors = base_scope
|
|
200
214
|
.where(platform: platform, resolved_at: nil)
|
|
201
215
|
.where("occurred_at >= ?", @start_date)
|
|
202
216
|
.count
|
|
203
217
|
|
|
204
|
-
resolved_errors =
|
|
218
|
+
resolved_errors = base_scope
|
|
205
219
|
.where(platform: platform)
|
|
206
220
|
.where.not(resolved_at: nil)
|
|
207
221
|
.where("occurred_at >= ?", @start_date)
|
|
@@ -210,12 +224,12 @@ module RailsErrorDashboard
|
|
|
210
224
|
resolution_rate = total_errors.positive? ? ((resolved_errors.to_f / total_errors) * 100).round(1) : 0.0
|
|
211
225
|
|
|
212
226
|
# Calculate error velocity (increasing or decreasing)
|
|
213
|
-
first_half =
|
|
227
|
+
first_half = base_scope
|
|
214
228
|
.where(platform: platform)
|
|
215
229
|
.where("occurred_at >= ? AND occurred_at < ?", @start_date, @start_date + (@days / 2.0).days)
|
|
216
230
|
.count
|
|
217
231
|
|
|
218
|
-
second_half =
|
|
232
|
+
second_half = base_scope
|
|
219
233
|
.where(platform: platform)
|
|
220
234
|
.where("occurred_at >= ?", @start_date + (@days / 2.0).days)
|
|
221
235
|
.count
|
|
@@ -5,12 +5,13 @@ module RailsErrorDashboard
|
|
|
5
5
|
# Query: Analyze recurring and persistent errors
|
|
6
6
|
# Returns data about high-frequency errors, persistent issues, and cyclical patterns
|
|
7
7
|
class RecurringIssues
|
|
8
|
-
def self.call(days = 30)
|
|
9
|
-
new(days).call
|
|
8
|
+
def self.call(days = 30, application_id: nil)
|
|
9
|
+
new(days, application_id: application_id).call
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def initialize(days = 30)
|
|
12
|
+
def initialize(days = 30, application_id: nil)
|
|
13
13
|
@days = days
|
|
14
|
+
@application_id = application_id
|
|
14
15
|
@start_date = days.days.ago
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -25,7 +26,9 @@ module RailsErrorDashboard
|
|
|
25
26
|
private
|
|
26
27
|
|
|
27
28
|
def base_query
|
|
28
|
-
ErrorLog.where("occurred_at >= ?", @start_date)
|
|
29
|
+
scope = ErrorLog.where("occurred_at >= ?", @start_date)
|
|
30
|
+
scope = scope.where(application_id: @application_id) if @application_id.present?
|
|
31
|
+
scope
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def high_frequency_errors
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_error_dashboard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.25
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -83,16 +83,16 @@ dependencies:
|
|
|
83
83
|
name: httparty
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
|
-
- - "
|
|
86
|
+
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version:
|
|
88
|
+
version: 0.24.0
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
|
-
- - "
|
|
93
|
+
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version:
|
|
95
|
+
version: 0.24.0
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: turbo-rails
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -386,7 +386,7 @@ metadata:
|
|
|
386
386
|
source_code_uri: https://github.com/AnjanJ/rails_error_dashboard
|
|
387
387
|
changelog_uri: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md
|
|
388
388
|
post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
|
|
389
|
-
\ Rails Error Dashboard v0.1.
|
|
389
|
+
\ Rails Error Dashboard v0.1.25\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
|
|
390
390
|
First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
|
|
391
391
|
db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
|
|
392
392
|
=> '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n
|