rails_error_dashboard 0.15.0 → 0.16.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -3
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +29 -2
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +10 -9
  5. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +7 -14
  6. data/app/helpers/rails_error_dashboard/overview_helper.rb +4 -0
  7. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +5 -0
  8. data/app/models/rails_error_dashboard/error_log.rb +11 -8
  9. data/app/models/rails_error_dashboard/error_occurrence.rb +1 -0
  10. data/app/models/rails_error_dashboard/error_occurrence_context.rb +14 -0
  11. data/app/views/layouts/rails_error_dashboard.html.erb +3 -1
  12. data/app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb +45 -0
  13. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +10 -3
  14. data/app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb +12 -0
  15. data/app/views/rails_error_dashboard/errors/overview.html.erb +10 -7
  16. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +16 -11
  17. data/app/views/rails_error_dashboard/errors/settings.html.erb +6 -5
  18. data/app/views/rails_error_dashboard/errors/show.html.erb +7 -4
  19. data/config/locales/de.yml +19 -0
  20. data/config/locales/en.yml +21 -0
  21. data/config/locales/es.yml +20 -0
  22. data/config/locales/fr.yml +20 -0
  23. data/config/locales/it.yml +20 -0
  24. data/config/locales/ja.yml +18 -0
  25. data/config/locales/pl.yml +21 -0
  26. data/config/locales/pt-BR.yml +20 -0
  27. data/config/locales/ru.yml +21 -0
  28. data/config/locales/uk.yml +21 -0
  29. data/config/locales/zh-CN.yml +18 -0
  30. data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +3 -3
  31. data/db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb +35 -0
  32. data/db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb +46 -0
  33. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +14 -5
  34. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +11 -2
  35. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +7 -2
  36. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +7 -2
  37. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +6 -0
  38. data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +2 -4
  39. data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +2 -4
  40. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +7 -6
  41. data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +2 -4
  42. data/lib/rails_error_dashboard/commands/log_error.rb +19 -7
  43. data/lib/rails_error_dashboard/commands/record_occurrence_context.rb +60 -0
  44. data/lib/rails_error_dashboard/configuration.rb +19 -3
  45. data/lib/rails_error_dashboard/error_reporter.rb +4 -2
  46. data/lib/rails_error_dashboard/manual_error_reporter.rb +11 -1
  47. data/lib/rails_error_dashboard/middleware/error_catcher.rb +18 -4
  48. data/lib/rails_error_dashboard/queries/analytics_stats.rb +0 -12
  49. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +59 -33
  50. data/lib/rails_error_dashboard/queries/platform_comparison.rb +14 -6
  51. data/lib/rails_error_dashboard/services/backtrace_processor.rb +16 -7
  52. data/lib/rails_error_dashboard/services/crash_capture.rb +5 -0
  53. data/lib/rails_error_dashboard/services/github_link_generator.rb +1 -6
  54. data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +15 -16
  55. data/lib/rails_error_dashboard/services/repository_url.rb +18 -0
  56. data/lib/rails_error_dashboard/services/similarity_calculator.rb +3 -9
  57. data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +6 -3
  58. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +6 -1
  59. data/lib/rails_error_dashboard/value_objects/occurrence_snapshot.rb +23 -0
  60. data/lib/rails_error_dashboard/version.rb +1 -1
  61. data/lib/rails_error_dashboard.rb +16 -0
  62. data/lib/tasks/error_dashboard.rake +2 -3
  63. metadata +10 -2
@@ -16,6 +16,13 @@ module RailsErrorDashboard
16
16
  # puts "#{result[:error].error_type} occurred #{result[:frequency]} times together"
17
17
  # end
18
18
  class CoOccurringErrors
19
+ # The newest occurrences of the target error that are compared. This
20
+ # renders on every error page while the feature is on, and it used to run
21
+ # one window query per lifetime occurrence: 50k occurrences, 50k queries.
22
+ TARGET_OCCURRENCES = 500
23
+ # At most this many occurrences of other errors are read for those windows
24
+ NEARBY_OCCURRENCES = 20_000
25
+
19
26
  # Find co-occurring errors
20
27
  #
21
28
  # @param error_log_id [Integer] ID of target error
@@ -34,53 +41,72 @@ module RailsErrorDashboard
34
41
  @limit = limit.to_i
35
42
  end
36
43
 
