rails_error_dashboard 0.10.0 → 0.11.1
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 +123 -49
- data/app/controllers/rails_error_dashboard/errors_controller.rb +3 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +8 -1
- data/app/models/rails_error_dashboard/error_log.rb +1 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +30 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +5 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -0
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +8 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +9 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +51 -1
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +8 -1
- data/app/views/rails_error_dashboard/errors/overview.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
- data/config/locales/de.yml +19 -2
- data/config/locales/en.yml +17 -0
- data/config/locales/es.yml +18 -2
- data/config/locales/fr.yml +19 -2
- data/config/locales/it.yml +27 -3
- data/config/locales/ja.yml +17 -3
- data/config/locales/pl.yml +19 -2
- data/config/locales/pt-BR.yml +18 -2
- data/config/locales/ru.yml +18 -2
- data/config/locales/uk.yml +18 -2
- data/config/locales/zh-CN.yml +15 -3
- data/db/migrate/20260826000001_add_environment_to_error_logs.rb +26 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +11 -0
- data/lib/rails_error_dashboard/commands/backfill_environments.rb +58 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +76 -29
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +40 -9
- data/lib/rails_error_dashboard/commands/log_error.rb +17 -0
- data/lib/rails_error_dashboard/configuration.rb +52 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +9 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
- data/lib/rails_error_dashboard/queries/filter_options.rb +9 -0
- data/lib/rails_error_dashboard/services/discord_payload_builder.rb +8 -0
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +6 -2
- data/lib/rails_error_dashboard/services/issue_body_formatter.rb +1 -0
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +1 -0
- data/lib/rails_error_dashboard/services/notification_throttler.rb +23 -0
- data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +1 -0
- data/lib/rails_error_dashboard/services/slack_payload_builder.rb +9 -0
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -0
- data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +1 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +1 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +6 -0
- metadata +24 -27
|
@@ -59,20 +59,38 @@ module RailsErrorDashboard
|
|
|
59
59
|
|
|
60
60
|
error_hash = canonical_hash(entry, application)
|
|
61
61
|
last_seen = parse_time(entry["last_seen_at"]) || Time.current
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
env = current_environment
|
|
63
|
+
|
|
64
|
+
# Priority 1: unresolved match — one UPDATE, no row instantiation.
|
|
65
|
+
# Environment mirrors FindOrIncrementError: exact row first, then a
|
|
66
|
+
# legacy NULL row which is stamped as it is claimed. Two statements
|
|
67
|
+
# rather than one IN (env, NULL), because update_all would hit BOTH
|
|
68
|
+
# rows when they coexist and double-count.
|
|
69
|
+
unresolved = ErrorLog.unresolved.where(error_hash: error_hash, application_id: application.id)
|
|
70
|
+
if env
|
|
71
|
+
updated = unresolved.where(environment: env)
|
|
72
|
+
.update_all([ "occurrence_count = occurrence_count + ?, last_seen_at = ?", count, last_seen ])
|
|
73
|
+
return count if updated.positive?
|
|
74
|
+
|
|
75
|
+
updated = unresolved.where(environment: nil)
|
|
76
|
+
.update_all([ "occurrence_count = occurrence_count + ?, last_seen_at = ?, environment = ?",
|
|
77
|
+
count, last_seen, env ])
|
|
78
|
+
return count if updated.positive?
|
|
79
|
+
else
|
|
80
|
+
updated = unresolved.update_all([ "occurrence_count = occurrence_count + ?, last_seen_at = ?", count, last_seen ])
|
|
81
|
+
return count if updated.positive?
|
|
82
|
+
end
|
|
68
83
|
|
|
69
84
|
# Priority 2: resolved/wont_fix match — reopen, mirroring
|
|
70
85
|
# FindOrIncrementError so storm recurrences don't stay buried
|
|
71
|
-
|
|
86
|
+
resolved_scope = ErrorLog
|
|
72
87
|
.where(error_hash: error_hash, application_id: application.id)
|
|
73
88
|
.where(status: %w[resolved wont_fix])
|
|
74
|
-
|
|
75
|
-
.
|
|
89
|
+
if env
|
|
90
|
+
resolved_scope = resolved_scope.where(environment: [ env, nil ])
|
|
91
|
+
.order(Arel.sql("CASE WHEN environment IS NULL THEN 1 ELSE 0 END"))
|
|
92
|
+
end
|
|
93
|
+
resolved = resolved_scope.order(last_seen_at: :desc).first
|
|
76
94
|
if resolved
|
|
77
95
|
attrs = {
|
|
78
96
|
resolved: false,
|
|
@@ -82,6 +100,7 @@ module RailsErrorDashboard
|
|
|
82
100
|
last_seen_at: last_seen
|
|
83
101
|
}
|
|
84
102
|
attrs[:reopened_at] = Time.current if ErrorLog.column_names.include?("reopened_at")
|
|
103
|
+
attrs[:environment] = env if env && resolved.environment.blank?
|
|
85
104
|
resolved.update!(attrs)
|
|
86
105
|
return count
|
|
87
106
|
end
|
|
@@ -89,7 +108,11 @@ module RailsErrorDashboard
|
|
|
89
108
|
# Priority 3: first seen during count-only mode — minimal ErrorLog
|
|
90
109
|
# from the exemplar (no backtrace/context was captured; the next
|
|
91
110
|
# occurrence after the storm fills in detail via the normal path)
|
|
111
|
+
create_attrs = {
|
|
112
|
+
environment: env
|
|
113
|
+
}.compact
|
|
92
114
|
ErrorLog.create!(
|
|
115
|
+
**create_attrs,
|
|
93
116
|
application_id: application.id,
|
|
94
117
|
error_type: entry["error_class"],
|
|
95
118
|
message: entry["message"],
|
|
@@ -123,6 +146,14 @@ module RailsErrorDashboard
|
|
|
123
146
|
Digest::SHA256.hexdigest(digest_input)[0..15]
|
|
124
147
|
end
|
|
125
148
|
|
|
149
|
+
# nil when the column is not migrated yet, so every environment clause
|
|
150
|
+
# above disappears and the command behaves exactly as before.
|
|
151
|
+
def current_environment
|
|
152
|
+
return nil unless ErrorLog.column_names.include?("environment")
|
|
153
|
+
|
|
154
|
+
RailsErrorDashboard.configuration.current_environment
|
|
155
|
+
end
|
|
156
|
+
|
|
126
157
|
def resolve_application
|
|
127
158
|
# Same chain LogError uses — app name is process-global
|
|
128
159
|
app_name = RailsErrorDashboard.configuration.application_name ||
|
|
@@ -290,6 +290,12 @@ module RailsErrorDashboard
|
|
|
290
290
|
attributes[:environment_info] = Services::EnvironmentSnapshot.snapshot.to_json
|
|
291
291
|
end
|
|
292
292
|
|
|
293
|
+
# Environment awareness (if column exists). context wins so a sender
|
|
294
|
+
# elsewhere can attribute an event to its own environment.
|
|
295
|
+
if ErrorLog.column_names.include?("environment")
|
|
296
|
+
attributes[:environment] = resolve_environment
|
|
297
|
+
end
|
|
298
|
+
|
|
293
299
|
# Apply sensitive data filtering (on by default)
|
|
294
300
|
attributes = Services::SensitiveDataFilter.filter_attributes(attributes)
|
|
295
301
|
|
|
@@ -424,12 +430,22 @@ module RailsErrorDashboard
|
|
|
424
430
|
def maybe_notify(error_log)
|
|
425
431
|
return if error_log.muted?
|
|
426
432
|
return if Services::StormProtection::Gate.notifications_suppressed?
|
|
433
|
+
return unless Services::NotificationThrottler.environment_allowed?(error_log)
|
|
427
434
|
return unless yield
|
|
428
435
|
|
|
429
436
|
Services::ErrorNotificationDispatcher.call(error_log)
|
|
430
437
|
Services::NotificationThrottler.record_notification(error_log)
|
|
431
438
|
end
|
|
432
439
|
|
|
440
|
+
# The environment this error is attributed to: an explicit context value
|
|
441
|
+
# (truncated to the column) or the process-wide resolution. Never nil.
|
|
442
|
+
def resolve_environment
|
|
443
|
+
name = @context[:environment].to_s.strip
|
|
444
|
+
return name[0, 64] unless name.empty?
|
|
445
|
+
|
|
446
|
+
RailsErrorDashboard.configuration.current_environment
|
|
447
|
+
end
|
|
448
|
+
|
|
433
449
|
# Find or create application for multi-app support
|
|
434
450
|
def find_or_create_application
|
|
435
451
|
app_name = RailsErrorDashboard.configuration.application_name ||
|
|
@@ -492,6 +508,7 @@ module RailsErrorDashboard
|
|
|
492
508
|
# Return early if baseline alerts are disabled or error is muted
|
|
493
509
|
return unless config.enable_baseline_alerts
|
|
494
510
|
return if error_log.muted?
|
|
511
|
+
return unless Services::NotificationThrottler.environment_allowed?(error_log)
|
|
495
512
|
return unless defined?(Queries::BaselineStats)
|
|
496
513
|
return unless defined?(BaselineAlertJob)
|
|
497
514
|
|
|
@@ -14,6 +14,12 @@ module RailsErrorDashboard
|
|
|
14
14
|
attr_accessor :application_name
|
|
15
15
|
attr_accessor :database # Database connection name for shared error dashboard DB
|
|
16
16
|
|
|
17
|
+
# Environment awareness. Errors record which environment they came from
|
|
18
|
+
# (production, staging, uat, ...). Names are free-form strings, never an
|
|
19
|
+
# enum -- teams invent environment names, and a fixed list is wrong tomorrow.
|
|
20
|
+
attr_accessor :environment # Overrides Rails.env for captured errors (ENV: ERROR_DASHBOARD_ENVIRONMENT)
|
|
21
|
+
attr_accessor :notification_environments # Only notify for these environments; nil = all (ENV: ERROR_DASHBOARD_NOTIFICATION_ENVIRONMENTS)
|
|
22
|
+
|
|
17
23
|
# Notifications
|
|
18
24
|
attr_accessor :slack_webhook_url
|
|
19
25
|
attr_accessor :notification_email_recipients
|
|
@@ -262,6 +268,10 @@ module RailsErrorDashboard
|
|
|
262
268
|
@application_name = ENV["APPLICATION_NAME"] # Auto-detected if not set
|
|
263
269
|
@database = nil # Use primary database by default
|
|
264
270
|
|
|
271
|
+
# Environment awareness defaults: attribute to Rails.env unless overridden
|
|
272
|
+
@environment = ENV["ERROR_DASHBOARD_ENVIRONMENT"].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
273
|
+
@notification_environments = parse_name_list(ENV["ERROR_DASHBOARD_NOTIFICATION_ENVIRONMENTS"])
|
|
274
|
+
|
|
265
275
|
# Notification settings (disabled by default - enable during installation or in initializer)
|
|
266
276
|
@slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
267
277
|
@notification_email_recipients = ENV.fetch("ERROR_NOTIFICATION_EMAILS", "").split(",").map(&:strip)
|
|
@@ -792,6 +802,26 @@ module RailsErrorDashboard
|
|
|
792
802
|
errors << "total_users_for_impact must be at least 1 (got: #{total_users_for_impact})"
|
|
793
803
|
end
|
|
794
804
|
|
|
805
|
+
# Validate environment (free-form, but it has to fit the 64-char column)
|
|
806
|
+
unless environment.nil?
|
|
807
|
+
if !environment.is_a?(String) || environment.strip.empty?
|
|
808
|
+
errors << "environment must not be blank (got: #{environment.inspect}); leave it nil to use Rails.env"
|
|
809
|
+
elsif environment.length > 64
|
|
810
|
+
errors << "environment must be 64 characters or fewer (got #{environment.length})"
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Validate notification_environments (nil = notify everywhere; otherwise names only)
|
|
815
|
+
unless notification_environments.nil?
|
|
816
|
+
valid_list = notification_environments.is_a?(Array) &&
|
|
817
|
+
notification_environments.any? &&
|
|
818
|
+
notification_environments.all? { |name| name.is_a?(String) && !name.strip.empty? }
|
|
819
|
+
unless valid_list
|
|
820
|
+
errors << "notification_environments must be nil or a non-empty Array of environment names " \
|
|
821
|
+
"(got: #{notification_environments.inspect})"
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
795
825
|
# Validate notification_minimum_severity (must be valid symbol)
|
|
796
826
|
if notification_minimum_severity
|
|
797
827
|
valid_notification_severities = %i[critical high medium low]
|
|
@@ -822,6 +852,28 @@ module RailsErrorDashboard
|
|
|
822
852
|
true
|
|
823
853
|
end
|
|
824
854
|
|
|
855
|
+
# The environment this process attributes captured errors to.
|
|
856
|
+
#
|
|
857
|
+
# Explicit option first, then Rails.env. Never nil and never raises: an
|
|
858
|
+
# error must still be captured when the environment cannot be named.
|
|
859
|
+
#
|
|
860
|
+
# @return [String]
|
|
861
|
+
def current_environment
|
|
862
|
+
name = environment.to_s.strip
|
|
863
|
+
return name unless name.empty?
|
|
864
|
+
|
|
865
|
+
rails_env = defined?(Rails) && Rails.respond_to?(:env) ? Rails.env.to_s.strip : ""
|
|
866
|
+
rails_env.empty? ? "unknown" : rails_env
|
|
867
|
+
rescue StandardError
|
|
868
|
+
"unknown"
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
# "production, uat" -> ["production", "uat"]; blank or all-blank -> nil.
|
|
872
|
+
def parse_name_list(raw)
|
|
873
|
+
list = raw.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
874
|
+
list.empty? ? nil : list
|
|
875
|
+
end
|
|
876
|
+
|
|
825
877
|
# Check if using default or blank demo credentials with basic auth
|
|
826
878
|
#
|
|
827
879
|
# Returns false if the user explicitly set ENV vars (even to the same default values),
|
|
@@ -25,6 +25,7 @@ module RailsErrorDashboard
|
|
|
25
25
|
errors_over_time: errors_over_time,
|
|
26
26
|
errors_by_type: errors_by_type,
|
|
27
27
|
errors_by_platform: errors_by_platform,
|
|
28
|
+
errors_by_environment: errors_by_environment,
|
|
28
29
|
errors_by_hour: errors_by_hour,
|
|
29
30
|
top_users: top_affected_users,
|
|
30
31
|
resolution_rate: resolution_rate,
|
|
@@ -88,6 +89,14 @@ module RailsErrorDashboard
|
|
|
88
89
|
base_query.group(:platform).count
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
# NULL (captured before the column existed) is reported under :unknown
|
|
93
|
+
# rather than dropped, so the chart's total still matches the period.
|
|
94
|
+
def errors_by_environment
|
|
95
|
+
return {} unless ErrorLog.column_names.include?("environment")
|
|
96
|
+
|
|
97
|
+
base_query.group(:environment).count.transform_keys { |env| env.nil? ? :unknown : env }
|
|
98
|
+
end
|
|
99
|
+
|
|
91
100
|
def errors_by_hour
|
|
92
101
|
# group_by_hour_of_day buckets into 0..23 to show diurnal patterns
|
|
93
102
|
# (when in the day errors peak). The chart title says "Errors by Hour
|
|
@@ -28,6 +28,7 @@ module RailsErrorDashboard
|
|
|
28
28
|
query = filter_by_error_type(query)
|
|
29
29
|
query = filter_by_resolved(query)
|
|
30
30
|
query = filter_by_platform(query)
|
|
31
|
+
query = filter_by_environment(query)
|
|
31
32
|
query = filter_by_application(query)
|
|
32
33
|
query = filter_by_user_id(query)
|
|
33
34
|
query = filter_by_app_version(query)
|
|
@@ -92,6 +93,13 @@ module RailsErrorDashboard
|
|
|
92
93
|
query.where(platform: @filters[:platform])
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
def filter_by_environment(query)
|
|
97
|
+
return query unless @filters[:environment].present?
|
|
98
|
+
return query unless ErrorLog.column_names.include?("environment")
|
|
99
|
+
|
|
100
|
+
query.where(environment: @filters[:environment])
|
|
101
|
+
end
|
|
102
|
+
|
|
95
103
|
def filter_by_application(query)
|
|
96
104
|
return query unless @filters[:application_id].present?
|
|
97
105
|
|
|
@@ -17,11 +17,20 @@ module RailsErrorDashboard
|
|
|
17
17
|
{
|
|
18
18
|
error_types: base_scope.distinct.pluck(:error_type).compact.sort,
|
|
19
19
|
platforms: base_scope.distinct.pluck(:platform).compact,
|
|
20
|
+
environments: environments,
|
|
20
21
|
applications: Application.ordered_by_name.pluck(:name, :id),
|
|
21
22
|
assignees: assignees
|
|
22
23
|
}
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
# Sorted so the select is stable between requests. Empty until the
|
|
27
|
+
# environment migration has run, so the index simply shows no filter.
|
|
28
|
+
def environments
|
|
29
|
+
return [] unless ErrorLog.column_names.include?("environment")
|
|
30
|
+
|
|
31
|
+
base_scope.distinct.pluck(:environment).compact.sort
|
|
32
|
+
end
|
|
33
|
+
|
|
25
34
|
def assignees
|
|
26
35
|
base_scope.where.not(assigned_to: nil)
|
|
27
36
|
.select(:assigned_to)
|
|
@@ -47,6 +47,7 @@ module RailsErrorDashboard
|
|
|
47
47
|
value: error_log.platform || unknown,
|
|
48
48
|
inline: true
|
|
49
49
|
},
|
|
50
|
+
*environment_fields(error_log, locale),
|
|
50
51
|
{
|
|
51
52
|
name: NotificationHelpers.label(:occurrences, locale),
|
|
52
53
|
value: error_log.occurrence_count.to_s,
|
|
@@ -84,6 +85,13 @@ module RailsErrorDashboard
|
|
|
84
85
|
|
|
85
86
|
# @param error_log [ErrorLog] The error
|
|
86
87
|
# @return [Integer] Discord color integer
|
|
88
|
+
# One field when the error carries an environment, none for a legacy row.
|
|
89
|
+
def self.environment_fields(error_log, locale)
|
|
90
|
+
return [] unless error_log.respond_to?(:environment) && error_log.environment.present?
|
|
91
|
+
|
|
92
|
+
[ { name: NotificationHelpers.label(:environment, locale), value: error_log.environment, inline: true } ]
|
|
93
|
+
end
|
|
94
|
+
|
|
87
95
|
def self.severity_color(error_log)
|
|
88
96
|
SEVERITY_COLORS[error_log.severity] || DEFAULT_COLOR
|
|
89
97
|
end
|
|
@@ -23,9 +23,11 @@ module RailsErrorDashboard
|
|
|
23
23
|
|
|
24
24
|
platforms = ErrorLog.distinct.pluck(:platform).compact
|
|
25
25
|
show_platform = platforms.size > 1
|
|
26
|
+
show_environment = ErrorLog.column_names.include?("environment") &&
|
|
27
|
+
ErrorLog.distinct.pluck(:environment).compact.size > 1
|
|
26
28
|
|
|
27
29
|
html = render_partial("rails_error_dashboard/errors/error_row",
|
|
28
|
-
error: error_log, show_platform: show_platform)
|
|
30
|
+
error: error_log, show_platform: show_platform, show_environment: show_environment)
|
|
29
31
|
|
|
30
32
|
Turbo::StreamsChannel.broadcast_prepend_to(
|
|
31
33
|
"error_list",
|
|
@@ -46,9 +48,11 @@ module RailsErrorDashboard
|
|
|
46
48
|
|
|
47
49
|
platforms = ErrorLog.distinct.pluck(:platform).compact
|
|
48
50
|
show_platform = platforms.size > 1
|
|
51
|
+
show_environment = ErrorLog.column_names.include?("environment") &&
|
|
52
|
+
ErrorLog.distinct.pluck(:environment).compact.size > 1
|
|
49
53
|
|
|
50
54
|
html = render_partial("rails_error_dashboard/errors/error_row",
|
|
51
|
-
error: error_log, show_platform: show_platform)
|
|
55
|
+
error: error_log, show_platform: show_platform, show_environment: show_environment)
|
|
52
56
|
|
|
53
57
|
Turbo::StreamsChannel.broadcast_replace_to(
|
|
54
58
|
"error_list",
|
|
@@ -104,6 +104,7 @@ module RailsErrorDashboard
|
|
|
104
104
|
|
|
105
105
|
def metadata_section
|
|
106
106
|
items = []
|
|
107
|
+
items << "- **Environment:** #{@error.environment}" if @error.respond_to?(:environment) && @error.environment.present?
|
|
107
108
|
items << "- **Platform:** #{@error.platform}" if @error.platform.present?
|
|
108
109
|
items << "- **First seen:** #{@error.first_seen_at&.utc&.strftime("%Y-%m-%d %H:%M:%S UTC")}" if @error.first_seen_at
|
|
109
110
|
items << "- **Occurrences:** #{@error.occurrence_count}" if @error.occurrence_count
|
|
@@ -435,6 +435,7 @@ module RailsErrorDashboard
|
|
|
435
435
|
|
|
436
436
|
def metadata_section
|
|
437
437
|
items = []
|
|
438
|
+
items << "- **Environment:** #{@error.environment}" if @error.respond_to?(:environment) && @error.environment.present?
|
|
438
439
|
items << "- **Platform:** #{@error.platform}" if @error.platform.present?
|
|
439
440
|
items << "- **First seen:** #{@error.first_seen_at&.utc&.strftime("%Y-%m-%d %H:%M:%S UTC")}" if @error.first_seen_at
|
|
440
441
|
items << "- **Occurrences:** #{@error.occurrence_count}" if @error.occurrence_count
|
|
@@ -29,6 +29,29 @@ module RailsErrorDashboard
|
|
|
29
29
|
true
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Does this error's environment pass config.notification_environments?
|
|
33
|
+
#
|
|
34
|
+
# nil list → everything notifies (the pre-0.11 behaviour). Accepts an
|
|
35
|
+
# ErrorLog, a bare environment name, or nothing (the process
|
|
36
|
+
# environment) — the storm and baseline paths have no row to hand over.
|
|
37
|
+
# A legacy row with a NULL environment is judged as the process
|
|
38
|
+
# environment, so upgrading never silences an error that was notifying
|
|
39
|
+
# before. Fails open: an allowlist bug must not lose a page.
|
|
40
|
+
#
|
|
41
|
+
# @param subject [ErrorLog, String, nil]
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
def environment_allowed?(subject = nil)
|
|
44
|
+
allowed = RailsErrorDashboard.configuration.notification_environments
|
|
45
|
+
return true if allowed.nil?
|
|
46
|
+
|
|
47
|
+
name = subject.respond_to?(:environment) ? subject.environment : subject
|
|
48
|
+
name = RailsErrorDashboard.configuration.current_environment if name.blank?
|
|
49
|
+
allowed.include?(name.to_s)
|
|
50
|
+
rescue => e
|
|
51
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] NotificationThrottler.environment_allowed? failed: #{e.message}")
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
32
55
|
# Does the error's severity meet the configured minimum?
|
|
33
56
|
# @param error_log [ErrorLog] The error to check
|
|
34
57
|
# @return [Boolean] true if severity is at or above minimum
|
|
@@ -46,6 +46,7 @@ module RailsErrorDashboard
|
|
|
46
46
|
controller: error_log.controller_name,
|
|
47
47
|
action: error_log.action_name,
|
|
48
48
|
platform: error_log.platform,
|
|
49
|
+
environment: error_log.environment,
|
|
49
50
|
occurrences: error_log.occurrence_count,
|
|
50
51
|
first_seen_at: error_log.first_seen_at&.iso8601,
|
|
51
52
|
last_seen_at: error_log.last_seen_at&.iso8601,
|
|
@@ -64,6 +64,7 @@ module RailsErrorDashboard
|
|
|
64
64
|
locale
|
|
65
65
|
)
|
|
66
66
|
},
|
|
67
|
+
*environment_fields(error_log, locale),
|
|
67
68
|
{
|
|
68
69
|
type: "mrkdwn",
|
|
69
70
|
text: NotificationHelpers.field(
|
|
@@ -74,6 +75,14 @@ module RailsErrorDashboard
|
|
|
74
75
|
}
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
# One field when the error carries an environment, none for a legacy
|
|
79
|
+
# row — so pre-0.11 notifications render exactly as they did.
|
|
80
|
+
def self.environment_fields(error_log, locale)
|
|
81
|
+
return [] unless error_log.respond_to?(:environment) && error_log.environment.present?
|
|
82
|
+
|
|
83
|
+
[ { type: "mrkdwn", text: NotificationHelpers.field(:environment, error_log.environment, locale) } ]
|
|
84
|
+
end
|
|
85
|
+
|
|
77
86
|
def self.message_block(error_log, locale)
|
|
78
87
|
{
|
|
79
88
|
type: "section",
|
|
@@ -232,6 +232,8 @@ module RailsErrorDashboard
|
|
|
232
232
|
def maybe_storm_notification(state)
|
|
233
233
|
return if state == :closed
|
|
234
234
|
return unless RailsErrorDashboard.configuration.storm_notification
|
|
235
|
+
# A storm on staging should not page whoever is on call for production.
|
|
236
|
+
return unless RailsErrorDashboard::Services::NotificationThrottler.environment_allowed?
|
|
235
237
|
|
|
236
238
|
episode = breaker.episode_snapshot
|
|
237
239
|
return unless episode
|
|
@@ -37,6 +37,7 @@ module RailsErrorDashboard
|
|
|
37
37
|
message: error_log.message,
|
|
38
38
|
severity: error_log.severity.to_s,
|
|
39
39
|
platform: error_log.platform,
|
|
40
|
+
environment: error_log.environment,
|
|
40
41
|
controller: error_log.controller_name,
|
|
41
42
|
action: error_log.action_name,
|
|
42
43
|
occurrence_count: error_log.occurrence_count,
|
|
@@ -109,6 +109,7 @@ require "rails_error_dashboard/commands/resolve_error"
|
|
|
109
109
|
require "rails_error_dashboard/commands/create_issue"
|
|
110
110
|
require "rails_error_dashboard/commands/link_existing_issue"
|
|
111
111
|
require "rails_error_dashboard/commands/flush_storm_counts"
|
|
112
|
+
require "rails_error_dashboard/commands/backfill_environments"
|
|
112
113
|
require "rails_error_dashboard/services/issue_body_formatter"
|
|
113
114
|
require "rails_error_dashboard/commands/batch_resolve_errors"
|
|
114
115
|
require "rails_error_dashboard/commands/batch_delete_errors"
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
namespace :rails_error_dashboard do
|
|
4
|
+
desc "Fill in environment on error logs captured before the column existed (from environment_info.rails_env)"
|
|
5
|
+
task backfill_environments: :environment do
|
|
6
|
+
updated = RailsErrorDashboard::Commands::BackfillEnvironments.call
|
|
7
|
+
puts "rails_error_dashboard: backfilled environment on #{updated} error log(s)."
|
|
8
|
+
end
|
|
9
|
+
|
|
4
10
|
namespace :db do
|
|
5
11
|
desc "Drop all Rails Error Dashboard database tables (⚠️ DESTRUCTIVE - deletes all error data)"
|
|
6
12
|
task drop: :environment do
|
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.
|
|
4
|
+
version: 0.11.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -219,29 +219,24 @@ dependencies:
|
|
|
219
219
|
- - "~>"
|
|
220
220
|
- !ruby/object:Gem::Version
|
|
221
221
|
version: '0.15'
|
|
222
|
-
description: '
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
mutates your app''s I18n; a missing translation falls back to English. Backtraces,
|
|
241
|
-
exception names and webhook payload keys stay English by design. The ten non-English
|
|
242
|
-
locales are machine-translated and not yet reviewed by native speakers. 5-minute
|
|
243
|
-
setup, works out-of-the-box. Rails 7.0-8.1, Ruby 3.2-4.0. BETA: API may change before
|
|
244
|
-
v1.0.0. Live demo: https://rails-error-dashboard.anjan.dev (gandalf/youshallnotpass)'
|
|
222
|
+
description: 'Rails Error Dashboard (RED) is an open-source, self-hosted Rails engine
|
|
223
|
+
for investigating production exceptions without sending error data to a monitoring
|
|
224
|
+
vendor. It groups errors and records request context and cause chains and, when
|
|
225
|
+
enabled, breadcrumbs plus local and instance variables captured before Ruby unwinds
|
|
226
|
+
the stack. RED attaches Rails and Ruby runtime health to the error record on every
|
|
227
|
+
captured occurrence, including Active Record pool, Puma, background jobs, GC, memory,
|
|
228
|
+
threads, file descriptors and system pressure. Built-in storm protection progressively
|
|
229
|
+
sheds expensive context and I/O during error floods while retaining useful exemplars
|
|
230
|
+
and exact occurrence counts. Run RED with your application''s database or an isolated
|
|
231
|
+
error database. It supports PostgreSQL, MySQL/Trilogy and SQLite, and includes workflow,
|
|
232
|
+
notifications (Slack, Email, Discord, PagerDuty, webhooks), two-way issue sync with
|
|
233
|
+
GitHub, GitLab, Codeberg and Linear, Copy as RSpec/curl/LLM, swallowed-exception
|
|
234
|
+
detection, LLM observability without prompt capture, OpenTelemetry span export and
|
|
235
|
+
Rails-specific operational views. The dashboard is translated into 11 languages
|
|
236
|
+
(machine-translated outside English, awaiting native review). A self-hosted Sentry
|
|
237
|
+
alternative that keeps error data in your own database. The gem is MIT and free
|
|
238
|
+
forever. Supports Rails 7.0-8.1 and Ruby 3.2-4.0. Beta: APIs may change before 1.0.
|
|
239
|
+
Live demo: https://rails-error-dashboard.anjan.dev'
|
|
245
240
|
email:
|
|
246
241
|
- anjan.jagirdar@gmail.com
|
|
247
242
|
executables: []
|
|
@@ -392,6 +387,7 @@ files:
|
|
|
392
387
|
- db/migrate/20260613000001_create_storm_events.rb
|
|
393
388
|
- db/migrate/20260730000001_create_rails_error_dashboard_rack_attack_events.rb
|
|
394
389
|
- db/migrate/20260824000001_add_user_agent_to_rack_attack_events.rb
|
|
390
|
+
- db/migrate/20260826000001_add_environment_to_error_logs.rb
|
|
395
391
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
396
392
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
397
393
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -401,6 +397,7 @@ files:
|
|
|
401
397
|
- lib/rails_error_dashboard.rb
|
|
402
398
|
- lib/rails_error_dashboard/commands/add_error_comment.rb
|
|
403
399
|
- lib/rails_error_dashboard/commands/assign_error.rb
|
|
400
|
+
- lib/rails_error_dashboard/commands/backfill_environments.rb
|
|
404
401
|
- lib/rails_error_dashboard/commands/batch_delete_errors.rb
|
|
405
402
|
- lib/rails_error_dashboard/commands/batch_mute_errors.rb
|
|
406
403
|
- lib/rails_error_dashboard/commands/batch_resolve_errors.rb
|
|
@@ -559,7 +556,7 @@ metadata:
|
|
|
559
556
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
560
557
|
post_install_message: |
|
|
561
558
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
562
|
-
RED (Rails Error Dashboard) v0.
|
|
559
|
+
RED (Rails Error Dashboard) v0.11.1
|
|
563
560
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
564
561
|
|
|
565
562
|
First install:
|
|
@@ -597,6 +594,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
597
594
|
requirements: []
|
|
598
595
|
rubygems_version: 3.6.9
|
|
599
596
|
specification_version: 4
|
|
600
|
-
summary:
|
|
601
|
-
|
|
597
|
+
summary: Rails-native, self-hosted error monitoring with exception-time Ruby state,
|
|
598
|
+
runtime health, and storm-safe capture.
|
|
602
599
|
test_files: []
|