rails_error_dashboard 0.1.6 → 0.1.7

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: 4d17d9a1383432925cda1b988f11b67d2cdfbb0ecc0f5e9b19ccd6016ac9b8bf
4
- data.tar.gz: 4a021c1c79014c8e14956cc1d1b10aee31413d3c001092594e306b1c4058251b
3
+ metadata.gz: b69a15c7ce0fdedb38654c9b4747fe8e1804419fd19ab15182182a02fa2d9fa6
4
+ data.tar.gz: d92fb59ecb07d9b59fbf2525e1d5a46bb0a59ff37cf259d20d3223e210d33fd9
5
5
  SHA512:
6
- metadata.gz: 5ef65729b6920e88d8d14654ef44716b0a8f387480b31c4f43d3354bc171ac94d74c4b144c8f2e489e9675fde2ca85c2757594aa1a428d73c9daa3691f9458b5
7
- data.tar.gz: 6fc3413345bf510fb38dedd6cf3236f119cdc6e3803c5c3e4efbd5bc4ce1e64f88bc30d3f609ee424566515eacbf01539bd11b8de6948bf574a732e8614ecdb5
6
+ metadata.gz: dbdb45fbc0015b4b293b8d3648403d68a117c43db16e5dff572317821fa3b0f9e9592ee6e921fef9fda6fd471126fe2c9b9f36a8d49bd1cbdda913c2d2833d51
7
+ data.tar.gz: 1da1a8275bf29468e2295334655748d00bc472ab99be2fca316e0217675ce86cbd8f07ba10b3113a7857a75d4d703231fcd743eb2b4c01fc3ed4f41d9cc9d084
data/README.md CHANGED
@@ -16,6 +16,14 @@ 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
+ ### 🎮 Try the Live Demo
20
+
21
+ **See it in action:** [https://rails-error-dashboard.anjan.dev](https://rails-error-dashboard.anjan.dev)
22
+
23
+ Username: `frodo` · Password: `precious`
24
+
25
+ Experience the full dashboard with 250+ realistic Rails errors, LOTR-themed demo data, and all features enabled.
26
+
19
27
  ---
20
28
 
21
29
  ### ⚠️ BETA SOFTWARE
@@ -406,6 +414,7 @@ config.webhook_urls = ['https://yourapp.com/hooks/errors']
406
414
 
407
415
  ### Development
408
416
  - **[Testing](docs/development/TESTING.md)** - Multi-version testing
417
+ - **[Smoke Tests](SMOKE_TESTS.md)** - Deployment verification tests
409
418
 
410
419
  **📖 [View all documentation →](docs/README.md)**
411
420
 
@@ -21,13 +21,13 @@ module RailsErrorDashboard
21
21
  end
22
22
 
23
23
  # Get critical alerts (critical/high severity errors from last hour)
24
+ # Filter by priority_level in database instead of loading all records into memory
24
25
  @critical_alerts = ErrorLog
25
26
  .where("occurred_at >= ?", 1.hour.ago)
26
27
  .where(resolved_at: nil)
27
- .select { |error| [ :critical, :high ].include?(error.severity) }
28
- .sort_by(&:occurred_at)
29
- .reverse
30
- .first(10)
28
+ .where(priority_level: [ 3, 4 ]) # 3 = high, 4 = critical (based on severity enum)
29
+ .order(occurred_at: :desc)
30
+ .limit(10)
31
31
  end
32
32
 
33
33
  def index
@@ -47,7 +47,10 @@ module RailsErrorDashboard
47
47
  end
48
48
 
49
49
  def show
50
- @error = ErrorLog.find(params[:id])
50
+ # Eagerly load associations to avoid N+1 queries
51
+ # - comments: Used in the comments section (@error.comments.count, @error.comments.recent_first)
52
+ # - parent_cascade_patterns/child_cascade_patterns: Used if cascade detection is enabled
53
+ @error = ErrorLog.includes(:comments, :parent_cascade_patterns, :child_cascade_patterns).find(params[:id])
51
54
  @related_errors = @error.related_errors(limit: 5)
52
55
 
53
56
  # Dispatch plugin event for error viewed
@@ -60,6 +60,10 @@ module RailsErrorDashboard
60
60
  after_create_commit :broadcast_new_error
61
61
  after_update_commit :broadcast_error_update
62
62
 
63
+ # Cache invalidation - clear analytics caches when errors are created/updated/deleted
64
+ after_save :clear_analytics_cache
65
+ after_destroy :clear_analytics_cache
66
+
63
67
  def set_defaults
64
68
  self.platform ||= "API"
65
69
  end
@@ -663,5 +667,18 @@ module RailsErrorDashboard
663
667
  100 # Default fallback
664
668
  end
665
669
  end
670
+
671
+ # Clear analytics caches when errors are created, updated, or destroyed
672
+ # This ensures dashboard and analytics always show fresh data
673
+ def clear_analytics_cache
674
+ # Use delete_matched to clear all cached analytics regardless of parameters
675
+ # Pattern matches: dashboard_stats/*, analytics_stats/*, platform_comparison/*
676
+ Rails.cache.delete_matched("dashboard_stats/*")
677
+ Rails.cache.delete_matched("analytics_stats/*")
678
+ Rails.cache.delete_matched("platform_comparison/*")
679
+ rescue => e
680
+ # Silently handle cache clearing errors to prevent blocking error logging
681
+ Rails.logger.error("Failed to clear analytics cache: #{e.message}") if Rails.logger
682
+ end
666
683
  end
667
684
  end
@@ -34,6 +34,7 @@
34
34
  <div class="row g-4">
35
35
  <!-- Error Information -->
36
36
  <div class="col-md-8">
37
+ <% cache [@error, 'error_details_v1'] do %>
37
38
  <div class="card mb-4">
38
39
  <div class="card-header bg-danger text-white">
39
40
  <h5 class="mb-0"><i class="bi bi-bug-fill"></i> <%= @error.error_type %></h5>
@@ -115,8 +116,10 @@
115
116
  <% end %>
116
117
  </div>
117
118
  </div>
119
+ <% end %>
118
120
 
119
121
  <!-- Request Context -->
122
+ <% cache [@error, 'request_context_v1'] do %>
120
123
  <div class="card mb-4">
121
124
  <div class="card-header bg-white">
122
125
  <h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
@@ -169,11 +172,13 @@
169
172
  </table>
170
173
  </div>
171
174
  </div>
175
+ <% end %>
172
176
 
173
177
  <!-- Similar Errors (Fuzzy Matching) -->
174
178
  <% if RailsErrorDashboard.configuration.enable_similar_errors && @error.respond_to?(:similar_errors) %>
175
179
  <% similar = @error.similar_errors(threshold: 0.6, limit: 5) %>
176
180
  <% if similar.any? %>
181
+ <% cache [@error, 'similar_errors_v1', similar.maximum(:updated_at)] do %>
177
182
  <div class="card mb-4">
178
183
  <div class="card-header bg-white">
179
184
  <h5 class="mb-0">
@@ -239,6 +244,7 @@
239
244
  </div>
240
245
  </div>
241
246
  </div>
247
+ <% end %>
242
248
  <% end %>
243
249
  <% end %>
244
250
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAdditionalPerformanceIndexes < ActiveRecord::Migration[7.0]
4
+ def change
5
+ # Composite index for workflow filtering (assigned errors with status)
6
+ # Common query: WHERE assigned_to = ? AND status = ? ORDER BY occurred_at DESC
7
+ # Used in: "Show me all errors assigned to John that are investigating"
8
+ add_index :rails_error_dashboard_error_logs, [ :assigned_to, :status, :occurred_at ],
9
+ name: 'index_error_logs_on_assignment_workflow',
10
+ if_not_exists: true
11
+
12
+ # Composite index for priority filtering with resolution status
13
+ # Common query: WHERE priority_level = ? AND resolved = ? ORDER BY occurred_at DESC
14
+ # Used in: "Show me all high priority unresolved errors"
15
+ add_index :rails_error_dashboard_error_logs, [ :priority_level, :resolved, :occurred_at ],
16
+ name: 'index_error_logs_on_priority_resolution',
17
+ if_not_exists: true
18
+
19
+ # Composite index for platform + status filtering (common in analytics)
20
+ # Common query: WHERE platform = ? AND status = ? ORDER BY occurred_at DESC
21
+ # Used in: "Show me all iOS errors that are new"
22
+ add_index :rails_error_dashboard_error_logs, [ :platform, :status, :occurred_at ],
23
+ name: 'index_error_logs_on_platform_status_time',
24
+ if_not_exists: true
25
+
26
+ # Composite index for version-based filtering
27
+ # Common query: WHERE app_version = ? AND resolved = ? ORDER BY occurred_at DESC
28
+ # Used in: "Show me all unresolved errors in version 2.1.0"
29
+ add_index :rails_error_dashboard_error_logs, [ :app_version, :resolved, :occurred_at ],
30
+ name: 'index_error_logs_on_version_resolution_time',
31
+ if_not_exists: true
32
+
33
+ # Composite index for snooze management
34
+ # Common query: WHERE snoozed_until IS NOT NULL AND snoozed_until < NOW()
35
+ # Used in: Finding errors that need to be unsnoozed
36
+ add_index :rails_error_dashboard_error_logs, [ :snoozed_until, :occurred_at ],
37
+ name: 'index_error_logs_on_snooze_time',
38
+ where: "snoozed_until IS NOT NULL",
39
+ if_not_exists: true
40
+
41
+ # Composite index for error hash lookups with time window
42
+ # Common query: WHERE error_hash = ? AND occurred_at >= ?
43
+ # Used in: Similar error detection within time windows
44
+ # Note: There's already an index on [error_hash, resolved, occurred_at]
45
+ # but this one is for time-based similarity without resolved filter
46
+
47
+ # Add GIN index for backtrace full-text search (PostgreSQL only)
48
+ # Improves search performance across both message and backtrace
49
+ if postgresql?
50
+ reversible do |dir|
51
+ dir.up do
52
+ execute <<-SQL
53
+ CREATE INDEX IF NOT EXISTS index_error_logs_on_searchable_text
54
+ ON rails_error_dashboard_error_logs
55
+ USING gin(to_tsvector('english',
56
+ COALESCE(message, '') || ' ' ||
57
+ COALESCE(backtrace, '') || ' ' ||
58
+ COALESCE(error_type, '')
59
+ ))
60
+ SQL
61
+ end
62
+
63
+ dir.down do
64
+ execute "DROP INDEX IF EXISTS index_error_logs_on_searchable_text"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def postgresql?
73
+ ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
74
+ end
75
+ end
@@ -60,6 +60,10 @@ module RailsErrorDashboard
60
60
  # Backtrace configuration
61
61
  attr_accessor :max_backtrace_lines
62
62
 
63
+ # Rate limiting configuration
64
+ attr_accessor :enable_rate_limiting
65
+ attr_accessor :rate_limit_per_minute
66
+
63
67
  # Enhanced metrics
64
68
  attr_accessor :app_version
65
69
  attr_accessor :git_sha
@@ -130,6 +134,10 @@ module RailsErrorDashboard
130
134
  @async_adapter = :sidekiq # Battle-tested default
131
135
  @max_backtrace_lines = 50
132
136
 
137
+ # Rate limiting defaults
138
+ @enable_rate_limiting = false # OFF by default (opt-in)
139
+ @rate_limit_per_minute = 100 # Requests per minute per IP for API endpoints
140
+
133
141
  # Enhanced metrics defaults
134
142
  @app_version = ENV["APP_VERSION"]
135
143
  @git_sha = ENV["GIT_SHA"]
@@ -8,6 +8,11 @@ module RailsErrorDashboard
8
8
  if RailsErrorDashboard.configuration.enable_middleware
9
9
  app.config.middleware.insert_before 0, RailsErrorDashboard::Middleware::ErrorCatcher
10
10
  end
11
+
12
+ # Add rate limiting middleware if enabled
13
+ if RailsErrorDashboard.configuration.enable_rate_limiting
14
+ app.config.middleware.use RailsErrorDashboard::Middleware::RateLimiter
15
+ end
11
16
  end
12
17
 
13
18
  # Subscribe to Rails error reporter
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Middleware
5
+ # Rate limiting middleware for Rails Error Dashboard routes
6
+ # Protects both dashboard UI and API endpoints from abuse
7
+ class RateLimiter
8
+ # Rate limits by endpoint type
9
+ LIMITS = {
10
+ # API endpoints (mobile/frontend) - stricter limits
11
+ "/error_dashboard/api" => { limit: 100, period: 60 }, # 100 req/min
12
+
13
+ # Dashboard pages (human users) - more lenient
14
+ "/error_dashboard" => { limit: 300, period: 60 } # 300 req/min
15
+ }.freeze
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ @cache = Rails.cache
20
+ end
21
+
22
+ def call(env)
23
+ return @app.call(env) unless enabled?
24
+
25
+ request = Rack::Request.new(env)
26
+
27
+ # Only apply rate limiting to error dashboard routes
28
+ return @app.call(env) unless error_dashboard_route?(request.path)
29
+
30
+ # Find matching rate limit configuration
31
+ limit_config = find_limit_config(request.path)
32
+ return @app.call(env) unless limit_config
33
+
34
+ # Check rate limit
35
+ key = rate_limit_key(request)
36
+ current_count = @cache.read(key).to_i
37
+
38
+ if current_count >= limit_config[:limit]
39
+ return rate_limit_response(request, limit_config)
40
+ end
41
+
42
+ # Increment counter with expiration
43
+ @cache.write(key, current_count + 1, expires_in: limit_config[:period].seconds)
44
+
45
+ @app.call(env)
46
+ end
47
+
48
+ private
49
+
50
+ def enabled?
51
+ RailsErrorDashboard.configuration.enable_rate_limiting
52
+ end
53
+
54
+ def error_dashboard_route?(path)
55
+ path.start_with?("/error_dashboard")
56
+ end
57
+
58
+ def find_limit_config(path)
59
+ # Match most specific route first (API before dashboard)
60
+ LIMITS.find { |pattern, _| path.start_with?(pattern) }&.last
61
+ end
62
+
63
+ def rate_limit_key(request)
64
+ # Key format: rate_limit:IP:path_prefix:time_window
65
+ # Time window ensures keys expire and reset
66
+ limit_config = find_limit_config(request.path)
67
+ time_window = Time.now.to_i / limit_config[:period]
68
+
69
+ "rate_limit:#{request.ip}:#{request.path}:#{time_window}"
70
+ end
71
+
72
+ def rate_limit_response(request, limit_config)
73
+ # Return JSON for API requests, HTML for dashboard
74
+ if request.path.start_with?("/error_dashboard/api")
75
+ json_rate_limit_response(limit_config)
76
+ else
77
+ html_rate_limit_response(limit_config)
78
+ end
79
+ end
80
+
81
+ def json_rate_limit_response(limit_config)
82
+ [
83
+ 429,
84
+ {
85
+ "Content-Type" => "application/json",
86
+ "Retry-After" => limit_config[:period].to_s,
87
+ "X-RateLimit-Limit" => limit_config[:limit].to_s,
88
+ "X-RateLimit-Period" => "#{limit_config[:period]} seconds"
89
+ },
90
+ [ { error: "Rate limit exceeded. Please try again later." }.to_json ]
91
+ ]
92
+ end
93
+
94
+ def html_rate_limit_response(limit_config)
95
+ body = <<~HTML
96
+ <!DOCTYPE html>
97
+ <html>
98
+ <head>
99
+ <title>Rate Limit Exceeded</title>
100
+ <style>
101
+ body {
102
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
103
+ max-width: 600px;
104
+ margin: 100px auto;
105
+ padding: 20px;
106
+ text-align: center;
107
+ }
108
+ h1 { color: #dc3545; }
109
+ p { color: #6c757d; line-height: 1.6; }
110
+ .code { background: #f8f9fa; padding: 10px; border-radius: 4px; margin: 20px 0; }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <h1>⚠️ Rate Limit Exceeded</h1>
115
+ <p>You've made too many requests to the error dashboard.</p>
116
+ <div class="code">
117
+ <strong>Limit:</strong> #{limit_config[:limit]} requests per #{limit_config[:period]} seconds
118
+ </div>
119
+ <p>Please wait a moment before trying again.</p>
120
+ </body>
121
+ </html>
122
+ HTML
123
+
124
+ [
125
+ 429,
126
+ {
127
+ "Content-Type" => "text/html",
128
+ "Retry-After" => limit_config[:period].to_s,
129
+ "X-RateLimit-Limit" => limit_config[:limit].to_s,
130
+ "X-RateLimit-Period" => "#{limit_config[:period]} seconds"
131
+ },
132
+ [ body ]
133
+ ]
134
+ end
135
+ end
136
+ end
137
+ end
@@ -15,19 +15,37 @@ module RailsErrorDashboard
15
15
  end
16
16
 
17
17
  def call
18
- {
19
- days: @days,
20
- error_stats: error_statistics,
21
- errors_over_time: errors_over_time,
22
- errors_by_type: errors_by_type,
23
- errors_by_platform: errors_by_platform,
24
- errors_by_hour: errors_by_hour,
25
- top_users: top_affected_users,
26
- resolution_rate: resolution_rate,
27
- mobile_errors: mobile_errors_count,
28
- api_errors: api_errors_count,
29
- pattern_insights: pattern_insights
30
- }
18
+ # Cache analytics data for 5 minutes to reduce database load
19
+ # Cache key includes days parameter and last error update timestamp
20
+ Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
21
+ {
22
+ days: @days,
23
+ error_stats: error_statistics,
24
+ errors_over_time: errors_over_time,
25
+ errors_by_type: errors_by_type,
26
+ errors_by_platform: errors_by_platform,
27
+ errors_by_hour: errors_by_hour,
28
+ top_users: top_affected_users,
29
+ resolution_rate: resolution_rate,
30
+ mobile_errors: mobile_errors_count,
31
+ api_errors: api_errors_count,
32
+ pattern_insights: pattern_insights
33
+ }
34
+ end
35
+ end
36
+
37
+ def cache_key
38
+ # Cache key includes:
39
+ # - Query class name
40
+ # - Days parameter (different time ranges = different caches)
41
+ # - Last error update timestamp (auto-invalidates when errors change)
42
+ # - Start date (ensures correct time window)
43
+ [
44
+ "analytics_stats",
45
+ @days,
46
+ ErrorLog.maximum(:updated_at)&.to_i || 0,
47
+ @start_date.to_date.to_s
48
+ ].join("/")
31
49
  end
32
50
 
33
51
  private
@@ -10,28 +10,42 @@ module RailsErrorDashboard
10
10
  end
11
11
 
12
12
  def call
13
- {
14
- total_today: ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day).count,
15
- total_week: ErrorLog.where("occurred_at >= ?", 7.days.ago).count,
16
- total_month: ErrorLog.where("occurred_at >= ?", 30.days.ago).count,
17
- unresolved: ErrorLog.unresolved.count,
18
- resolved: ErrorLog.resolved.count,
19
- by_platform: ErrorLog.group(:platform).count,
20
- top_errors: top_errors,
21
- # Trend visualizations
22
- errors_trend_7d: errors_trend_7d,
23
- errors_by_severity_7d: errors_by_severity_7d,
24
- spike_detected: spike_detected?,
25
- spike_info: spike_info,
26
- # New metrics for Overview dashboard
27
- error_rate: error_rate,
28
- affected_users_today: affected_users_today,
29
- affected_users_yesterday: affected_users_yesterday,
30
- affected_users_change: affected_users_change,
31
- trend_percentage: trend_percentage,
32
- trend_direction: trend_direction,
33
- top_errors_by_impact: top_errors_by_impact
34
- }
13
+ # Cache dashboard stats for 1 minute to reduce database load
14
+ # Dashboard is viewed frequently, so short cache prevents stale data
15
+ Rails.cache.fetch(cache_key, expires_in: 1.minute) do
16
+ {
17
+ total_today: ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day).count,
18
+ total_week: ErrorLog.where("occurred_at >= ?", 7.days.ago).count,
19
+ total_month: ErrorLog.where("occurred_at >= ?", 30.days.ago).count,
20
+ unresolved: ErrorLog.unresolved.count,
21
+ resolved: ErrorLog.resolved.count,
22
+ by_platform: ErrorLog.group(:platform).count,
23
+ top_errors: top_errors,
24
+ # Trend visualizations
25
+ errors_trend_7d: errors_trend_7d,
26
+ errors_by_severity_7d: errors_by_severity_7d,
27
+ spike_detected: spike_detected?,
28
+ spike_info: spike_info,
29
+ # New metrics for Overview dashboard
30
+ error_rate: error_rate,
31
+ affected_users_today: affected_users_today,
32
+ affected_users_yesterday: affected_users_yesterday,
33
+ affected_users_change: affected_users_change,
34
+ trend_percentage: trend_percentage,
35
+ trend_direction: trend_direction,
36
+ top_errors_by_impact: top_errors_by_impact
37
+ }
38
+ end
39
+ end
40
+
41
+ def cache_key
42
+ # Cache key includes last error update timestamp for auto-invalidation
43
+ # Also includes current hour to ensure fresh data
44
+ [
45
+ "dashboard_stats",
46
+ ErrorLog.maximum(:updated_at)&.to_i || 0,
47
+ Time.current.hour
48
+ ].join("/")
35
49
  end
36
50
 
37
51
  private
@@ -53,14 +67,19 @@ module RailsErrorDashboard
53
67
  end
54
68
 
55
69
  # Get error counts by severity for last 7 days
70
+ # OPTIMIZED: Use database filtering instead of loading all records into Ruby
56
71
  def errors_by_severity_7d
57
- last_7_days = ErrorLog.where("occurred_at >= ?", 7.days.ago)
72
+ base_scope = ErrorLog.where("occurred_at >= ?", 7.days.ago)
58
73
 
59
74
  {
60
- critical: last_7_days.select { |e| e.severity == :critical }.count,
61
- high: last_7_days.select { |e| e.severity == :high }.count,
62
- medium: last_7_days.select { |e| e.severity == :medium }.count,
63
- low: last_7_days.select { |e| e.severity == :low }.count
75
+ critical: base_scope.where(error_type: ErrorLog::CRITICAL_ERROR_TYPES).count,
76
+ high: base_scope.where(error_type: ErrorLog::HIGH_SEVERITY_ERROR_TYPES).count,
77
+ medium: base_scope.where(error_type: ErrorLog::MEDIUM_SEVERITY_ERROR_TYPES).count,
78
+ low: base_scope.where.not(
79
+ error_type: ErrorLog::CRITICAL_ERROR_TYPES +
80
+ ErrorLog::HIGH_SEVERITY_ERROR_TYPES +
81
+ ErrorLog::MEDIUM_SEVERITY_ERROR_TYPES
82
+ ).count
64
83
  }
65
84
  end
66
85
 
@@ -80,14 +80,21 @@ module RailsErrorDashboard
80
80
  # Use PostgreSQL full-text search if available (much faster with GIN index)
81
81
  # Otherwise fall back to LIKE query
82
82
  if postgresql?
83
- # Use to_tsquery for full-text search with GIN index
84
- # This is dramatically faster on large datasets
85
- search_term = @filters[:search].split.map { |word| "#{word}:*" }.join(" & ")
86
- query.where("to_tsvector('english', message) @@ to_tsquery('english', ?)", search_term)
83
+ # Use plainto_tsquery for full-text search with GIN index created in migration
84
+ # This leverages index_error_logs_on_searchable_text for fast searches
85
+ # across message, backtrace, and error_type fields
86
+ query.where(
87
+ "to_tsvector('english', COALESCE(message, '') || ' ' || COALESCE(backtrace, '') || ' ' || COALESCE(error_type, '')) @@ plainto_tsquery('english', ?)",
88
+ @filters[:search]
89
+ )
87
90
  else
88
- # Fall back to LIKE for SQLite/MySQL
91
+ # Fall back to LIKE for SQLite/MySQL - search across all relevant fields
89
92
  # Use LOWER() for case-insensitive search
90
- query.where("LOWER(message) LIKE LOWER(?)", "%#{@filters[:search]}%")
93
+ search_pattern = "%#{@filters[:search]}%"
94
+ query.where(
95
+ "LOWER(message) LIKE LOWER(?) OR LOWER(COALESCE(backtrace, '')) LIKE LOWER(?) OR LOWER(error_type) LIKE LOWER(?)",
96
+ search_pattern, search_pattern, search_pattern
97
+ )
91
98
  end
92
99
  end
93
100
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
@@ -38,6 +38,7 @@ require "rails_error_dashboard/queries/recurring_issues"
38
38
  require "rails_error_dashboard/queries/mttr_stats"
39
39
  require "rails_error_dashboard/error_reporter"
40
40
  require "rails_error_dashboard/middleware/error_catcher"
41
+ require "rails_error_dashboard/middleware/rate_limiter"
41
42
 
42
43
  # Plugin system
43
44
  require "rails_error_dashboard/plugin"
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.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -298,7 +298,6 @@ files:
298
298
  - app/models/rails_error_dashboard/error_occurrence.rb
299
299
  - app/views/layouts/rails_error_dashboard.html.erb
300
300
  - app/views/layouts/rails_error_dashboard/application.html.erb
301
- - app/views/layouts/rails_error_dashboard_old_backup.html.erb
302
301
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb
303
302
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb
304
303
  - app/views/rails_error_dashboard/errors/_error_row.html.erb
@@ -325,6 +324,7 @@ files:
325
324
  - db/migrate/20251225102500_create_error_baselines.rb
326
325
  - db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb
327
326
  - db/migrate/20251226020100_create_error_comments.rb
327
+ - db/migrate/20251229111223_add_additional_performance_indexes.rb
328
328
  - lib/generators/rails_error_dashboard/install/install_generator.rb
329
329
  - lib/generators/rails_error_dashboard/install/templates/README
330
330
  - lib/generators/rails_error_dashboard/install/templates/initializer.rb
@@ -341,6 +341,7 @@ files:
341
341
  - lib/rails_error_dashboard/error_reporter.rb
342
342
  - lib/rails_error_dashboard/logger.rb
343
343
  - lib/rails_error_dashboard/middleware/error_catcher.rb
344
+ - lib/rails_error_dashboard/middleware/rate_limiter.rb
344
345
  - lib/rails_error_dashboard/plugin.rb
345
346
  - lib/rails_error_dashboard/plugin_registry.rb
346
347
  - lib/rails_error_dashboard/plugins/audit_log_plugin.rb
@@ -375,6 +376,16 @@ metadata:
375
376
  homepage_uri: https://github.com/AnjanJ/rails_error_dashboard
376
377
  source_code_uri: https://github.com/AnjanJ/rails_error_dashboard
377
378
  changelog_uri: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md
379
+ post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
380
+ \ Rails Error Dashboard v0.1.7 installed successfully!\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F4E6
381
+ Next steps to get started:\n\n 1. Run the installer:\n rails generate rails_error_dashboard:install\n\n
382
+ \ 2. Run migrations:\n rails db:migrate\n\n 3. Mount the engine in config/routes.rb:\n
383
+ \ mount RailsErrorDashboard::Engine => '/error_dashboard'\n\n 4. Start your
384
+ server and visit:\n http://localhost:3000/error_dashboard\n\n\U0001F3AE Try
385
+ the live demo: https://rails-error-dashboard.anjan.dev\n (Username: frodo, Password:
386
+ precious)\n\n\U0001F4D6 Documentation: https://github.com/AnjanJ/rails_error_dashboard\n\U0001F4AC
387
+ Questions? https://github.com/AnjanJ/rails_error_dashboard/issues\n\n⚠️ BETA: API
388
+ may change before v1.0.0 - Use at your own discretion\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
378
389
  rdoc_options: []
379
390
  require_paths:
380
391
  - lib
@@ -1,383 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Error Dashboard - Audio Intelli API</title>
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <%= csrf_meta_tags %>
7
- <%= csp_meta_tag %>
8
-
9
- <!-- Apply theme immediately to prevent flash of wrong theme -->
10
- <script>
11
- // This runs BEFORE body renders to prevent flash
12
- (function() {
13
- const savedTheme = localStorage.getItem('theme');
14
- if (savedTheme === 'dark') {
15
- // Add to html element so we can style body
16
- document.documentElement.setAttribute('data-theme', 'dark');
17
- }
18
- })();
19
- </script>
20
-
21
- <!-- Bootstrap CSS -->
22
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
23
- <!-- Bootstrap Icons -->
24
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
25
- <!-- Chart.js with date adapter -->
26
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
27
- <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
28
- <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
29
-
30
- <!-- Turbo for real-time updates -->
31
- <script type="module">
32
- import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/+esm'
33
- </script>
34
-
35
- <!-- Rails Error Dashboard Styles - Catppuccin Mocha Theme -->
36
- <style>
37
- <%= File.read(RailsErrorDashboard::Engine.root.join("app/assets/stylesheets/rails_error_dashboard/application.css")) %>
38
- </style>
39
- </head>
40
-
41
- <body>
42
- <!-- Loading Indicator -->
43
- <div id="loading-indicator"></div>
44
-
45
- <!-- Top Navbar -->
46
- <nav class="navbar navbar-dark">
47
- <div class="container-fluid">
48
- <div class="d-flex align-items-center">
49
- <!-- Mobile menu toggle -->
50
- <button class="btn btn-link text-white d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu">
51
- <i class="bi bi-list fs-4"></i>
52
- </button>
53
- <a class="navbar-brand fw-bold" href="<%= root_path %>">
54
- <i class="bi bi-bug-fill"></i>
55
- Error Dashboard
56
- </a>
57
- </div>
58
- <div class="d-flex align-items-center gap-3">
59
- <button class="theme-toggle" id="themeToggle" onclick="toggleTheme()">
60
- <i class="bi bi-moon-fill" id="themeIcon"></i>
61
- </button>
62
- <div class="text-white d-none d-md-block">
63
- <small><%= Rails.env.titleize %> Environment</small>
64
- </div>
65
- </div>
66
- </div>
67
- </nav>
68
-
69
- <div class="container-fluid">
70
- <div class="row">
71
- <!-- Sidebar - Desktop -->
72
- <nav class="col-md-2 d-none d-md-block sidebar" id="sidebarDesktop">
73
- <div class="position-sticky pt-3">
74
- <ul class="nav flex-column">
75
- <li class="nav-item">
76
- <%= link_to root_path, class: "nav-link #{request.path == root_path ? 'active' : ''}" do %>
77
- <i class="bi bi-speedometer2"></i> Overview
78
- <% end %>
79
- </li>
80
- <li class="nav-item">
81
- <%= link_to errors_path, class: "nav-link #{request.path == errors_path ? 'active' : ''}" do %>
82
- <i class="bi bi-list-ul"></i> All Errors
83
- <% end %>
84
- </li>
85
- <li class="nav-item">
86
- <%= link_to analytics_errors_path, class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}" do %>
87
- <i class="bi bi-graph-up"></i> Analytics
88
- <% end %>
89
- </li>
90
- <% if RailsErrorDashboard.configuration.enable_platform_comparison %>
91
- <li class="nav-item">
92
- <%= link_to platform_comparison_errors_path, class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}" do %>
93
- <i class="bi bi-phone"></i> Platform Health
94
- <% end %>
95
- </li>
96
- <% end %>
97
- <% if RailsErrorDashboard.configuration.enable_error_correlation %>
98
- <li class="nav-item">
99
- <%= link_to correlation_errors_path, class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
100
- <i class="bi bi-diagram-3"></i> Correlation
101
- <% end %>
102
- </li>
103
- <% end %>
104
- </ul>
105
-
106
- <hr class="my-3">
107
-
108
- <h6 class="sidebar-heading px-3 mt-4 mb-2 text-muted text-uppercase">
109
- <small>Quick Filters</small>
110
- </h6>
111
- <ul class="nav flex-column">
112
- <li class="nav-item">
113
- <%= link_to errors_path(unresolved: true), class: "nav-link" do %>
114
- <i class="bi bi-exclamation-circle text-danger"></i> Unresolved
115
- <% end %>
116
- </li>
117
- <li class="nav-item">
118
- <%= link_to errors_path(platform: 'iOS'), class: "nav-link" do %>
119
- <i class="bi bi-phone"></i> iOS Errors
120
- <% end %>
121
- </li>
122
- <li class="nav-item">
123
- <%= link_to errors_path(platform: 'Android'), class: "nav-link" do %>
124
- <i class="bi bi-phone"></i> Android Errors
125
- <% end %>
126
- </li>
127
- </ul>
128
- </div>
129
- </nav>
130
-
131
- <!-- Sidebar - Mobile (Offcanvas) -->
132
- <div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
133
- <div class="offcanvas-header">
134
- <h5 class="offcanvas-title" id="sidebarMenuLabel">
135
- <i class="bi bi-bug-fill"></i> Error Dashboard
136
- </h5>
137
- <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
138
- </div>
139
- <div class="offcanvas-body">
140
- <ul class="nav flex-column">
141
- <li class="nav-item">
142
- <%= link_to root_path, class: "nav-link #{request.path == root_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
143
- <i class="bi bi-speedometer2"></i> Overview
144
- <% end %>
145
- </li>
146
- <li class="nav-item">
147
- <%= link_to errors_path, class: "nav-link #{request.path == errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
148
- <i class="bi bi-list-ul"></i> All Errors
149
- <% end %>
150
- </li>
151
- <li class="nav-item">
152
- <%= link_to analytics_errors_path, class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
153
- <i class="bi bi-graph-up"></i> Analytics
154
- <% end %>
155
- </li>
156
- <% if RailsErrorDashboard.configuration.enable_platform_comparison %>
157
- <li class="nav-item">
158
- <%= link_to platform_comparison_errors_path, class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
159
- <i class="bi bi-phone"></i> Platform Health
160
- <% end %>
161
- </li>
162
- <% end %>
163
- <% if RailsErrorDashboard.configuration.enable_error_correlation %>
164
- <li class="nav-item">
165
- <%= link_to correlation_errors_path, class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
166
- <i class="bi bi-diagram-3"></i> Correlation
167
- <% end %>
168
- </li>
169
- <% end %>
170
- </ul>
171
-
172
- <hr class="my-3">
173
-
174
- <h6 class="sidebar-heading px-3 mt-4 mb-2 text-muted text-uppercase">
175
- <small>Quick Filters</small>
176
- </h6>
177
- <ul class="nav flex-column">
178
- <li class="nav-item">
179
- <%= link_to errors_path(unresolved: true), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
180
- <i class="bi bi-exclamation-circle text-danger"></i> Unresolved
181
- <% end %>
182
- </li>
183
- <li class="nav-item">
184
- <%= link_to errors_path(platform: 'iOS'), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
185
- <i class="bi bi-phone"></i> iOS Errors
186
- <% end %>
187
- </li>
188
- <li class="nav-item">
189
- <%= link_to errors_path(platform: 'Android'), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
190
- <i class="bi bi-phone"></i> Android Errors
191
- <% end %>
192
- </li>
193
- </ul>
194
-
195
- <hr class="my-3">
196
-
197
- <div class="px-3">
198
- <small class="text-muted">Environment</small>
199
- <div class="fw-bold"><%= Rails.env.titleize %></div>
200
- </div>
201
- </div>
202
- </div>
203
-
204
- <!-- Main content -->
205
- <main class="col-md-10 ms-sm-auto px-md-4">
206
- <%= yield %>
207
- </main>
208
- </div>
209
- </div>
210
-
211
- <!-- Bootstrap JS -->
212
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
213
-
214
- <!-- Theme Toggle Script -->
215
- <script>
216
- // Catppuccin Mocha Chart Colors
217
- function getCatppuccinChartConfig(isDark) {
218
- if (!isDark) {
219
- // Light theme - keep existing for now
220
- return {
221
- textColor: '#1F2937',
222
- gridColor: 'rgba(0, 0, 0, 0.1)',
223
- tooltipBg: 'rgba(0, 0, 0, 0.8)',
224
- colors: {
225
- red: '#EF4444',
226
- green: '#10B981',
227
- yellow: '#F59E0B',
228
- blue: '#3B82F6',
229
- purple: '#8B5CF6',
230
- pink: '#EC4899',
231
- teal: '#14B8A6',
232
- orange: '#F97316',
233
- sapphire: '#3B82F6'
234
- }
235
- };
236
- }
237
-
238
- // Dark theme - Catppuccin Mocha
239
- return {
240
- textColor: '#cdd6f4', // ctp-text
241
- gridColor: 'rgba(88, 91, 112, 0.3)', // ctp-surface2 with opacity
242
- tooltipBg: '#313244', // ctp-surface0
243
- colors: {
244
- red: '#f38ba8', // ctp-red
245
- green: '#a6e3a1', // ctp-green
246
- yellow: '#f9e2af', // ctp-yellow
247
- blue: '#89b4fa', // ctp-blue
248
- purple: '#cba6f7', // ctp-mauve
249
- pink: '#f5c2e7', // ctp-pink
250
- teal: '#94e2d5', // ctp-teal
251
- orange: '#fab387', // ctp-peach
252
- sapphire: '#74c7ec' // ctp-sapphire
253
- }
254
- };
255
- }
256
-
257
- // Get platform-specific color
258
- function getPlatformColor(platform, isDark) {
259
- const config = getCatppuccinChartConfig(isDark);
260
- const platformMap = {
261
- 'ios': isDark ? config.textColor : '#000000',
262
- 'android': config.colors.green,
263
- 'web': config.colors.blue,
264
- 'api': config.colors.sapphire
265
- };
266
- return platformMap[platform.toLowerCase()] || config.colors.purple;
267
- }
268
-
269
- // Update Chart.js defaults for dark/light theme
270
- function updateChartTheme(isDark) {
271
- if (typeof Chart !== 'undefined') {
272
- const config = getCatppuccinChartConfig(isDark);
273
-
274
- Chart.defaults.color = config.textColor;
275
- Chart.defaults.borderColor = config.gridColor;
276
- Chart.defaults.scale.grid.color = config.gridColor;
277
- Chart.defaults.scale.ticks.color = config.textColor;
278
- Chart.defaults.plugins.legend.labels.color = config.textColor;
279
- Chart.defaults.plugins.tooltip.backgroundColor = config.tooltipBg;
280
- Chart.defaults.plugins.tooltip.titleColor = config.textColor;
281
- Chart.defaults.plugins.tooltip.bodyColor = config.textColor;
282
- Chart.defaults.plugins.tooltip.borderColor = config.gridColor;
283
- }
284
- }
285
-
286
- // Load theme from localStorage on page load
287
- document.addEventListener('DOMContentLoaded', function() {
288
- console.log('📄 DOMContentLoaded - Loading theme from localStorage');
289
-
290
- const savedTheme = localStorage.getItem('theme') || 'light';
291
- const isDark = savedTheme === 'dark';
292
-
293
- console.log('Saved theme:', savedTheme, '| isDark:', isDark);
294
-
295
- if (isDark) {
296
- document.body.classList.add('dark-mode');
297
- document.documentElement.setAttribute('data-theme', 'dark');
298
- console.log('✅ Applied dark theme (body.dark-mode + html[data-theme=dark])');
299
- updateThemeIcon(true);
300
- } else {
301
- // Ensure light mode is clean
302
- document.body.classList.remove('dark-mode');
303
- document.documentElement.removeAttribute('data-theme');
304
- console.log('✅ Applied light theme (removed classes)');
305
- updateThemeIcon(false);
306
- }
307
-
308
- // Update Chart.js theme
309
- updateChartTheme(isDark);
310
- console.log('📊 Chart.js theme updated');
311
- });
312
-
313
- function toggleTheme() {
314
- try {
315
- console.log('🎨 Toggle theme clicked');
316
-
317
- const body = document.body;
318
- const isDark = body.classList.toggle('dark-mode');
319
-
320
- console.log('Dark mode:', isDark);
321
-
322
- // Sync with html data attribute
323
- if (isDark) {
324
- document.documentElement.setAttribute('data-theme', 'dark');
325
- console.log('✅ Set data-theme=dark');
326
- } else {
327
- document.documentElement.removeAttribute('data-theme');
328
- console.log('✅ Removed data-theme');
329
- }
330
-
331
- // Save preference
332
- localStorage.setItem('theme', isDark ? 'dark' : 'light');
333
- console.log('💾 Saved to localStorage:', isDark ? 'dark' : 'light');
334
-
335
- // Update icon
336
- updateThemeIcon(isDark);
337
- console.log('🌙 Updated icon');
338
-
339
- // Update Chart.js theme
340
- updateChartTheme(isDark);
341
- console.log('📊 Updated Chart.js');
342
-
343
- // Reload page to apply chart theme (Chart.js requires re-render)
344
- console.log('🔄 Reloading page...');
345
- setTimeout(() => location.reload(), 100);
346
- } catch (error) {
347
- console.error('❌ Error in toggleTheme:', error);
348
- alert('Error toggling theme: ' + error.message);
349
- }
350
- }
351
-
352
- function updateThemeIcon(isDark) {
353
- const icon = document.getElementById('themeIcon');
354
- if (isDark) {
355
- icon.className = 'bi bi-sun-fill';
356
- } else {
357
- icon.className = 'bi bi-moon-fill';
358
- }
359
- }
360
-
361
- // Loading indicator for form submissions and link clicks
362
- const loadingIndicator = document.getElementById('loading-indicator');
363
-
364
- // Show loading on form submit
365
- document.addEventListener('submit', function() {
366
- loadingIndicator.classList.add('active');
367
- });
368
-
369
- // Show loading on link clicks (except anchors)
370
- document.addEventListener('click', function(e) {
371
- const link = e.target.closest('a');
372
- if (link && link.href && !link.href.startsWith('#') && !link.target) {
373
- loadingIndicator.classList.add('active');
374
- }
375
- });
376
-
377
- // Hide loading when page loads
378
- window.addEventListener('load', function() {
379
- loadingIndicator.classList.remove('active');
380
- });
381
- </script>
382
- </body>
383
- </html>