44
+ # Four queries however many occurrences there are: the target exists, its
45
+ # newest occurrence times, other errors' occurrences in those windows, and
46
+ # the co-occurring errors themselves.
37
47
  def find_co_occurring
38
- target_error = ErrorLog.find_by(id: @error_log_id)
39
- return [] unless target_error
40
-
41
- # Get all occurrences of the target error
42
- target_occurrences = ErrorOccurrence.where(error_log_id: @error_log_id)
43
- return [] if target_occurrences.empty?
44
-
45
- # For each occurrence, find other errors in the time window
46
- co_occurrence_data = Hash.new { |h, k| h[k] = { count: 0, delays: [] } }
48
+ return [] unless ErrorLog.exists?(id: @error_log_id)
47
49
 
48
- target_occurrences.find_each do |occurrence|
49
- window = @window_minutes.minutes
50
- start_time = occurrence.occurred_at - window
51
- end_time = occurrence.occurred_at + window
50
+ targets = ErrorOccurrence.where(error_log_id: @error_log_id)
51
+ .order(occurred_at: :desc).limit(TARGET_OCCURRENCES).pluck(:occurred_at).sort
52
+ return [] if targets.empty?
52
53
 
53
- # Find other error occurrences in this time window
54
- nearby_occurrences = ErrorOccurrence
55
- .in_time_window(start_time, end_time)
56
- .where.not(error_log_id: @error_log_id)
57
- .includes(:error_log)
54
+ window = @window_minutes.minutes
55
+ stats = Hash.new { |h, k| h[k] = { count: 0, delay_sum: 0.0 } }
58
56
 
59
- nearby_occurrences.each do |nearby|
60
- error_log_id = nearby.error_log_id
61
- co_occurrence_data[error_log_id][:count] += 1
57
+ nearby_occurrences(merged_windows(targets, window)).each do |error_log_id, occurred_at|
58
+ # Each target occurrence within the window of this one is a pair, as
59
+ # before. Delay is negative when this error came first.
60
+ index = targets.bsearch_index { |target| target >= occurred_at - window }
61
+ next unless index
62
62
 
63
- # Calculate delay (negative = before, positive = after target error)
64
- delay = (nearby.occurred_at - occurrence.occurred_at).to_f
65
- co_occurrence_data[error_log_id][:delays] << delay
63
+ while index < targets.size && targets[index] <= occurred_at + window
64
+ stats[error_log_id][:count] += 1
65
+ stats[error_log_id][:delay_sum] += (occurred_at - targets[index]).to_f
66
+ index += 1
66
67
  end
67
68
  end
68
69
 
69
- # Filter by minimum frequency and build results
70
- results = co_occurrence_data.select { |_id, data| data[:count] >= @min_frequency }.map do |error_log_id, data|
71
- error = ErrorLog.find(error_log_id)
72
- avg_delay = data[:delays].sum / data[:delays].size
70
+ frequent = stats.select { |_id, data| data[:count] >= @min_frequency }
71
+ errors = ErrorLog.where(id: frequent.keys).index_by(&:id)
73
72
 
74
- {
75
- error: error,
76
- frequency: data[:count],
77
- avg_delay_seconds: avg_delay.round(2)
78
- }
73
+ results = frequent.filter_map do |error_log_id, data|
74
+ error = errors[error_log_id] or next
75
+ { error: error, frequency: data[:count], avg_delay_seconds: (data[:delay_sum] / data[:count]).round(2) }
79
76
  end
80
77
 
81
78
  # Sort by frequency (most common first) and limit results
82
79
  results.sort_by { |r| -r[:frequency] }.first(@limit)
83
80
  end
81
+
82
+ private
83
+
84
+ # The windows around each target time, overlapping ones merged, so a burst
85
+ # of target occurrences becomes one range
86
+ def merged_windows(targets, window)
87
+ targets.each_with_object([]) do |target, ranges|
88
+ from = target - window
89
+ to = target + window
90
+ if ranges.any? && from <= ranges.last[1]
91
+ ranges.last[1] = to
92
+ else
93
+ ranges << [ from, to ]
94
+ end
95
+ end
96
+ end
97
+
98
+ # [error_log_id, occurred_at] for other errors' occurrences in any window
99
+ def nearby_occurrences(ranges)
100
+ column = "#{ErrorOccurrence.table_name}.occurred_at"
101
+ in_a_window = ranges.map { "(#{column} BETWEEN ? AND ?)" }.join(" OR ")
102
+
103
+ ErrorOccurrence
104
+ .where.not(error_log_id: @error_log_id)
105
+ .where(in_a_window, *ranges.flatten)
106
+ .order(occurred_at: :desc)
107
+ .limit(NEARBY_OCCURRENCES)
108
+ .pluck(:error_log_id, :occurred_at)
109
+ end
84
110
  end
