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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +123 -49
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +3 -0
  4. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +8 -1
  5. data/app/models/rails_error_dashboard/error_log.rb +1 -0
  6. data/app/views/layouts/rails_error_dashboard.html.erb +30 -0
  7. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +5 -0
  8. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -0
  9. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +8 -0
  10. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +9 -0
  11. data/app/views/rails_error_dashboard/errors/analytics.html.erb +51 -1
  12. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +1 -1
  13. data/app/views/rails_error_dashboard/errors/index.html.erb +8 -1
  14. data/app/views/rails_error_dashboard/errors/overview.html.erb +2 -2
  15. data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
  16. data/config/locales/de.yml +19 -2
  17. data/config/locales/en.yml +17 -0
  18. data/config/locales/es.yml +18 -2
  19. data/config/locales/fr.yml +19 -2
  20. data/config/locales/it.yml +27 -3
  21. data/config/locales/ja.yml +17 -3
  22. data/config/locales/pl.yml +19 -2
  23. data/config/locales/pt-BR.yml +18 -2
  24. data/config/locales/ru.yml +18 -2
  25. data/config/locales/uk.yml +18 -2
  26. data/config/locales/zh-CN.yml +15 -3
  27. data/db/migrate/20260826000001_add_environment_to_error_logs.rb +26 -0
  28. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +11 -0
  29. data/lib/rails_error_dashboard/commands/backfill_environments.rb +58 -0
  30. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +76 -29
  31. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +40 -9
  32. data/lib/rails_error_dashboard/commands/log_error.rb +17 -0
  33. data/lib/rails_error_dashboard/configuration.rb +52 -0
  34. data/lib/rails_error_dashboard/queries/analytics_stats.rb +9 -0
  35. data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
  36. data/lib/rails_error_dashboard/queries/filter_options.rb +9 -0
  37. data/lib/rails_error_dashboard/services/discord_payload_builder.rb +8 -0
  38. data/lib/rails_error_dashboard/services/error_broadcaster.rb +6 -2
  39. data/lib/rails_error_dashboard/services/issue_body_formatter.rb +1 -0
  40. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +1 -0
  41. data/lib/rails_error_dashboard/services/notification_throttler.rb +23 -0
  42. data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +1 -0
  43. data/lib/rails_error_dashboard/services/slack_payload_builder.rb +9 -0
  44. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -0
  45. data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +1 -0
  46. data/lib/rails_error_dashboard/version.rb +1 -1
  47. data/lib/rails_error_dashboard.rb +1 -0
  48. data/lib/tasks/rails_error_dashboard_tasks.rake +6 -0
  49. 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
- # Priority 1: unresolved match — one UPDATE, no row instantiation
64
- updated = ErrorLog.unresolved
65
- .where(error_hash: error_hash, application_id: application.id)
66
- .update_all([ "occurrence_count = occurrence_count + ?, last_seen_at = ?", count, last_seen ])
67
- return count if updated.positive?
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
- resolved = ErrorLog
86
+ resolved_scope = ErrorLog
72
87
  .where(error_hash: error_hash, application_id: application.id)
73
88
  .where(status: %w[resolved wont_fix])
74
- .order(last_seen_at: :desc)
75
- .first
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,
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.1"
3
3
  end
@@ -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.10.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: 'Own your errors. Own your stack. A fully open-source, self-hosted error
223
- tracking Rails engine — a free Sentry alternative that runs entirely inside your
224
- own process, with no external services and zero recurring cost. Captures what SaaS
225
- tools charge extra for: local and instance variables at the moment of failure (via
226
- TracePoint), exception cause chains, swallowed-exception detection, breadcrumbs,
227
- and system-health snapshots (GC, memory, threads, connection pool, Puma). Plus N+1
228
- query detection, storm protection (a circuit breaker that shields your app from
229
- error floods, ON by default), multi-app support, error sampling, and async logging
230
- via Sidekiq, SolidQueue, or GoodJob. Runs on SQLite, PostgreSQL, or MySQL/Trilogy
231
- — in your app''s existing database or an isolated separate error database. Beautiful
232
- dashboard UI (dark/light), multi-channel notifications (Slack, Email, Discord, PagerDuty,
233
- webhooks), workflow management, advanced analytics, platform detection (iOS/Android/Web/API),
234
- and two-way issue sync with GitHub, GitLab, Codeberg, and Linear. Also: LLM observability,
235
- AI-powered debugging help, and OpenTelemetry span export. The dashboard, mailers
236
- and notification payloads are translated into 11 languages: English, German, Spanish,
237
- French, Brazilian Portuguese, Italian, Polish, Russian, Ukrainian, Japanese, and
238
- Simplified Chinese — 1,515 source strings, set via config.dashboard_locale with
239
- a per-user picker. RED translates through its own private I18n backend and never
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.10.0
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: Self-hosted error tracking for Rails — local variables, system health, separate
601
- or shared database, dashboard in 11 languages. A free, open-source Sentry alternative.
597
+ summary: Rails-native, self-hosted error monitoring with exception-time Ruby state,
598
+ runtime health, and storm-safe capture.
602
599
  test_files: []