rails_error_dashboard 0.1.1 → 0.1.4
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 +92 -21
- data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +107 -0
- data/app/assets/stylesheets/rails_error_dashboard/_components.scss +625 -0
- data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +257 -0
- data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +203 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css.map +7 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +135 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +80 -4
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
- data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
- data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
- data/app/models/rails_error_dashboard/error_comment.rb +27 -0
- data/app/models/rails_error_dashboard/error_log.rb +159 -0
- data/app/views/layouts/rails_error_dashboard/application.html.erb +39 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +796 -299
- data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +2 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +4 -4
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +439 -22
- data/app/views/rails_error_dashboard/errors/index.html.erb +127 -11
- data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +29 -18
- data/app/views/rails_error_dashboard/errors/show.html.erb +353 -54
- data/config/routes.rb +11 -1
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
- data/db/migrate/20251226020100_create_error_comments.rb +18 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +8 -2
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +21 -0
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +317 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +47 -9
- data/lib/rails_error_dashboard/commands/resolve_error.rb +1 -1
- data/lib/rails_error_dashboard/configuration.rb +8 -0
- data/lib/rails_error_dashboard/error_reporter.rb +4 -4
- data/lib/rails_error_dashboard/logger.rb +105 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +2 -2
- data/lib/rails_error_dashboard/plugin.rb +3 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +1 -1
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -1
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +109 -1
- data/lib/rails_error_dashboard/queries/errors_list.rb +134 -7
- data/lib/rails_error_dashboard/queries/mttr_stats.rb +111 -0
- data/lib/rails_error_dashboard/queries/recurring_issues.rb +97 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +5 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +85 -4
- metadata +36 -2
|
@@ -6,6 +6,30 @@ module RailsErrorDashboard
|
|
|
6
6
|
|
|
7
7
|
before_action :authenticate_dashboard_user!
|
|
8
8
|
|
|
9
|
+
def overview
|
|
10
|
+
# Get dashboard stats using Query
|
|
11
|
+
@stats = Queries::DashboardStats.call
|
|
12
|
+
|
|
13
|
+
# Get platform health summary (if enabled)
|
|
14
|
+
if RailsErrorDashboard.configuration.enable_platform_comparison
|
|
15
|
+
comparison = Queries::PlatformComparison.new(days: 7)
|
|
16
|
+
@platform_health = comparison.platform_health_summary
|
|
17
|
+
@platform_scores = comparison.platform_stability_scores
|
|
18
|
+
else
|
|
19
|
+
@platform_health = {}
|
|
20
|
+
@platform_scores = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get critical alerts (critical/high severity errors from last hour)
|
|
24
|
+
@critical_alerts = ErrorLog
|
|
25
|
+
.where("occurred_at >= ?", 1.hour.ago)
|
|
26
|
+
.where(resolved_at: nil)
|
|
27
|
+
.select { |error| [ :critical, :high ].include?(error.severity) }
|
|
28
|
+
.sort_by(&:occurred_at)
|
|
29
|
+
.reverse
|
|
30
|
+
.first(10)
|
|
31
|
+
end
|
|
32
|
+
|
|
9
33
|
def index
|
|
10
34
|
# Use Query to get filtered errors
|
|
11
35
|
errors_query = Queries::ErrorsList.call(filter_params)
|
|
@@ -42,6 +66,70 @@ module RailsErrorDashboard
|
|
|
42
66
|
redirect_to error_path(@error)
|
|
43
67
|
end
|
|
44
68
|
|
|
69
|
+
# Phase 3: Workflow Integration Actions
|
|
70
|
+
|
|
71
|
+
def assign
|
|
72
|
+
@error = ErrorLog.find(params[:id])
|
|
73
|
+
@error.assign_to!(params[:assigned_to])
|
|
74
|
+
redirect_to error_path(@error)
|
|
75
|
+
rescue => e
|
|
76
|
+
redirect_to error_path(@error)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def unassign
|
|
80
|
+
@error = ErrorLog.find(params[:id])
|
|
81
|
+
@error.unassign!
|
|
82
|
+
redirect_to error_path(@error)
|
|
83
|
+
rescue => e
|
|
84
|
+
redirect_to error_path(@error)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update_priority
|
|
88
|
+
@error = ErrorLog.find(params[:id])
|
|
89
|
+
@error.update!(priority_level: params[:priority_level])
|
|
90
|
+
redirect_to error_path(@error)
|
|
91
|
+
rescue => e
|
|
92
|
+
redirect_to error_path(@error)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def snooze
|
|
96
|
+
@error = ErrorLog.find(params[:id])
|
|
97
|
+
@error.snooze!(params[:hours].to_i, reason: params[:reason])
|
|
98
|
+
redirect_to error_path(@error)
|
|
99
|
+
rescue => e
|
|
100
|
+
redirect_to error_path(@error)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def unsnooze
|
|
104
|
+
@error = ErrorLog.find(params[:id])
|
|
105
|
+
@error.unsnooze!
|
|
106
|
+
redirect_to error_path(@error)
|
|
107
|
+
rescue => e
|
|
108
|
+
redirect_to error_path(@error)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def update_status
|
|
112
|
+
@error = ErrorLog.find(params[:id])
|
|
113
|
+
if @error.update_status!(params[:status], comment: params[:comment])
|
|
114
|
+
redirect_to error_path(@error)
|
|
115
|
+
else
|
|
116
|
+
redirect_to error_path(@error)
|
|
117
|
+
end
|
|
118
|
+
rescue => e
|
|
119
|
+
redirect_to error_path(@error)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def add_comment
|
|
123
|
+
@error = ErrorLog.find(params[:id])
|
|
124
|
+
@error.comments.create!(
|
|
125
|
+
author_name: params[:author_name],
|
|
126
|
+
body: params[:body]
|
|
127
|
+
)
|
|
128
|
+
redirect_to error_path(@error)
|
|
129
|
+
rescue => e
|
|
130
|
+
redirect_to error_path(@error)
|
|
131
|
+
end
|
|
132
|
+
|
|
45
133
|
def analytics
|
|
46
134
|
days = (params[:days] || 30).to_i
|
|
47
135
|
@days = days
|
|
@@ -58,6 +146,22 @@ module RailsErrorDashboard
|
|
|
58
146
|
@resolution_rate = analytics[:resolution_rate]
|
|
59
147
|
@mobile_errors = analytics[:mobile_errors]
|
|
60
148
|
@api_errors = analytics[:api_errors]
|
|
149
|
+
|
|
150
|
+
# Get recurring issues data
|
|
151
|
+
recurring = Queries::RecurringIssues.call(days)
|
|
152
|
+
@recurring_data = recurring
|
|
153
|
+
|
|
154
|
+
# Get release correlation data
|
|
155
|
+
correlation = Queries::ErrorCorrelation.new(days: days)
|
|
156
|
+
@errors_by_version = correlation.errors_by_version
|
|
157
|
+
@problematic_releases = correlation.problematic_releases
|
|
158
|
+
@release_comparison = calculate_release_comparison
|
|
159
|
+
|
|
160
|
+
# Get MTTR data
|
|
161
|
+
mttr_data = Queries::MttrStats.call(days)
|
|
162
|
+
@mttr_stats = mttr_data
|
|
163
|
+
@overall_mttr = mttr_data[:overall_mttr]
|
|
164
|
+
@mttr_by_platform = mttr_data[:mttr_by_platform]
|
|
61
165
|
end
|
|
62
166
|
|
|
63
167
|
def platform_comparison
|
|
@@ -133,13 +237,43 @@ module RailsErrorDashboard
|
|
|
133
237
|
|
|
134
238
|
private
|
|
135
239
|
|
|
240
|
+
def calculate_release_comparison
|
|
241
|
+
return {} if @errors_by_version.empty? || @errors_by_version.count < 2
|
|
242
|
+
|
|
243
|
+
versions_sorted = @errors_by_version.sort_by { |_, data| data[:last_seen] || Time.at(0) }.reverse
|
|
244
|
+
latest = versions_sorted.first
|
|
245
|
+
previous = versions_sorted.second
|
|
246
|
+
|
|
247
|
+
return {} if latest.nil? || previous.nil?
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
latest_version: latest[0],
|
|
251
|
+
latest_count: latest[1][:count],
|
|
252
|
+
latest_critical: latest[1][:critical_count],
|
|
253
|
+
previous_version: previous[0],
|
|
254
|
+
previous_count: previous[1][:count],
|
|
255
|
+
previous_critical: previous[1][:critical_count],
|
|
256
|
+
change_percentage: previous[1][:count] > 0 ? ((latest[1][:count] - previous[1][:count]).to_f / previous[1][:count] * 100).round(1) : 0.0
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
136
260
|
def filter_params
|
|
137
261
|
{
|
|
138
262
|
error_type: params[:error_type],
|
|
139
263
|
unresolved: params[:unresolved],
|
|
140
264
|
platform: params[:platform],
|
|
141
265
|
search: params[:search],
|
|
142
|
-
severity: params[:severity]
|
|
266
|
+
severity: params[:severity],
|
|
267
|
+
timeframe: params[:timeframe],
|
|
268
|
+
frequency: params[:frequency],
|
|
269
|
+
# Phase 3: Workflow filter params
|
|
270
|
+
status: params[:status],
|
|
271
|
+
assigned_to: params[:assigned_to],
|
|
272
|
+
priority_level: params[:priority_level],
|
|
273
|
+
hide_snoozed: params[:hide_snoozed],
|
|
274
|
+
# Sorting params
|
|
275
|
+
sort_by: params[:sort_by],
|
|
276
|
+
sort_direction: params[:sort_direction]
|
|
143
277
|
}
|
|
144
278
|
end
|
|
145
279
|
|
|
@@ -1,21 +1,97 @@
|
|
|
1
1
|
module RailsErrorDashboard
|
|
2
2
|
module ApplicationHelper
|
|
3
3
|
# Returns Bootstrap color class for error severity
|
|
4
|
+
# Uses Catppuccin Mocha colors in dark theme via CSS variables
|
|
4
5
|
# @param severity [Symbol] The severity level (:critical, :high, :medium, :low, :info)
|
|
5
6
|
# @return [String] Bootstrap color class (danger, warning, info, secondary)
|
|
6
7
|
def severity_color(severity)
|
|
7
8
|
case severity&.to_sym
|
|
8
9
|
when :critical
|
|
9
|
-
"danger"
|
|
10
|
+
"danger" # Maps to --ctp-red in dark mode
|
|
10
11
|
when :high
|
|
11
|
-
"warning"
|
|
12
|
+
"warning" # Maps to --ctp-peach in dark mode
|
|
12
13
|
when :medium
|
|
13
|
-
"info"
|
|
14
|
+
"info" # Maps to --ctp-blue in dark mode
|
|
14
15
|
when :low
|
|
15
|
-
"secondary"
|
|
16
|
+
"secondary" # Maps to --ctp-overlay1 in dark mode
|
|
16
17
|
else
|
|
17
18
|
"secondary"
|
|
18
19
|
end
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
# Returns CSS variable for severity color (for inline styles)
|
|
23
|
+
# Useful when you need to set background-color or color directly
|
|
24
|
+
# @param severity [Symbol] The severity level
|
|
25
|
+
# @return [String] CSS variable reference
|
|
26
|
+
def severity_color_var(severity)
|
|
27
|
+
case severity&.to_sym
|
|
28
|
+
when :critical
|
|
29
|
+
"var(--ctp-red)"
|
|
30
|
+
when :high
|
|
31
|
+
"var(--ctp-peach)"
|
|
32
|
+
when :medium
|
|
33
|
+
"var(--ctp-blue)"
|
|
34
|
+
when :low
|
|
35
|
+
"var(--ctp-overlay1)"
|
|
36
|
+
else
|
|
37
|
+
"var(--ctp-overlay1)"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns platform-specific color class
|
|
42
|
+
# @param platform [String] Platform name (ios, android, web, api)
|
|
43
|
+
# @return [String] CSS color variable
|
|
44
|
+
def platform_color_var(platform)
|
|
45
|
+
case platform&.downcase
|
|
46
|
+
when "ios"
|
|
47
|
+
"var(--platform-ios)"
|
|
48
|
+
when "android"
|
|
49
|
+
"var(--platform-android)"
|
|
50
|
+
when "web"
|
|
51
|
+
"var(--platform-web)"
|
|
52
|
+
when "api"
|
|
53
|
+
"var(--platform-api)"
|
|
54
|
+
else
|
|
55
|
+
"var(--text-color)"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the current user name for filtering "My Errors"
|
|
60
|
+
# Uses configured dashboard username or system username
|
|
61
|
+
# @return [String] Current user identifier
|
|
62
|
+
def current_user_name
|
|
63
|
+
RailsErrorDashboard.configuration.dashboard_username || ENV["USER"] || "unknown"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Generates a sortable column header link
|
|
67
|
+
# @param label [String] The column label to display
|
|
68
|
+
# @param column [String] The column name to sort by
|
|
69
|
+
# @return [String] HTML safe link with sort indicator
|
|
70
|
+
def sortable_header(label, column)
|
|
71
|
+
current_sort = params[:sort_by]
|
|
72
|
+
current_direction = params[:sort_direction] || "desc"
|
|
73
|
+
|
|
74
|
+
# Determine new direction: if clicking same column, toggle; otherwise default to desc
|
|
75
|
+
new_direction = if current_sort == column
|
|
76
|
+
current_direction == "asc" ? "desc" : "asc"
|
|
77
|
+
else
|
|
78
|
+
"desc"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Choose icon based on current state
|
|
82
|
+
icon = if current_sort == column
|
|
83
|
+
current_direction == "asc" ? "▲" : "▼"
|
|
84
|
+
else
|
|
85
|
+
"⇅" # Unsorted indicator
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Preserve existing filter params while adding sort params
|
|
89
|
+
link_params = params.permit!.to_h.merge(sort_by: column, sort_direction: new_direction)
|
|
90
|
+
|
|
91
|
+
link_to errors_path(link_params), class: "text-decoration-none" do
|
|
92
|
+
content_tag(:span, "#{label} ", class: current_sort == column ? "fw-bold" : "") +
|
|
93
|
+
content_tag(:span, icon, class: "text-muted small")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
20
96
|
end
|
|
21
97
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module BacktraceHelper
|
|
5
|
+
# Parse backtrace string into structured frames
|
|
6
|
+
def parse_backtrace(backtrace_string)
|
|
7
|
+
Services::BacktraceParser.parse(backtrace_string)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Filter to show only application code
|
|
11
|
+
def filter_app_code(frames)
|
|
12
|
+
frames.select { |f| f[:category] == :app }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Filter to show framework/gem code
|
|
16
|
+
def filter_framework_code(frames)
|
|
17
|
+
frames.reject { |f| f[:category] == :app }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get icon for frame category
|
|
21
|
+
def frame_icon(category)
|
|
22
|
+
case category
|
|
23
|
+
when :app
|
|
24
|
+
'<i class="bi bi-code-square text-success"></i>'.html_safe
|
|
25
|
+
when :gem
|
|
26
|
+
'<i class="bi bi-box text-info"></i>'.html_safe
|
|
27
|
+
when :framework
|
|
28
|
+
'<i class="bi bi-gear text-warning"></i>'.html_safe
|
|
29
|
+
when :ruby_core
|
|
30
|
+
'<i class="bi bi-gem text-secondary"></i>'.html_safe
|
|
31
|
+
else
|
|
32
|
+
'<i class="bi bi-question-circle text-muted"></i>'.html_safe
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get color class for frame category
|
|
37
|
+
def frame_color_class(category)
|
|
38
|
+
case category
|
|
39
|
+
when :app
|
|
40
|
+
"text-success fw-bold"
|
|
41
|
+
when :gem
|
|
42
|
+
"text-info"
|
|
43
|
+
when :framework
|
|
44
|
+
"text-warning"
|
|
45
|
+
when :ruby_core
|
|
46
|
+
"text-secondary"
|
|
47
|
+
else
|
|
48
|
+
"text-muted"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get background color class for frame
|
|
53
|
+
# Uses Catppuccin-themed backtrace frame classes from _components.scss
|
|
54
|
+
def frame_bg_class(category)
|
|
55
|
+
case category
|
|
56
|
+
when :app
|
|
57
|
+
"backtrace-frame frame-app"
|
|
58
|
+
when :gem
|
|
59
|
+
"backtrace-frame frame-gem"
|
|
60
|
+
when :framework
|
|
61
|
+
"backtrace-frame frame-framework"
|
|
62
|
+
when :ruby_core
|
|
63
|
+
"backtrace-frame frame-ruby-core"
|
|
64
|
+
else
|
|
65
|
+
""
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Format category name
|
|
70
|
+
def frame_category_name(category)
|
|
71
|
+
case category
|
|
72
|
+
when :app
|
|
73
|
+
"Your Code"
|
|
74
|
+
when :gem
|
|
75
|
+
"Gem"
|
|
76
|
+
when :framework
|
|
77
|
+
"Rails Framework"
|
|
78
|
+
when :ruby_core
|
|
79
|
+
"Ruby Core"
|
|
80
|
+
else
|
|
81
|
+
"Unknown"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Count frames by category
|
|
86
|
+
def frame_count_by_category(frames)
|
|
87
|
+
frames.group_by { |f| f[:category] }
|
|
88
|
+
.transform_values(&:count)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module OverviewHelper
|
|
5
|
+
# All helper methods return Bootstrap semantic classes (success, warning, danger)
|
|
6
|
+
# These automatically map to Catppuccin Mocha colors in dark theme via CSS variables:
|
|
7
|
+
# - success → --ctp-green
|
|
8
|
+
# - warning → --ctp-peach
|
|
9
|
+
# - danger → --ctp-red
|
|
10
|
+
def error_rate_border_class(rate)
|
|
11
|
+
return "border-success" if rate < 1.0
|
|
12
|
+
return "border-warning" if rate < 5.0
|
|
13
|
+
"border-danger"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def error_rate_text_class(rate)
|
|
17
|
+
return "text-success" if rate < 1.0
|
|
18
|
+
return "text-warning" if rate < 5.0
|
|
19
|
+
"text-danger"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def trend_arrow(value)
|
|
23
|
+
return "→" if value.zero?
|
|
24
|
+
value > 0 ? "↑" : "↓"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def trend_color_class(value)
|
|
28
|
+
return "text-muted" if value.zero?
|
|
29
|
+
value > 0 ? "text-danger" : "text-success"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def trend_text(direction)
|
|
33
|
+
case direction
|
|
34
|
+
when :increasing
|
|
35
|
+
"Increasing"
|
|
36
|
+
when :decreasing
|
|
37
|
+
"Decreasing"
|
|
38
|
+
else
|
|
39
|
+
"Stable"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def severity_icon(severity)
|
|
44
|
+
case severity
|
|
45
|
+
when :critical
|
|
46
|
+
"🔴"
|
|
47
|
+
when :high
|
|
48
|
+
"🟠"
|
|
49
|
+
when :medium
|
|
50
|
+
"🟡"
|
|
51
|
+
else
|
|
52
|
+
"⚪"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def health_status_color(status)
|
|
57
|
+
case status
|
|
58
|
+
when :healthy
|
|
59
|
+
"success"
|
|
60
|
+
when :warning
|
|
61
|
+
"warning"
|
|
62
|
+
else
|
|
63
|
+
"danger"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def health_status_text(status)
|
|
68
|
+
case status
|
|
69
|
+
when :healthy
|
|
70
|
+
"✅ Healthy"
|
|
71
|
+
when :warning
|
|
72
|
+
"⚠️ Warning"
|
|
73
|
+
else
|
|
74
|
+
"🔴 Critical"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module UserAgentHelper
|
|
5
|
+
# Parse user agent string into browser, OS, and device info
|
|
6
|
+
def parse_user_agent(user_agent_string)
|
|
7
|
+
return default_user_agent_info if user_agent_string.blank?
|
|
8
|
+
|
|
9
|
+
browser = Browser.new(user_agent_string)
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
browser_name: browser_name(browser),
|
|
13
|
+
browser_version: browser.version,
|
|
14
|
+
os_name: os_name(browser),
|
|
15
|
+
device_type: device_type(browser),
|
|
16
|
+
platform: browser.platform.to_s.titleize,
|
|
17
|
+
is_mobile: browser.device.mobile?,
|
|
18
|
+
is_tablet: browser.device.tablet?,
|
|
19
|
+
is_bot: browser.bot?
|
|
20
|
+
}
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
Rails.logger.warn "[RailsErrorDashboard] Failed to parse user agent: #{e.message}"
|
|
23
|
+
default_user_agent_info
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get browser icon based on browser name
|
|
27
|
+
def browser_icon(browser_info)
|
|
28
|
+
return '<i class="bi bi-question-circle text-muted"></i>'.html_safe if browser_info.blank?
|
|
29
|
+
|
|
30
|
+
case browser_info[:browser_name]&.downcase
|
|
31
|
+
when "chrome"
|
|
32
|
+
'<i class="bi bi-browser-chrome text-warning"></i>'.html_safe
|
|
33
|
+
when "firefox"
|
|
34
|
+
'<i class="bi bi-browser-firefox text-danger"></i>'.html_safe
|
|
35
|
+
when "safari"
|
|
36
|
+
'<i class="bi bi-browser-safari text-primary"></i>'.html_safe
|
|
37
|
+
when "edge"
|
|
38
|
+
'<i class="bi bi-browser-edge text-info"></i>'.html_safe
|
|
39
|
+
else
|
|
40
|
+
'<i class="bi bi-globe text-secondary"></i>'.html_safe
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get OS icon based on OS name
|
|
45
|
+
def os_icon(browser_info)
|
|
46
|
+
return '<i class="bi bi-question-circle text-muted"></i>'.html_safe if browser_info.blank?
|
|
47
|
+
|
|
48
|
+
case browser_info[:os_name]&.downcase
|
|
49
|
+
when /windows/
|
|
50
|
+
'<i class="bi bi-windows text-primary"></i>'.html_safe
|
|
51
|
+
when /mac|darwin/
|
|
52
|
+
'<i class="bi bi-apple text-secondary"></i>'.html_safe
|
|
53
|
+
when /linux/
|
|
54
|
+
'<i class="bi bi-ubuntu text-danger"></i>'.html_safe
|
|
55
|
+
when /android/
|
|
56
|
+
'<i class="bi bi-android2 text-success"></i>'.html_safe
|
|
57
|
+
when /ios|iphone|ipad/
|
|
58
|
+
'<i class="bi bi-apple text-secondary"></i>'.html_safe
|
|
59
|
+
else
|
|
60
|
+
'<i class="bi bi-cpu text-muted"></i>'.html_safe
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get device icon based on device type
|
|
65
|
+
def device_icon(browser_info)
|
|
66
|
+
return '<i class="bi bi-question-circle text-muted"></i>'.html_safe if browser_info.blank?
|
|
67
|
+
|
|
68
|
+
if browser_info[:is_mobile]
|
|
69
|
+
'<i class="bi bi-phone text-success"></i>'.html_safe
|
|
70
|
+
elsif browser_info[:is_tablet]
|
|
71
|
+
'<i class="bi bi-tablet text-info"></i>'.html_safe
|
|
72
|
+
else
|
|
73
|
+
'<i class="bi bi-laptop text-secondary"></i>'.html_safe
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def browser_name(browser)
|
|
80
|
+
return "Chrome" if browser.chrome?
|
|
81
|
+
return "Firefox" if browser.firefox?
|
|
82
|
+
return "Safari" if browser.safari?
|
|
83
|
+
return "Edge" if browser.edge?
|
|
84
|
+
return "Opera" if browser.opera?
|
|
85
|
+
return "Internet Explorer" if browser.ie?
|
|
86
|
+
"Unknown"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def os_name(browser)
|
|
90
|
+
return "Windows" if browser.platform.windows?
|
|
91
|
+
return "macOS" if browser.platform.mac?
|
|
92
|
+
return "Linux" if browser.platform.linux?
|
|
93
|
+
return "Android" if browser.platform.android?
|
|
94
|
+
return "iOS" if browser.platform.ios?
|
|
95
|
+
"Unknown"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def device_type(browser)
|
|
99
|
+
return "Mobile" if browser.device.mobile?
|
|
100
|
+
return "Tablet" if browser.device.tablet?
|
|
101
|
+
return "Bot" if browser.bot?
|
|
102
|
+
"Desktop"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def default_user_agent_info
|
|
106
|
+
{
|
|
107
|
+
browser_name: "Unknown",
|
|
108
|
+
browser_version: nil,
|
|
109
|
+
os_name: "Unknown",
|
|
110
|
+
device_type: "Unknown",
|
|
111
|
+
platform: "Unknown",
|
|
112
|
+
is_mobile: false,
|
|
113
|
+
is_tablet: false,
|
|
114
|
+
is_bot: false
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Model: ErrorComment
|
|
5
|
+
# Represents a comment/note on an error for team collaboration
|
|
6
|
+
class ErrorComment < ErrorLogsRecord
|
|
7
|
+
self.table_name = "rails_error_dashboard_error_comments"
|
|
8
|
+
|
|
9
|
+
belongs_to :error_log, class_name: "RailsErrorDashboard::ErrorLog"
|
|
10
|
+
|
|
11
|
+
validates :author_name, presence: true, length: { maximum: 255 }
|
|
12
|
+
validates :body, presence: true, length: { maximum: 10_000 }
|
|
13
|
+
|
|
14
|
+
scope :recent_first, -> { order(created_at: :desc) }
|
|
15
|
+
scope :oldest_first, -> { order(created_at: :asc) }
|
|
16
|
+
|
|
17
|
+
# Get formatted timestamp for display
|
|
18
|
+
def formatted_time
|
|
19
|
+
created_at.strftime("%b %d, %Y at %I:%M %p")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if comment was created recently (within last hour)
|
|
23
|
+
def recent?
|
|
24
|
+
created_at > 1.hour.ago
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|