85
111
  end
86
112
  end
@@ -160,18 +160,25 @@ module RailsErrorDashboard
160
160
  def platform_stability_scores
161
161
  platforms = base_scope.distinct.pluck(:platform).compact
162
162
  error_rates = error_rate_by_platform
163
+ # A score compares platforms. With fewer than two that have errors in
164
+ # the window there is nothing to compare against, so none is rated (a
165
+ # lone platform used to come out Critical every time). The count is of
166
+ # platforms with errors in the window: the list above is every
167
+ # platform ever seen.
168
+ return {} if error_rates.size < 2
163
169
  resolution_times = resolution_time_by_platform
164
170
 
165
- # Find max values for normalization
166
- max_errors = error_rates.values.max || 1
171
+ total_errors = error_rates.values.sum
167
172
  max_resolution_time = resolution_times.values.compact.max || 1
168
173
 
169
174
  platforms.each_with_object({}) do |platform, result|
170
175
  error_count = error_rates[platform] || 0
171
176
  avg_resolution = resolution_times[platform] || 0
172
177
 
173
- # Normalize to 0-1 scale (inverted - lower is better)
174
- error_score = 1.0 - (error_count.to_f / max_errors)
178
+ # This platform's share of all errors, inverted. Against the busiest
179
+ # platform instead, the busiest always scored 0 here and so was always
180
+ # Critical.
181
+ error_score = total_errors.positive? ? 1.0 - (error_count.to_f / total_errors) : 1.0
175
182
  resolution_score = avg_resolution.positive? ? 1.0 - (avg_resolution / max_resolution_time) : 1.0
176
183
 
177
184
  # Weight: 70% error count, 30% resolution time
@@ -252,9 +259,10 @@ module RailsErrorDashboard
252
259
  critical_errors: critical_errors,
253
260
  unresolved_errors: unresolved_errors,
254
261
  resolution_rate: resolution_rate,
255
- stability_score: stability_scores[platform] || 0,
262
+ # nil and :not_rated when there are fewer than two platforms
263
+ stability_score: stability_scores[platform],
256
264
  error_velocity: velocity, # Positive = increasing, negative = decreasing
257
- health_status: determine_health_status(stability_scores[platform] || 0, velocity)
265
+ health_status: stability_scores.key?(platform) ? determine_health_status(stability_scores[platform], velocity) : :not_rated
258
266
  }
259
267
  end
260
268
  end
@@ -99,13 +99,7 @@ module RailsErrorDashboard
99
99
  return nil if backtrace.blank?
100
100
 
101
101
  lines = backtrace.is_a?(String) ? backtrace.split("\n") : backtrace
102
- frames = lines.first(20).map do |line|
103
- if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
104
- "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
105
- elsif line =~ %r{([^/]+\.rb)}
106
- Regexp.last_match(1)
107
- end
108
- end.compact.uniq
102
+ frames = lines.first(20).map { |line| frame_key(line) }.compact.uniq
109
103
 
110
104
  return nil if frames.empty?
111
105
 
@@ -113,6 +107,21 @@ module RailsErrorDashboard
113
107
  Digest::SHA256.hexdigest(file_paths.join("|"))[0..15]
114
108
  end
115
109
 
