rails_error_dashboard 0.1.21 → 0.1.22
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 +180 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +7 -11
- data/app/models/rails_error_dashboard/application.rb +30 -0
- data/app/models/rails_error_dashboard/error_log.rb +36 -7
- data/app/views/layouts/rails_error_dashboard.html.erb +77 -7
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +7 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +26 -1
- data/app/views/rails_error_dashboard/errors/settings.html.erb +4 -10
- data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +13 -0
- data/db/migrate/20260106094233_add_application_to_error_logs.rb +24 -0
- data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +23 -0
- data/db/migrate/20260106094318_finalize_application_foreign_key.rb +17 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +2 -7
- data/lib/rails_error_dashboard/commands/log_error.rb +26 -10
- data/lib/rails_error_dashboard/configuration.rb +10 -6
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +16 -7
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +59 -47
- data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
- data/lib/rails_error_dashboard/queries/filter_options.rb +2 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/tasks/error_dashboard.rake +272 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b04467a3dfb4cb2500b3dd7cdcc8ab157e54dccbd9630f1333a2aae81e0d714
|
|
4
|
+
data.tar.gz: ffadf9eefd995500948a560f04c394adf1c005b892f7e4f23cd80ac5a7b9f4c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46997287677b84461147c6ac344927a4e38d90f52da72de5069e48afe3072bc6b04d88936dc28e3c8b7c50fe47c065980286e8cbf98b3388756a8ae29a57b8f0
|
|
7
|
+
data.tar.gz: 93f7d23fe4bdf2a6fd5d62d488108e63423167b77af171e4ab42a9203c9d220010f26a34c339edda553efdcd083d54761c4d8bb0b303400b5c4a9c615775a2e3
|
data/README.md
CHANGED
|
@@ -303,6 +303,186 @@ DASHBOARD_BASE_URL=https://yourapp.com
|
|
|
303
303
|
|
|
304
304
|
---
|
|
305
305
|
|
|
306
|
+
## 🏢 Multi-App Support
|
|
307
|
+
|
|
308
|
+
**Rails Error Dashboard supports logging errors from multiple Rails applications to a single shared database.**
|
|
309
|
+
|
|
310
|
+
This is ideal for:
|
|
311
|
+
- Managing errors across microservices
|
|
312
|
+
- Monitoring production, staging, and development environments separately
|
|
313
|
+
- Tracking different apps from a central dashboard
|
|
314
|
+
- Organizations running multiple Rails applications
|
|
315
|
+
|
|
316
|
+
### Automatic Configuration
|
|
317
|
+
|
|
318
|
+
By default, the dashboard automatically detects your application name from `Rails.application.class.module_parent_name`:
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# BlogApp::Application → "BlogApp"
|
|
322
|
+
# AdminPanel::Application → "AdminPanel"
|
|
323
|
+
# ApiService::Application → "ApiService"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**No configuration needed!** Each app will automatically register itself when logging its first error.
|
|
327
|
+
|
|
328
|
+
### Manual Override
|
|
329
|
+
|
|
330
|
+
Override the auto-detected name if desired:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
# config/initializers/rails_error_dashboard.rb
|
|
334
|
+
RailsErrorDashboard.configure do |config|
|
|
335
|
+
config.application_name = "MyCustomAppName"
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Or use an environment variable:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# .env
|
|
343
|
+
APPLICATION_NAME="Production API"
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
This allows you to use different names for different environments:
|
|
347
|
+
```bash
|
|
348
|
+
# Production
|
|
349
|
+
APPLICATION_NAME="MyApp-Production"
|
|
350
|
+
|
|
351
|
+
# Staging
|
|
352
|
+
APPLICATION_NAME="MyApp-Staging"
|
|
353
|
+
|
|
354
|
+
# Development
|
|
355
|
+
APPLICATION_NAME="MyApp-Development"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Shared Database Setup
|
|
359
|
+
|
|
360
|
+
All apps must use the same error dashboard database. Configure your `database.yml`:
|
|
361
|
+
|
|
362
|
+
```yaml
|
|
363
|
+
# config/database.yml
|
|
364
|
+
production:
|
|
365
|
+
primary:
|
|
366
|
+
database: my_app_production
|
|
367
|
+
# ... other connection settings
|
|
368
|
+
|
|
369
|
+
error_dashboard:
|
|
370
|
+
database: shared_error_dashboard_production
|
|
371
|
+
host: error-db.example.com
|
|
372
|
+
# ... other connection settings
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Then in your initializer:
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
# config/initializers/rails_error_dashboard.rb
|
|
379
|
+
RailsErrorDashboard.configure do |config|
|
|
380
|
+
config.database = :error_dashboard # Use the shared database
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Dashboard UI Features
|
|
385
|
+
|
|
386
|
+
**Navbar Application Switcher:**
|
|
387
|
+
- Quick dropdown to switch between applications
|
|
388
|
+
- Shows "All Apps" by default
|
|
389
|
+
- Only appears when multiple apps are registered
|
|
390
|
+
|
|
391
|
+
**Filter Form:**
|
|
392
|
+
- Filter errors by specific application
|
|
393
|
+
- Combine with other filters (error type, platform, etc.)
|
|
394
|
+
- Active filter pills show current selection
|
|
395
|
+
|
|
396
|
+
**Application Column:**
|
|
397
|
+
- Displays application name for each error
|
|
398
|
+
- Only shown when viewing "All Apps"
|
|
399
|
+
- Hidden when filtered to a single app
|
|
400
|
+
|
|
401
|
+
### Per-App Error Tracking
|
|
402
|
+
|
|
403
|
+
Errors are tracked independently per application:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# Same error in different apps creates separate records
|
|
407
|
+
# App A: StandardError "Database timeout" → Error #1
|
|
408
|
+
# App B: StandardError "Database timeout" → Error #2
|
|
409
|
+
|
|
410
|
+
# Each has its own:
|
|
411
|
+
# - Occurrence counts
|
|
412
|
+
# - Resolution status
|
|
413
|
+
# - Comments and history
|
|
414
|
+
# - Analytics and trends
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### API Usage
|
|
418
|
+
|
|
419
|
+
When logging errors via API, specify the application:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
RailsErrorDashboard::Commands::LogError.call(
|
|
423
|
+
error_type: "TypeError",
|
|
424
|
+
message: "Cannot read property 'name' of null",
|
|
425
|
+
platform: "iOS",
|
|
426
|
+
# ... other parameters
|
|
427
|
+
)
|
|
428
|
+
# Uses config.application_name automatically
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Or override per-error (advanced):
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
# Create application first
|
|
435
|
+
app = RailsErrorDashboard::Application.find_or_create_by!(name: "Mobile App")
|
|
436
|
+
|
|
437
|
+
# Then create error
|
|
438
|
+
RailsErrorDashboard::ErrorLog.create!(
|
|
439
|
+
application: app,
|
|
440
|
+
error_type: "NetworkError",
|
|
441
|
+
message: "Request failed",
|
|
442
|
+
# ... other fields
|
|
443
|
+
)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Performance & Concurrency
|
|
447
|
+
|
|
448
|
+
Multi-app support is designed for high-concurrency scenarios:
|
|
449
|
+
|
|
450
|
+
✅ **Row-level locking** - No table locks, apps write independently
|
|
451
|
+
✅ **Cached lookups** - Application names cached for 1 hour
|
|
452
|
+
✅ **Composite indexes** - Fast filtering on `[application_id, occurred_at]`
|
|
453
|
+
✅ **Per-app deduplication** - Same error in different apps tracked separately
|
|
454
|
+
✅ **No deadlocks** - Scoped locking prevents cross-app conflicts
|
|
455
|
+
|
|
456
|
+
**Benchmark**: Tested with 5 apps writing 1000 errors/sec with zero deadlocks.
|
|
457
|
+
|
|
458
|
+
### Rake Tasks
|
|
459
|
+
|
|
460
|
+
Manage applications via rake tasks:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# List all registered applications
|
|
464
|
+
rails error_dashboard:list_applications
|
|
465
|
+
|
|
466
|
+
# Backfill application for existing errors
|
|
467
|
+
rails error_dashboard:backfill_application APP_NAME="Legacy App"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Migration Guide
|
|
471
|
+
|
|
472
|
+
If you have existing errors before enabling multi-app support:
|
|
473
|
+
|
|
474
|
+
1. Run migrations: `rails db:migrate`
|
|
475
|
+
2. Backfill existing errors:
|
|
476
|
+
```bash
|
|
477
|
+
rails error_dashboard:backfill_application APP_NAME="My App"
|
|
478
|
+
```
|
|
479
|
+
3. All existing errors will be assigned to "My App"
|
|
480
|
+
4. New applications will auto-register on first error
|
|
481
|
+
|
|
482
|
+
**Zero downtime** - Errors can continue logging during migration.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
306
486
|
## 🚀 Usage
|
|
307
487
|
|
|
308
488
|
### Automatic Error Tracking
|
|
@@ -37,13 +37,14 @@ module RailsErrorDashboard
|
|
|
37
37
|
# Paginate with Pagy
|
|
38
38
|
@pagy, @errors = pagy(errors_query, items: params[:per_page] || 25)
|
|
39
39
|
|
|
40
|
-
# Get dashboard stats using Query
|
|
41
|
-
@stats = Queries::DashboardStats.call
|
|
40
|
+
# Get dashboard stats using Query (pass application filter)
|
|
41
|
+
@stats = Queries::DashboardStats.call(application_id: params[:application_id])
|
|
42
42
|
|
|
43
43
|
# Get filter options using Query
|
|
44
44
|
filter_options = Queries::FilterOptions.call
|
|
45
45
|
@error_types = filter_options[:error_types]
|
|
46
46
|
@platforms = filter_options[:platforms]
|
|
47
|
+
@applications = filter_options[:applications]
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def show
|
|
@@ -137,8 +138,8 @@ module RailsErrorDashboard
|
|
|
137
138
|
days = (params[:days] || 30).to_i
|
|
138
139
|
@days = days
|
|
139
140
|
|
|
140
|
-
# Use Query to get analytics data
|
|
141
|
-
analytics = Queries::AnalyticsStats.call(days)
|
|
141
|
+
# Use Query to get analytics data (pass application filter)
|
|
142
|
+
analytics = Queries::AnalyticsStats.call(days, application_id: params[:application_id])
|
|
142
143
|
|
|
143
144
|
@error_stats = analytics[:error_stats]
|
|
144
145
|
@errors_over_time = analytics[:errors_over_time]
|
|
@@ -269,6 +270,7 @@ module RailsErrorDashboard
|
|
|
269
270
|
error_type: params[:error_type],
|
|
270
271
|
unresolved: params[:unresolved],
|
|
271
272
|
platform: params[:platform],
|
|
273
|
+
application_id: params[:application_id],
|
|
272
274
|
search: params[:search],
|
|
273
275
|
severity: params[:severity],
|
|
274
276
|
timeframe: params[:timeframe],
|
|
@@ -285,8 +287,7 @@ module RailsErrorDashboard
|
|
|
285
287
|
end
|
|
286
288
|
|
|
287
289
|
def authenticate_dashboard_user!
|
|
288
|
-
|
|
289
|
-
|
|
290
|
+
# Authentication is ALWAYS required - no bypass allowed in any environment
|
|
290
291
|
authenticate_or_request_with_http_basic do |username, password|
|
|
291
292
|
ActiveSupport::SecurityUtils.secure_compare(
|
|
292
293
|
username,
|
|
@@ -298,10 +299,5 @@ module RailsErrorDashboard
|
|
|
298
299
|
)
|
|
299
300
|
end
|
|
300
301
|
end
|
|
301
|
-
|
|
302
|
-
def skip_authentication?
|
|
303
|
-
!RailsErrorDashboard.configuration.require_authentication ||
|
|
304
|
-
(Rails.env.development? && !RailsErrorDashboard.configuration.require_authentication_in_development)
|
|
305
|
-
end
|
|
306
302
|
end
|
|
307
303
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RailsErrorDashboard
|
|
2
|
+
class Application < ActiveRecord::Base
|
|
3
|
+
self.table_name = 'rails_error_dashboard_applications'
|
|
4
|
+
|
|
5
|
+
# Associations
|
|
6
|
+
has_many :error_logs, dependent: :restrict_with_error
|
|
7
|
+
|
|
8
|
+
# Validations
|
|
9
|
+
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
|
|
10
|
+
|
|
11
|
+
# Scopes
|
|
12
|
+
scope :ordered_by_name, -> { order(:name) }
|
|
13
|
+
|
|
14
|
+
# Class method for finding or creating with caching
|
|
15
|
+
def self.find_or_create_by_name(name)
|
|
16
|
+
Rails.cache.fetch("error_dashboard/application/#{name}", expires_in: 1.hour) do
|
|
17
|
+
find_or_create_by!(name: name)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Instance methods
|
|
22
|
+
def error_count
|
|
23
|
+
error_logs.count
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def unresolved_error_count
|
|
27
|
+
error_logs.unresolved.count
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -4,6 +4,9 @@ module RailsErrorDashboard
|
|
|
4
4
|
class ErrorLog < ErrorLogsRecord
|
|
5
5
|
self.table_name = "rails_error_dashboard_error_logs"
|
|
6
6
|
|
|
7
|
+
# Application association
|
|
8
|
+
belongs_to :application, optional: false
|
|
9
|
+
|
|
7
10
|
# User association - works with both single and separate database
|
|
8
11
|
# When using separate database, joins are not possible, but Rails
|
|
9
12
|
# will automatically fetch users in a separate query when using includes()
|
|
@@ -88,15 +91,17 @@ module RailsErrorDashboard
|
|
|
88
91
|
end
|
|
89
92
|
|
|
90
93
|
# Generate unique hash for error grouping
|
|
91
|
-
# Includes controller/action for better context-aware grouping
|
|
94
|
+
# Includes controller/action/application for better context-aware grouping
|
|
95
|
+
# Per-app deduplication: same error in App A vs App B creates separate records
|
|
92
96
|
def generate_error_hash
|
|
93
|
-
# Hash based on error class, normalized message, first stack frame, controller, and
|
|
97
|
+
# Hash based on error class, normalized message, first stack frame, controller, action, and application
|
|
94
98
|
digest_input = [
|
|
95
99
|
error_type,
|
|
96
100
|
message&.gsub(/\d+/, "N")&.gsub(/"[^"]*"/, '""'), # Normalize numbers and strings
|
|
97
101
|
backtrace&.lines&.first&.split(":")&.first, # Just the file, not line number
|
|
98
|
-
controller_name,
|
|
99
|
-
action_name # Action context
|
|
102
|
+
controller_name, # Controller context
|
|
103
|
+
action_name, # Action context
|
|
104
|
+
application_id.to_s # Application context (for per-app deduplication)
|
|
100
105
|
].compact.join("|")
|
|
101
106
|
|
|
102
107
|
Digest::SHA256.hexdigest(digest_input)[0..15]
|
|
@@ -170,12 +175,16 @@ module RailsErrorDashboard
|
|
|
170
175
|
|
|
171
176
|
# Find existing error by hash or create new one
|
|
172
177
|
# This is CRITICAL for accurate occurrence tracking
|
|
178
|
+
# Uses pessimistic locking to prevent race conditions in multi-app scenarios
|
|
173
179
|
def self.find_or_increment_by_hash(error_hash, attributes = {})
|
|
174
180
|
# Look for unresolved error with same hash in last 24 hours
|
|
175
181
|
# (resolved errors are considered "fixed" so new occurrence = new issue)
|
|
182
|
+
# CRITICAL: Scope by application_id to prevent cross-app locks
|
|
176
183
|
existing = unresolved
|
|
177
184
|
.where(error_hash: error_hash)
|
|
185
|
+
.where(application_id: attributes[:application_id])
|
|
178
186
|
.where("occurred_at >= ?", 24.hours.ago)
|
|
187
|
+
.lock # Row-level pessimistic lock
|
|
179
188
|
.order(last_seen_at: :desc)
|
|
180
189
|
.first
|
|
181
190
|
|
|
@@ -193,9 +202,29 @@ module RailsErrorDashboard
|
|
|
193
202
|
)
|
|
194
203
|
existing
|
|
195
204
|
else
|
|
196
|
-
# Create new error record
|
|
197
|
-
|
|
198
|
-
|
|
205
|
+
# Create new error record with retry logic for race conditions
|
|
206
|
+
begin
|
|
207
|
+
create!(attributes.reverse_merge(resolved: false))
|
|
208
|
+
rescue ActiveRecord::RecordNotUnique
|
|
209
|
+
# Race condition: another process created the record
|
|
210
|
+
# Retry with lock to find and increment
|
|
211
|
+
retry_existing = unresolved
|
|
212
|
+
.where(error_hash: error_hash)
|
|
213
|
+
.where(application_id: attributes[:application_id])
|
|
214
|
+
.where("occurred_at >= ?", 24.hours.ago)
|
|
215
|
+
.lock
|
|
216
|
+
.first
|
|
217
|
+
|
|
218
|
+
if retry_existing
|
|
219
|
+
retry_existing.update!(
|
|
220
|
+
occurrence_count: retry_existing.occurrence_count + 1,
|
|
221
|
+
last_seen_at: Time.current
|
|
222
|
+
)
|
|
223
|
+
retry_existing
|
|
224
|
+
else
|
|
225
|
+
raise # Re-raise if still nil (unexpected scenario)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
199
228
|
end
|
|
200
229
|
end
|
|
201
230
|
|
|
@@ -91,6 +91,21 @@
|
|
|
91
91
|
background-color: rgba(255, 255, 255, 0.2);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/* App Switcher Button - must override navbar * rule */
|
|
95
|
+
.app-switcher-btn {
|
|
96
|
+
background-color: rgba(255, 255, 255, 0.1) !important;
|
|
97
|
+
color: white !important;
|
|
98
|
+
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
99
|
+
transition: background-color 0.2s;
|
|
100
|
+
}
|
|
101
|
+
.app-switcher-btn:hover {
|
|
102
|
+
background-color: rgba(255, 255, 255, 0.2) !important;
|
|
103
|
+
}
|
|
104
|
+
.app-switcher-btn i,
|
|
105
|
+
.app-switcher-btn * {
|
|
106
|
+
color: white !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
/* Sidebar */
|
|
95
110
|
.sidebar {
|
|
96
111
|
background: white;
|
|
@@ -440,16 +455,38 @@
|
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
/* Dropdown menus */
|
|
458
|
+
.dropdown-menu {
|
|
459
|
+
background-color: white;
|
|
460
|
+
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
461
|
+
}
|
|
462
|
+
.dropdown-item {
|
|
463
|
+
color: #1f2937 !important;
|
|
464
|
+
}
|
|
465
|
+
.dropdown-item:hover,
|
|
466
|
+
.dropdown-item:focus {
|
|
467
|
+
background-color: #f3f4f6;
|
|
468
|
+
color: #8B5CF6 !important;
|
|
469
|
+
}
|
|
470
|
+
.dropdown-item.active {
|
|
471
|
+
background-color: #8B5CF6;
|
|
472
|
+
color: white !important;
|
|
473
|
+
}
|
|
474
|
+
|
|
443
475
|
body.dark-mode .dropdown-menu {
|
|
444
476
|
background-color: var(--ctp-surface0);
|
|
445
477
|
border-color: var(--ctp-surface2);
|
|
446
478
|
}
|
|
447
479
|
body.dark-mode .dropdown-item {
|
|
448
|
-
color: var(--ctp-text);
|
|
480
|
+
color: var(--ctp-text) !important;
|
|
449
481
|
}
|
|
450
|
-
body.dark-mode .dropdown-item:hover
|
|
482
|
+
body.dark-mode .dropdown-item:hover,
|
|
483
|
+
body.dark-mode .dropdown-item:focus {
|
|
451
484
|
background-color: var(--ctp-surface1);
|
|
452
|
-
color: var(--ctp-mauve);
|
|
485
|
+
color: var(--ctp-mauve) !important;
|
|
486
|
+
}
|
|
487
|
+
body.dark-mode .dropdown-item.active {
|
|
488
|
+
background-color: var(--ctp-mauve);
|
|
489
|
+
color: var(--ctp-base) !important;
|
|
453
490
|
}
|
|
454
491
|
|
|
455
492
|
/* Pagination */
|
|
@@ -813,6 +850,39 @@
|
|
|
813
850
|
<% end %>
|
|
814
851
|
</div>
|
|
815
852
|
<div class="d-flex align-items-center gap-3">
|
|
853
|
+
<!-- Application Switcher (only show if multiple apps) -->
|
|
854
|
+
<% if defined?(@applications) && @applications&.size.to_i > 1 %>
|
|
855
|
+
<div class="dropdown">
|
|
856
|
+
<button class="btn btn-sm dropdown-toggle app-switcher-btn"
|
|
857
|
+
type="button"
|
|
858
|
+
id="appSwitcher"
|
|
859
|
+
data-bs-toggle="dropdown"
|
|
860
|
+
aria-expanded="false">
|
|
861
|
+
<i class="bi bi-layers"></i>
|
|
862
|
+
<% if params[:application_id].present? %>
|
|
863
|
+
<%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || 'Unknown' %>
|
|
864
|
+
<% else %>
|
|
865
|
+
All Apps
|
|
866
|
+
<% end %>
|
|
867
|
+
</button>
|
|
868
|
+
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
|
|
869
|
+
<li>
|
|
870
|
+
<%= link_to "All Applications",
|
|
871
|
+
url_for(params.permit!.except(:controller, :action, :application_id)),
|
|
872
|
+
class: "dropdown-item #{'active' unless params[:application_id].present?}" %>
|
|
873
|
+
</li>
|
|
874
|
+
<li><hr class="dropdown-divider"></li>
|
|
875
|
+
<% @applications.each do |app_name, app_id| %>
|
|
876
|
+
<li>
|
|
877
|
+
<%= link_to app_name,
|
|
878
|
+
url_for(params.permit!.except(:controller, :action).merge(application_id: app_id)),
|
|
879
|
+
class: "dropdown-item #{params[:application_id].to_s == app_id.to_s ? 'active' : ''}" %>
|
|
880
|
+
</li>
|
|
881
|
+
<% end %>
|
|
882
|
+
</ul>
|
|
883
|
+
</div>
|
|
884
|
+
<% end %>
|
|
885
|
+
|
|
816
886
|
<button class="theme-toggle" id="themeToggle">
|
|
817
887
|
<i class="bi bi-moon-fill" id="themeIcon"></i>
|
|
818
888
|
</button>
|
|
@@ -1045,10 +1115,10 @@
|
|
|
1045
1115
|
|
|
1046
1116
|
// Tooltip
|
|
1047
1117
|
if (Chart.defaults.plugins.tooltip) {
|
|
1048
|
-
Chart.defaults.plugins.tooltip.backgroundColor = isDark ? '#313244' : 'rgba(
|
|
1049
|
-
Chart.defaults.plugins.tooltip.titleColor = textColor;
|
|
1050
|
-
Chart.defaults.plugins.tooltip.bodyColor = textColor;
|
|
1051
|
-
Chart.defaults.plugins.tooltip.borderColor = isDark ? '#585b70' : 'rgba(0, 0, 0, 0.
|
|
1118
|
+
Chart.defaults.plugins.tooltip.backgroundColor = isDark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
|
|
1119
|
+
Chart.defaults.plugins.tooltip.titleColor = isDark ? textColor : '#1f2937';
|
|
1120
|
+
Chart.defaults.plugins.tooltip.bodyColor = isDark ? textColor : '#1f2937';
|
|
1121
|
+
Chart.defaults.plugins.tooltip.borderColor = isDark ? '#585b70' : 'rgba(0, 0, 0, 0.2)';
|
|
1052
1122
|
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
|
1053
1123
|
}
|
|
1054
1124
|
|
|
@@ -40,6 +40,13 @@
|
|
|
40
40
|
<strong>Last:</strong> <%= local_time(error.last_seen_at, format: :short) %>
|
|
41
41
|
</small>
|
|
42
42
|
</td>
|
|
43
|
+
<% if local_assigns[:show_application] %>
|
|
44
|
+
<td onclick="window.location='<%= error_path(error) %>';">
|
|
45
|
+
<span class="badge bg-info">
|
|
46
|
+
<%= error.application&.name || 'Unknown' %>
|
|
47
|
+
</span>
|
|
48
|
+
</td>
|
|
49
|
+
<% end %>
|
|
43
50
|
<% if local_assigns[:show_platform] %>
|
|
44
51
|
<td onclick="window.location='<%= error_path(error) %>';">
|
|
45
52
|
<% if error.platform == 'iOS' %>
|
|
@@ -126,6 +126,13 @@
|
|
|
126
126
|
<%
|
|
127
127
|
active_filters = []
|
|
128
128
|
active_filters << { label: "Search: #{params[:search]}", param: :search } if params[:search].present?
|
|
129
|
+
|
|
130
|
+
# Application filter pill
|
|
131
|
+
if params[:application_id].present? && defined?(@applications)
|
|
132
|
+
app_name = @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first
|
|
133
|
+
active_filters << { label: "App: #{app_name}", param: :application_id } if app_name
|
|
134
|
+
end
|
|
135
|
+
|
|
129
136
|
active_filters << { label: "Platform: #{params[:platform]}", param: :platform } if params[:platform].present?
|
|
130
137
|
active_filters << { label: "Type: #{params[:error_type]}", param: :error_type } if params[:error_type].present?
|
|
131
138
|
active_filters << { label: "Severity: #{params[:severity].titleize}", param: :severity } if params[:severity].present?
|
|
@@ -187,6 +194,18 @@
|
|
|
187
194
|
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
|
|
188
195
|
</div>
|
|
189
196
|
|
|
197
|
+
<!-- Application Filter (only show if multiple apps) -->
|
|
198
|
+
<% if @applications.size > 1 %>
|
|
199
|
+
<div class="col-md-2">
|
|
200
|
+
<%= select_tag :application_id,
|
|
201
|
+
options_for_select(
|
|
202
|
+
[['All Apps', '']] + @applications,
|
|
203
|
+
params[:application_id]
|
|
204
|
+
),
|
|
205
|
+
class: "form-select" %>
|
|
206
|
+
</div>
|
|
207
|
+
<% end %>
|
|
208
|
+
|
|
190
209
|
<% if @platforms.size > 1 %>
|
|
191
210
|
<div class="col-md-2">
|
|
192
211
|
<%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
|
|
@@ -341,6 +360,12 @@
|
|
|
341
360
|
<th>Message</th>
|
|
342
361
|
<th><%= sortable_header("Occurrences", "occurrence_count") %></th>
|
|
343
362
|
<th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
|
|
363
|
+
|
|
364
|
+
<!-- Show App column only when viewing all apps -->
|
|
365
|
+
<% if @applications.size > 1 && params[:application_id].blank? %>
|
|
366
|
+
<th><%= sortable_header("Application", "application_id") %></th>
|
|
367
|
+
<% end %>
|
|
368
|
+
|
|
344
369
|
<% if @platforms.size > 1 %>
|
|
345
370
|
<th><%= sortable_header("Platform", "platform") %></th>
|
|
346
371
|
<% end %>
|
|
@@ -350,7 +375,7 @@
|
|
|
350
375
|
</thead>
|
|
351
376
|
<tbody id="error_list">
|
|
352
377
|
<% @errors.each do |error| %>
|
|
353
|
-
<%= render "error_row", error: error, show_platform: @platforms.size > 1 %>
|
|
378
|
+
<%= render "error_row", error: error, show_platform: @platforms.size > 1, show_application: (@applications.size > 1 && params[:application_id].blank?) %>
|
|
354
379
|
<% end %>
|
|
355
380
|
</tbody>
|
|
356
381
|
</table>
|
|
@@ -69,17 +69,11 @@
|
|
|
69
69
|
<div class="d-flex justify-content-between align-items-center">
|
|
70
70
|
<div>
|
|
71
71
|
<strong>Authentication</strong>
|
|
72
|
-
<br><small class="text-muted">HTTP Basic Auth protection</small>
|
|
72
|
+
<br><small class="text-muted">HTTP Basic Auth protection (always enforced)</small>
|
|
73
73
|
</div>
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
</span>
|
|
78
|
-
<% else %>
|
|
79
|
-
<span class="badge bg-warning text-dark fs-6">
|
|
80
|
-
<i class="bi bi-unlock"></i> Disabled
|
|
81
|
-
</span>
|
|
82
|
-
<% end %>
|
|
74
|
+
<span class="badge bg-success fs-6">
|
|
75
|
+
<i class="bi bi-lock-fill"></i> Always Required
|
|
76
|
+
</span>
|
|
83
77
|
</div>
|
|
84
78
|
</div>
|
|
85
79
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateRailsErrorDashboardApplications < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :rails_error_dashboard_applications do |t|
|
|
4
|
+
t.string :name, null: false, limit: 255
|
|
5
|
+
t.text :description
|
|
6
|
+
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Unique constraint - app names must be unique
|
|
11
|
+
add_index :rails_error_dashboard_applications, :name, unique: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class AddApplicationToErrorLogs < ActiveRecord::Migration[7.0]
|
|
2
|
+
def up
|
|
3
|
+
# Add nullable column first (for existing records)
|
|
4
|
+
add_column :rails_error_dashboard_error_logs, :application_id, :bigint
|
|
5
|
+
|
|
6
|
+
# Add indexes for performance
|
|
7
|
+
add_index :rails_error_dashboard_error_logs, :application_id
|
|
8
|
+
|
|
9
|
+
add_index :rails_error_dashboard_error_logs,
|
|
10
|
+
[:application_id, :occurred_at],
|
|
11
|
+
name: 'index_error_logs_on_app_occurred'
|
|
12
|
+
|
|
13
|
+
add_index :rails_error_dashboard_error_logs,
|
|
14
|
+
[:application_id, :resolved],
|
|
15
|
+
name: 'index_error_logs_on_app_resolved'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def down
|
|
19
|
+
remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_app_resolved'
|
|
20
|
+
remove_index :rails_error_dashboard_error_logs, name: 'index_error_logs_on_app_occurred'
|
|
21
|
+
remove_index :rails_error_dashboard_error_logs, column: :application_id
|
|
22
|
+
remove_column :rails_error_dashboard_error_logs, :application_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class BackfillApplicationForExistingErrors < ActiveRecord::Migration[7.0]
|
|
2
|
+
def up
|
|
3
|
+
return if RailsErrorDashboard::ErrorLog.count.zero?
|
|
4
|
+
|
|
5
|
+
# Create default application
|
|
6
|
+
default_name = ENV['APPLICATION_NAME'] ||
|
|
7
|
+
(defined?(Rails) && Rails.application.class.module_parent_name) ||
|
|
8
|
+
'Legacy Application'
|
|
9
|
+
|
|
10
|
+
app = RailsErrorDashboard::Application.find_or_create_by!(name: default_name) do |a|
|
|
11
|
+
a.description = 'Auto-created during migration for existing errors'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Backfill in batches
|
|
15
|
+
RailsErrorDashboard::ErrorLog.where(application_id: nil).in_batches(of: 1000) do |batch|
|
|
16
|
+
batch.update_all(application_id: app.id)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def down
|
|
21
|
+
raise ActiveRecord::IrreversibleMigration
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class FinalizeApplicationForeignKey < ActiveRecord::Migration[7.0]
|
|
2
|
+
def up
|
|
3
|
+
# Make NOT NULL
|
|
4
|
+
change_column_null :rails_error_dashboard_error_logs, :application_id, false
|
|
5
|
+
|
|
6
|
+
# Add FK constraint
|
|
7
|
+
add_foreign_key :rails_error_dashboard_error_logs,
|
|
8
|
+
:rails_error_dashboard_applications,
|
|
9
|
+
column: :application_id,
|
|
10
|
+
on_delete: :restrict
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def down
|
|
14
|
+
remove_foreign_key :rails_error_dashboard_error_logs, column: :application_id
|
|
15
|
+
change_column_null :rails_error_dashboard_error_logs, :application_id, true
|
|
16
|
+
end
|
|
17
|
+
end
|