110
+ # "file.rb:method" for one backtrace line, the unit similar-error matching
111
+ # compares. It accepts Ruby 3.3's in `save' and Ruby 3.4's in 'User#save',
112
+ # and drops 3.4's class qualifier ("block in User#save" -> "block in
113
+ # save"), so a frame matches whichever Ruby recorded it.
114
+ #
115
+ # @param line [String]
116
+ # @return [String, nil]
117
+ def self.frame_key(line)
118
+ if line =~ %r{([^/]+\.rb):.*?in [`'](.+)'$}
119
+ "#{Regexp.last_match(1)}:#{Regexp.last_match(2).gsub(/\b[A-Z][\w:]*[#.]/, "")}"
120
+ elsif line =~ %r{([^/]+\.rb)}
121
+ Regexp.last_match(1)
122
+ end
123
+ end
124
+
116
125
  # Calculate signature directly from Location objects
117
126
  # @param locations [Array<Thread::Backtrace::Location>] Backtrace locations
118
127
  # @return [String, nil] 16-character hex signature
@@ -267,6 +267,11 @@ module RailsErrorDashboard
267
267
  attributes[:exception_cause] = cause_json
268
268
  end
269
269
 
270
+ # The at_exit hook wrote this backtrace from the crashed process
271
+ if ErrorLog.column_names.include?("backtrace_source")
272
+ attributes[:backtrace_source] = "server"
273
+ end
274
+
270
275
  if ErrorLog.column_names.include?("error_hash")
271
276
  attributes[:error_hash] = Services::ErrorHashGenerator.from_attributes(
272
277
  error_type: attributes[:error_type],
@@ -77,14 +77,9 @@ module RailsErrorDashboard
77
77
 
78
78
  private
79
79
 
80
- # Normalize repository URL (remove .git suffix, trailing slashes, etc.)
81
- #
82
80
  # @return [String]
83
81
  def normalize_repository_url
84
- url = repository_url.strip
85
- url = url.chomp(".git")
86
- url = url.chomp("/")
87
- url
82
+ RepositoryUrl.normalize(repository_url)
88
83
  end
89
84
 
90
85
  # Host of the repository URL, downcased, or nil when the value is not an
@@ -64,33 +64,32 @@ module RailsErrorDashboard
64
64
  raw = @error.backtrace
65
65
  return nil if raw.blank?
66
66
 
67
- lines = raw.split("\n")
68
- app_lines = lines.reject { |l| l.include?("/gems/") || l.include?("/ruby/") || l.include?("/vendor/") }
69
- app_lines = lines.first(MAX_BACKTRACE_LINES) if app_lines.empty?
67
+ app_lines = app_frames.map { |frame| frame[:full_line] }
68
+ app_lines = raw.split("\n") if app_lines.empty?
70
69
  app_lines = app_lines.first(MAX_BACKTRACE_LINES)
71
70
 
72
71
  "## Backtrace\n\n```\n#{app_lines.join("\n")}\n```"
73
72
  end
74
73
 
74
+ # The application's frames, as the error page classifies them. RED stores
75
+ # frames shortened ("gems/…", "ruby/…"), so a "/gems/" substring test
76
+ # kept every gem frame.
77
+ def app_frames
78
+ @app_frames ||= Services::BacktraceParser.parse(@error.backtrace.to_s).select { |frame| frame[:category] == :app }
79
+ end
80
+
75
81
  def source_code_section
76
82
  return nil unless defined?(RailsErrorDashboard) &&
77
83
  RailsErrorDashboard.configuration.enable_source_code_integration
78
84
 
79
- raw = @error.backtrace
80
- return nil if raw.blank?
81
-
82
- lines = raw.split("\n")
83
- app_lines = lines.reject { |l| l.include?("/gems/") || l.include?("/ruby/") || l.include?("/vendor/") }
84
- return nil if app_lines.empty?
85
+ return nil if @error.backtrace.blank? || app_frames.empty?
86
+ # A client's frames describe code that ran on the client
87
+ return nil if @error.client_backtrace?
85
88
 
86
89
  snippets = []
87
- app_lines.first(3).each do |frame|
88
- # Parse "file:line:in 'method'" format
89
- match = frame.match(/^(.+?):(\d+)/)
90
- next unless match
91
-
92
- file_path = match[1]
93
- line_number = match[2].to_i
90
+ app_frames.first(3).each do |frame|
91
+ file_path = frame[:file_path]
92
+ line_number = frame[:line_number]
94
93
 
95
94
  reader = Services::SourceCodeReader.new(file_path, line_number)
96
95
  source_lines = reader.read_lines(context: 3)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # The one normal form of a configured repository URL, shared by the View
6
+ # Source links and the issue tracker: no surrounding whitespace, no trailing
7
+ # slashes, no ".git". The slashes go first, or ".git/" keeps its ".git".
8
+ module RepositoryUrl
9
+ # @param url [String, nil]
10
+ # @return [String, nil]
11
+ def self.normalize(url)
12
+ return nil if url.nil?
13
+
14
+ url.to_s.strip.sub(%r{/+\z}, "").delete_suffix(".git").sub(%r{/+\z}, "")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -80,19 +80,13 @@ module RailsErrorDashboard
80
80
  end
81
81
 
82
82
  # Extract meaningful frames from backtrace
83
- # Format: "file.rb:123:in `method_name`" => "file.rb:method_name"
83
+ # "file.rb:123:in `name'" (Ruby 3.3) or "file.rb:123:in 'Class#name'"
84
+ # (Ruby 3.4) => "file.rb:name", via BacktraceProcessor.frame_key
84
85
  def extract_frames(backtrace)
85
86
  return [] if backtrace.blank?
86
87
 
87
88
  lines = backtrace.is_a?(String) ? backtrace.split("\n") : backtrace
88
- lines.first(20).map do |line| # Only consider first 20 frames
89
- # Extract file path and method name, ignore line numbers
90
- if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
91
- "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
92
- elsif line =~ %r{([^/]+\.rb)}
93
- Regexp.last_match(1)
94
- end
95
- end.compact.uniq
89
+ lines.first(20).map { |line| BacktraceProcessor.frame_key(line) }.compact.uniq # Only consider first 20 frames
96
90
  end
97
91
 
98
92
  # Normalize message for comparison
@@ -38,7 +38,8 @@ module RailsErrorDashboard
38
38
  Entry = Struct.new(
39
39
  :error_class, :message, :first_app_frame,
40
40
  :controller_name, :action_name, :custom_hash, :environment,
41
- :opaque_identity, :count, :first_seen_at, :last_seen_at, :buckets
41
+ :opaque_identity, :count, :first_seen_at, :last_seen_at, :buckets,
42
+ :capture_source
42
43
  )
43
44
 
44
45
  # The bucket an instant belongs to, as an epoch second.
@@ -120,6 +121,7 @@ module RailsErrorDashboard
120
121
  # job CANNOT recompute this -- it has to travel, or a storm count
121
122
  # lands on a different row than the full capture path would.
122
123
  "opaque_identity" => entry.opaque_identity,
124
+ "capture_source" => entry.capture_source,
123
125
  "count" => entry.count.value,
124
126
  "first_seen_at" => entry.first_seen_at.iso8601,
125
127
  "last_seen_at" => entry.last_seen_at.iso8601,
@@ -167,7 +169,7 @@ module RailsErrorDashboard
167
169
  parts[:controller_name], parts[:action_name], parts[:custom_hash], parts[:environment],
168
170
  parts[:opaque_identity],
169
171
  Concurrent::AtomicFixnum.new(0), first_seen_at || Time.current, last_seen_at || Time.current,
170
- Concurrent::Map.new
172
+ Concurrent::Map.new, parts[:capture_source]
171
173
  )
172
174
  end
173
175
  end
@@ -216,7 +218,8 @@ module RailsErrorDashboard
216
218
  error_class: entry["error_class"], message: entry["message"],
217
219
  first_app_frame: entry["first_app_frame"], controller_name: entry["controller_name"],
218
220
  action_name: entry["action_name"], custom_hash: entry["custom_hash"],
219
- environment: entry["environment"], opaque_identity: entry["opaque_identity"]
221
+ environment: entry["environment"], opaque_identity: entry["opaque_identity"],
222
+ capture_source: entry["capture_source"]
220
223
  }
221
224
  end
222
225
 
@@ -279,7 +279,12 @@ module RailsErrorDashboard
279
279
  # Only an EXPLICIT environment from the caller. nil means "the
280
280
  # worker's own environment", resolved at flush time exactly as
281
281
  # LogError resolves it for a full capture.
282
- environment: context[:environment].to_s.strip.presence&.[](0, 64)
282
+ environment: context[:environment].to_s.strip.presence&.[](0, 64),
283
+ # Whose backtrace first_app_frame came from, for the row the flush
284
+ # may build from it. Not part of the identity or the gate key: a
285
+ # client report and a server capture of one error share an entry,
286
+ # which keeps the first event's frame, and so its source.
287
+ capture_source: context[:_capture_source] == "client" ? "client" : "server"
283
288
  )
284
289
  end
285
290
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module ValueObjects
5
+ # The error as it looked at one occurrence: a read-only copy of the error
6
+ # row whose breadcrumbs, system health and variables are that occurrence's
7
+ # stored context. Every other field keeps the row's latest value.
8
+ #
9
+ # A real ErrorLog rather than a delegator, so every way a card reads a
10
+ # piece sees the occurrence's value: the instance-variables card goes
11
+ # through read_attribute and error.class.column_names, because
12
+ # Object#instance_variables shadows the attribute reader.
13
+ module OccurrenceSnapshot
14
+ # @param error [ErrorLog] the error the context belongs to
15
+ # @param context [ErrorOccurrenceContext] one of its occurrences' contexts
16
+ # @return [ErrorLog] read-only; a piece the occurrence did not capture is nil
17
+ def self.build(error, context)
18
+ pieces = Commands::RecordOccurrenceContext::PIECES.to_h { |piece| [ piece.to_s, context.read_attribute(piece) ] }
19
+ error.class.instantiate(error.attributes_before_type_cast.merge(pieces)).tap(&:readonly!)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -23,6 +23,7 @@ begin; require "turbo-rails"; rescue LoadError; end
23
23
  # Core library files
24
24
  require "rails_error_dashboard/value_objects/error_context"
25
25
  require "rails_error_dashboard/value_objects/llm_call_event"
26
+ require "rails_error_dashboard/value_objects/occurrence_snapshot"
26
27
  require "rails_error_dashboard/integrations/o_tel"
27
28
  require "rails_error_dashboard/integrations/tracer"
28
29
  require "rails_error_dashboard/integrations/llm_span_processor"
@@ -59,6 +60,7 @@ require "rails_error_dashboard/services/source_code_reader"
59
60
  require "rails_error_dashboard/services/git_blame_reader"
60
61
  require "rails_error_dashboard/services/git_head_reader"
61
62
  require "rails_error_dashboard/services/solid_queue_config_check"
63
+ require "rails_error_dashboard/services/repository_url"
62
64
  require "rails_error_dashboard/services/github_link_generator"
63
65
  require "rails_error_dashboard/services/cause_chain_extractor"
64
66
  require "rails_error_dashboard/services/environment_snapshot"
@@ -116,6 +118,7 @@ require "rails_error_dashboard/queries/error_cascades"
116
118
  require "rails_error_dashboard/queries/baseline_stats"
117
119
  require "rails_error_dashboard/queries/platform_comparison"
118
120
  require "rails_error_dashboard/queries/error_correlation"
121
+ require "rails_error_dashboard/commands/record_occurrence_context"
119
122
  require "rails_error_dashboard/commands/log_error"
120
123
  require "rails_error_dashboard/commands/resolve_error"
121
124
  require "rails_error_dashboard/commands/create_issue"
@@ -226,6 +229,19 @@ module RailsErrorDashboard
226
229
  remove_instance_variable(:@detected_git_sha) if defined?(@detected_git_sha)
227
230
  end
228
231
 
232
+ # The name errors are filed under: config.application_name, then the
233
+ # APPLICATION_NAME variable, then the Rails app's module name. Capture, the
234
+ # storm and event flushers, and the backfill all use this one chain, or the
235
+ # same app's rows end up under two Applications, or under none.
236
+ #
237
+ # @return [String]
238
+ def self.resolved_application_name
239
+ configuration.application_name.presence ||
240
+ ENV["APPLICATION_NAME"].presence ||
241
+ (defined?(Rails) && Rails.application&.class&.module_parent_name) ||
242
+ "Rails Application"
243
+ end
244
+
229
245
  # Register a plugin
230
246
  # @param plugin [Plugin] The plugin instance to register
231
247
  # @return [Boolean] True if registered successfully, false otherwise
@@ -335,9 +335,8 @@ namespace :error_dashboard do
335
335
 
336
336
  desc "Backfill application_id for existing errors"
337
337
  task backfill_application: :environment do
338
- app_name = ENV["APP_NAME"] || ENV["APPLICATION_NAME"] ||
339
- (defined?(Rails) && Rails.application.class.module_parent_name) ||
340
- "Legacy Application"
338
+ # APP_NAME overrides; otherwise the name capture files new errors under
339
+ app_name = ENV["APP_NAME"].presence || RailsErrorDashboard.resolved_application_name
341
340
 
342
341
  puts "\n" + "=" * 80
343
342
  puts "RAILS ERROR DASHBOARD - BACKFILL APPLICATION"
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.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -312,6 +312,7 @@ files:
312
312
  - app/models/rails_error_dashboard/error_log.rb
313
313
  - app/models/rails_error_dashboard/error_logs_record.rb
314
314
  - app/models/rails_error_dashboard/error_occurrence.rb
315
+ - app/models/rails_error_dashboard/error_occurrence_context.rb
315
316
  - app/models/rails_error_dashboard/event_count.rb
316
317
  - app/models/rails_error_dashboard/event_timing_gap.rb
317
318
  - app/models/rails_error_dashboard/missing_translation.rb
@@ -326,6 +327,7 @@ files:
326
327
  - app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb
327
328
  - app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb
328
329
  - app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb
330
+ - app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb
329
331
  - app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb
330
332
  - app/views/rails_error_dashboard/errors/_discussion.html.erb
331
333
  - app/views/rails_error_dashboard/errors/_error_cascades.html.erb
@@ -336,6 +338,7 @@ files:
336
338
  - app/views/rails_error_dashboard/errors/_llm_summary.html.erb
337
339
  - app/views/rails_error_dashboard/errors/_local_variables.html.erb
338
340
  - app/views/rails_error_dashboard/errors/_modals.html.erb
341
+ - app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb
339
342
  - app/views/rails_error_dashboard/errors/_pattern_insights.html.erb
340
343
  - app/views/rails_error_dashboard/errors/_request_context.html.erb
341
344
  - app/views/rails_error_dashboard/errors/_show_scripts.html.erb
@@ -432,6 +435,8 @@ files:
432
435
  - db/migrate/20260920000001_add_buckets_incomplete_to_storm_events.rb
433
436
  - db/migrate/20260920000002_create_event_timing_gaps.rb
434
437
  - db/migrate/20261010000001_create_missing_translations.rb
438
+ - db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb
439
+ - db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb
435
440
  - lib/generators/rails_error_dashboard/install/install_generator.rb
436
441
  - lib/generators/rails_error_dashboard/install/templates/README
437
442
  - lib/generators/rails_error_dashboard/install/templates/initializer.rb
@@ -459,6 +464,7 @@ files:
459
464
  - lib/rails_error_dashboard/commands/link_existing_issue.rb
460
465
  - lib/rails_error_dashboard/commands/log_error.rb
461
466
  - lib/rails_error_dashboard/commands/mute_error.rb
467
+ - lib/rails_error_dashboard/commands/record_occurrence_context.rb
462
468
  - lib/rails_error_dashboard/commands/resolve_error.rb
463
469
  - lib/rails_error_dashboard/commands/scrub_invalid_encoding.rb
464
470
  - lib/rails_error_dashboard/commands/snooze_error.rb
@@ -573,6 +579,7 @@ files:
573
579
  - lib/rails_error_dashboard/services/platform_detector.rb
574
580
  - lib/rails_error_dashboard/services/priority_score_calculator.rb
575
581
  - lib/rails_error_dashboard/services/rack_attack_tracker.rb
582
+ - lib/rails_error_dashboard/services/repository_url.rb
576
583
  - lib/rails_error_dashboard/services/resolution_callbacks.rb
577
584
  - lib/rails_error_dashboard/services/rspec_generator.rb
578
585
  - lib/rails_error_dashboard/services/sensitive_data_filter.rb
@@ -605,6 +612,7 @@ files:
605
612
  - lib/rails_error_dashboard/translation.rb
606
613
  - lib/rails_error_dashboard/value_objects/error_context.rb
607
614
  - lib/rails_error_dashboard/value_objects/llm_call_event.rb
615
+ - lib/rails_error_dashboard/value_objects/occurrence_snapshot.rb
608
616
  - lib/rails_error_dashboard/version.rb
609
617
  - lib/tasks/error_dashboard.rake
610
618
  - lib/tasks/rails_error_dashboard_tasks.rake
@@ -620,7 +628,7 @@ metadata:
620
628
  funding_uri: https://github.com/sponsors/AnjanJ
621
629
  post_install_message: |
622
630
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
623
- RED (Rails Error Dashboard) v0.15.0
631
+ RED (Rails Error Dashboard) v0.16.0
624
632
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
625
633
 
626
634
  First install: