rails_error_dashboard 0.14.0 → 0.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15c3e5f83e8ce2990ce4649cd7c71be0542ca7149863a6bc85ff8505d5ee0a4d
4
- data.tar.gz: ccd0115a385bfbfeab743f6e84a4ff77875f449a0adfc16c54b2fa0e21f43029
3
+ metadata.gz: b8dc068309c56704b429f7798ce4c3c83b628853179ebc2695c50fa684d2fae9
4
+ data.tar.gz: 64e3a06e8b60ce8f4c3fe649ca750f228a16e64405b7e239b8440f53e74e536d
5
5
  SHA512:
6
- metadata.gz: 972ecd630a40e2aa58dc33e22492acafb4910310a078a6172458796b6073420a27f773e6709004bd6172eabc6db1acd0de9128f345fa3931a905ea59592e591b
7
- data.tar.gz: a1b25a254a0154f94f260054ddcf7cedaf08039f4f62b234270eb9b7c0ffa84d03d0e09fb0213f7c53aa143200aa6edcfcd4916d54eb8495b6dcb375b8dca2c7
6
+ metadata.gz: b8605107393952d4e308ebb23f5c1a58ac88dafc9a9664c2429ddb83777a450cb148c1890b25495166a51238d954320bdfcba4b2ef265fe63e8ffea960fb91df
7
+ data.tar.gz: 4ab62e966ce41257abefdb37e6e3f14a6ae732754cac9ef6c4a4ed37af14982f825e90dac17e8681274959d11ec74bf403458a96ee3069af5919def63b7d77e5
@@ -224,16 +224,14 @@ module RailsErrorDashboard
224
224
  previous_start = @start_date
225
225
  previous_end = current_start
226
226
 
227
- # Both periods come from base_query, which carries the application
228
- # filter (and the window start). Querying ErrorLog directly made this
229
- # the one panel on an application-filtered page that counted every app.
230
- current_errors = base_query
231
- .where("occurred_at >= ?", current_start)
232
- .count
233
-
234
- previous_errors = base_query
235
- .where("occurred_at < ?", previous_end)
236
- .count
227
+ # EVENTS in each period, counted where they happened, over app_scope
228
+ # (which carries the application filter -- querying ErrorLog directly
229
+ # once made this the one panel that counted every app). Counting groups
230
+ # BORN in each period measured how many new errors appeared, not whether
231
+ # the application errors more or less: a chronic error that fired all
232
+ # month counted as zero in both halves.
233
+ current_errors = Queries::EventVolume.in_window(app_scope, current_start)
234
+ previous_errors = Queries::EventVolume.in_window(app_scope, previous_start, previous_end)
237
235
 
238
236
  change_percentage = if previous_errors > 0
239
237
  ((current_errors - previous_errors).to_f / previous_errors * 100).round(1)
@@ -261,25 +259,28 @@ module RailsErrorDashboard
261
259
  # Get top error types by platform
262
260
  # Shows which errors are platform-specific vs cross-platform
263
261
  # @return [Hash] Platform => top error types
262
+ #
263
+ # EVENT ranking: each platform's error types by their events in the
264
+ # window, and `also_on` from where else that type had events. Counting
265
+ # groups first seen in the window left out every chronic error, and
266
+ # ranked a type with many one-off groups above one that fired all day.
264
267
  def platform_specific_errors
265
- platforms = base_query.distinct.pluck(:platform).compact
268
+ platforms = app_scope.distinct.pluck(:platform).compact
269
+ counts_by_platform = platforms.index_with do |platform|
270
+ Queries::EventVolume.by_group_attribute(app_scope.where(platform: platform), :error_type, @start_date)
271
+ .select { |_, count| count.positive? }
272
+ end
273
+
274
+ counts_by_platform.each_with_object({}) do |(platform, counts_by_type), result|
275
+ next if counts_by_type.empty?
266
276
 
267
- platforms.each_with_object({}) do |platform, result|
268
- platform_errors = base_query.where(platform: platform)
269
- top_errors = platform_errors
270
- .group(:error_type)
271
- .count
272
- .sort_by { |_, count| -count }
273
- .first(5)
277
+ top_errors = counts_by_type.sort_by { |error_type, count| [ -count, error_type.to_s ] }.first(5)
274
278
 
275
279
  result[platform] = top_errors.map do |error_type, count|
276
- # Check if this error occurs on other platforms
277
- other_platforms = base_query
278
- .where(error_type: error_type)
279
- .where.not(platform: platform)
280
- .distinct
281
- .pluck(:platform)
282
- .compact
280
+ # Other platforms where this type had events in the window
281
+ other_platforms = counts_by_platform
282
+ .select { |other, other_counts| other != platform && other_counts.key?(error_type) }
283
+ .keys
283
284
 
284
285
  {
285
286
  error_type: error_type,
@@ -293,12 +294,20 @@ module RailsErrorDashboard
293
294
 
294
295
  private
295
296
 
296
- def base_query
297
- scope = ErrorLog.where("occurred_at >= ?", @start_date)
297
+ # Every group of the application, NOT cut by date -- what EventVolume
298
+ # must be handed, or it never sees an older group's recurrences.
299
+ def app_scope
300
+ scope = ErrorLog.all
298
301
  scope = scope.where(application_id: @application_id) if @application_id.present?
299
302
  scope
300
303
  end
301
304
 
305
+ # Groups FIRST SEEN in the window. Release attribution (errors by
306
+ # version / git SHA) is first-seen by design, so those figures use this.
307
+ def base_query
308
+ app_scope.where("occurred_at >= ?", @start_date)
309
+ end
310
+
302
311
  # Check if app_version column exists
303
312
  def has_version_column?
304
313
  ErrorLog.column_names.include?("app_version")
@@ -12,6 +12,24 @@ module RailsErrorDashboard
12
12
  # - Platform stability scores
13
13
  # - Cross-platform errors
14
14
  #
15
+ # Two counting units, kept distinct on purpose -- the same split as
16
+ # AnalyticsStats, for the same reason.
17
+ #
18
+ # EVENT figures -> Queries::EventVolume: how much erroring happened in
19
+ # the window. Error rate, daily trend, severity
20
+ # distribution, cross-platform totals, the top-errors
21
+ # ranking, and the health card's total, critical count
22
+ # and velocity.
23
+ # GROUP figures -> groups FIRST SEEN in the window: the state of
24
+ # distinct errors. Unresolved count, resolution rate and
25
+ # resolution time. A group is what gets resolved; an
26
+ # event cannot be.
27
+ #
28
+ # An ErrorLog row is a GROUP and its occurred_at is FIRST-SEEN. Selecting
29
+ # groups by it and counting them made a group born last month that recurs
30
+ # today invisible here -- zero errors on this page (and on the Overview's
31
+ # platform health card) while the Overview's own total counted the event.
32
+ #
15
33
  # @example
16
34
  # comparison = PlatformComparison.new(days: 7)
17
35
  # comparison.error_rate_by_platform
@@ -29,7 +47,9 @@ module RailsErrorDashboard
29
47
 
30
48
  private
31
49
 
32
- # Base scope that filters by application when present
50
+ # Every group of the selected application, NOT cut by date. EventVolume
51
+ # must be handed this: a scope already filtered by first-seen drops the
52
+ # events of every older group that recurred inside the window.
33
53
  # @return [ActiveRecord::Relation]
34
54
  def base_scope
35
55
  scope = ErrorLog.all
@@ -37,38 +57,47 @@ module RailsErrorDashboard
37
57
  scope
38
58
  end
39
59
 
60
+ def platforms
61
+ @platforms ||= base_scope.distinct.pluck(:platform).compact
62
+ end
63
+
64
+ def volume
65
+ @volume ||= Queries::EventVolume.new(base_scope, @start_date)
66
+ end
67
+
68
+ # platform => { error_type => events in the window }.
69
+ #
70
+ # Severity, critical counts and cross-platform totals all fold from this
71
+ # one breakdown, so they cannot drift from each other, and each platform's
72
+ # parts sum to its error rate (the same primitive, exhaustively grouped).
73
+ def type_volume_by_platform
74
+ @type_volume_by_platform ||= platforms.index_with do |platform|
75
+ Queries::EventVolume.by_group_attribute(base_scope.where(platform: platform), :error_type, @start_date)
76
+ end
77
+ end
78
+
40
79
  public
41
80
 
42
- # Get error count by platform for the time period
43
- # @return [Hash] Platform name => error count
81
+ # Events per platform in the window.
82
+ # @return [Hash] Platform name => event count
44
83
  def error_rate_by_platform
45
- base_scope
46
- .where("occurred_at >= ?", @start_date)
47
- .group(:platform)
48
- .count
84
+ # select, not the raw breakdown: that is a Hash.new(0), and a platform
85
+ # with no events must read as absent (nil), not as a silent zero.
86
+ @error_rate_by_platform ||= volume.by_group_attribute(:platform).select { |_, count| count.positive? }
49
87
  end
50
88
 
51
- # Get severity distribution by platform
89
+ # Events per platform, by severity of their error type.
52
90
  # @return [Hash] Platform => { severity => count }
53
91
  def severity_distribution_by_platform
54
- platforms = base_scope.distinct.pluck(:platform).compact
55
-
56
- platforms.each_with_object({}) do |platform, result|
57
- errors = base_scope
58
- .where(platform: platform)
59
- .where("occurred_at >= ?", @start_date)
60
-
61
- # Calculate severity in Ruby since it's a method, not a column
62
- severity_counts = Hash.new(0)
63
- errors.each do |error|
64
- severity_counts[error.severity] += 1
92
+ type_volume_by_platform.transform_values do |counts_by_type|
93
+ counts_by_type.each_with_object(Hash.new(0)) do |(error_type, count), severities|
94
+ severities[Services::SeverityClassifier.classify(error_type)] += count
65
95
  end
66
-
67
- result[platform] = severity_counts
68
96
  end
69
97
  end
70
98
 
71
99
  # Get average resolution time by platform
100
+ # GROUP figure: a resolution belongs to a group, selected by first-seen.
72
101
  # @return [Hash] Platform => average hours to resolve
73
102
  def resolution_time_by_platform
74
103
  platforms = base_scope.distinct.pluck(:platform).compact
@@ -90,28 +119,38 @@ module RailsErrorDashboard
90
119
  end
91
120
  end
92
121
 
93
- # Get top 10 errors for each platform
122
+ # Top 10 groups per platform, ranked by their events in the window.
123
+ #
124
+ # :occurrence_count is the group's count WITHIN the window, not its
125
+ # lifetime total -- the key is kept so the view and callers are
126
+ # unchanged. The per-group breakdown holds one integer pair per group
127
+ # with events in the window; only the top ten rows are loaded.
94
128
  # @return [Hash] Platform => Array of error hashes
95
129
  def top_errors_by_platform
96
- platforms = base_scope.distinct.pluck(:platform).compact
97
-
98
130
  platforms.each_with_object({}) do |platform, result|
99
- result[platform] = base_scope
100
- .where(platform: platform)
101
- .where("occurred_at >= ?", @start_date)
102
- .select(:id, :error_type, :message, :occurrence_count, :occurred_at)
103
- .order(occurrence_count: :desc)
104
- .limit(10)
105
- .map do |error|
106
- {
107
- id: error.id,
108
- error_type: error.error_type,
109
- message: error.message&.truncate(100),
110
- severity: error.severity, # Calls the method
111
- occurrence_count: error.occurrence_count,
112
- occurred_at: error.occurred_at
113
- }
114
- end
131
+ events_by_group = Queries::EventVolume.by_group_attribute(
132
+ base_scope.where(platform: platform), :id, @start_date
133
+ )
134
+ top = events_by_group.select { |_, count| count.positive? }
135
+ .sort_by { |id, count| [ -count, id ] }
136
+ .first(10)
137
+ groups = ErrorLog.where(id: top.map(&:first))
138
+ .select(:id, :error_type, :message, :occurred_at)
139
+ .index_by(&:id)
140
+
141
+ result[platform] = top.filter_map do |id, count|
142
+ error = groups[id]
143
+ next unless error
144
+
145
+ {
146
+ id: error.id,
147
+ error_type: error.error_type,
148
+ message: error.message&.truncate(100),
149
+ severity: error.severity, # Calls the method
150
+ occurrence_count: count,
151
+ occurred_at: error.occurred_at
152
+ }
153
+ end
115
154
  end
116
155
  end
117
156
 
@@ -141,98 +180,70 @@ module RailsErrorDashboard
141
180
  end
142
181
  end
143
182
 
144
- # Find errors that occur across multiple platforms
183
+ # Error types with events on more than one platform in the window.
145
184
  # @return [Array<Hash>] Errors with their platforms
146
185
  def cross_platform_errors
147
- # Get error types that appear on 2+ platforms
148
- error_types_with_platforms = base_scope
149
- .where("occurred_at >= ?", @start_date)
150
- .group(:error_type, :platform)
151
- .select(:error_type, :platform)
152
- .having("COUNT(*) > 0")
153
- .pluck(:error_type, :platform)
154
-
155
- # Group by error_type to find those on multiple platforms
156
- errors_by_type = error_types_with_platforms.group_by { |error_type, _| error_type }
157
-
158
- errors_by_type
159
- .select { |_, platforms| platforms.map(&:last).uniq.count > 1 }
160
- .map do |error_type, platform_pairs|
161
- platforms = platform_pairs.map(&:last).uniq
162
- total_count = base_scope
163
- .where(error_type: error_type, platform: platforms)
164
- .where("occurred_at >= ?", @start_date)
165
- .sum(:occurrence_count)
186
+ by_type = Hash.new { |hash, error_type| hash[error_type] = {} }
187
+ type_volume_by_platform.each do |platform, counts_by_type|
188
+ counts_by_type.each do |error_type, count|
189
+ by_type[error_type][platform] = count if count.positive?
190
+ end
191
+ end
166
192
 
193
+ by_type
194
+ .select { |_, breakdown| breakdown.size > 1 }
195
+ .map do |error_type, breakdown|
167
196
  {
168
197
  error_type: error_type,
169
- platforms: platforms.sort,
170
- total_occurrences: total_count,
171
- platform_breakdown: platforms.each_with_object({}) do |platform, breakdown|
172
- breakdown[platform] = base_scope
173
- .where(error_type: error_type, platform: platform)
174
- .where("occurred_at >= ?", @start_date)
175
- .sum(:occurrence_count)
176
- end
198
+ platforms: breakdown.keys.sort,
199
+ total_occurrences: breakdown.values.sum,
200
+ platform_breakdown: breakdown
177
201
  }
178
202
  end
179
203
  .sort_by { |error| -error[:total_occurrences] }
180
204
  end
181
205
 
182
- # Get daily error trend by platform
206
+ # Events per day per platform, on the day each event HAPPENED.
207
+ #
208
+ # The zero-filled date range is preserved: the chart needs a point for
209
+ # every day in the window, not only the days that had errors.
183
210
  # @return [Hash] Platform => { date => count }
184
211
  def daily_trend_by_platform
185
- platforms = base_scope.distinct.pluck(:platform).compact
212
+ days = (@start_date.to_date..Date.current)
186
213
 
187
- platforms.each_with_object({}) do |platform, result|
188
- result[platform] = base_scope
189
- .where(platform: platform)
190
- .where("occurred_at >= ?", @start_date)
191
- .group_by_day(:occurred_at, range: @start_date..Time.current)
192
- .count
214
+ platforms.index_with do |platform|
215
+ counts = Queries::EventVolume.by_day(base_scope.where(platform: platform), @start_date)
216
+ days.to_h { |day| [ day, counts[day] || 0 ] }
193
217
  end
194
218
  end
195
219
 
196
220
  # Get platform health summary
197
221
  # @return [Hash] Platform => health metrics
198
222
  def platform_health_summary
199
- platforms = base_scope.distinct.pluck(:platform).compact
200
223
  error_rates = error_rate_by_platform
201
224
  stability_scores = platform_stability_scores
225
+ severities = severity_distribution_by_platform
226
+ midpoint = @start_date + (@days / 2.0).days
202
227
 
203
228
  platforms.each_with_object({}) do |platform, result|
204
- total_errors = error_rates[platform] || 0
205
-
206
- # Count critical errors by checking severity method
207
- critical_errors = base_scope
208
- .where(platform: platform)
209
- .where("occurred_at >= ?", @start_date)
210
- .select { |error| error.severity == :critical }
211
- .count
229
+ platform_scope = base_scope.where(platform: platform)
230
+ groups_in_window = platform_scope.where("occurred_at >= ?", @start_date)
212
231
 
213
- unresolved_errors = base_scope
214
- .where(platform: platform, resolved_at: nil)
215
- .where("occurred_at >= ?", @start_date)
216
- .count
217
-
218
- resolved_errors = base_scope
219
- .where(platform: platform)
220
- .where.not(resolved_at: nil)
221
- .where("occurred_at >= ?", @start_date)
222
- .count
223
-
224
- resolution_rate = total_errors.positive? ? ((resolved_errors.to_f / total_errors) * 100).round(1) : 0.0
225
-
226
- # Calculate error velocity (increasing or decreasing)
227
- first_half = base_scope
228
- .where(platform: platform)
229
- .where("occurred_at >= ? AND occurred_at < ?", @start_date, @start_date + (@days / 2.0).days)
230
- .count
231
-
232
- second_half = base_scope
233
- .where(platform: platform)
234
- .where("occurred_at >= ?", @start_date + (@days / 2.0).days)
235
- .count
232
+ # EVENT figures: part of the same volume as the error rate.
233
+ total_errors = error_rates[platform] || 0
234
+ critical_errors = severities.fetch(platform, {})[:critical] || 0
235
+
236
+ # GROUP figures. The resolution rate divides groups by groups; divided
237
+ # by the event total it would mix units and stop meaning anything.
238
+ unresolved_errors = groups_in_window.where(resolved_at: nil).count
239
+ resolved_errors = groups_in_window.where.not(resolved_at: nil).count
240
+ total_groups = unresolved_errors + resolved_errors
241
+ resolution_rate = total_groups.positive? ? ((resolved_errors.to_f / total_groups) * 100).round(1) : 0.0
242
+
243
+ # Error velocity: events in the second half of the window against the
244
+ # first, each counted where it happened.
245
+ first_half = Queries::EventVolume.in_window(platform_scope, @start_date, midpoint)
246
+ second_half = Queries::EventVolume.in_window(platform_scope, midpoint)
236
247
 
237
248
  velocity = first_half.positive? ? (((second_half - first_half).to_f / first_half) * 100).round(1) : 0.0
238
249
 
@@ -5,6 +5,9 @@ module RailsErrorDashboard
5
5
  # Query: Analyze recurring and persistent errors
6
6
  # Returns data about high-frequency errors, persistent issues, and cyclical patterns
7
7
  class RecurringIssues
8
+ # Minimum events IN THE WINDOW for a group to count as high frequency.
9
+ HIGH_FREQUENCY_THRESHOLD = 10
10
+
8
11
  def self.call(days = 30, application_id: nil)
9
12
  new(days, application_id: application_id).call
10
13
  end
@@ -25,37 +28,53 @@ module RailsErrorDashboard
25
28
 
26
29
  private
27
30
 
28
- def base_query
29
- scope = ErrorLog.where("occurred_at >= ?", @start_date)
31
+ # Every group of the application, NOT cut by date -- what EventVolume
32
+ # must be handed, or it never sees an older group's recurrences.
33
+ def app_scope
34
+ scope = ErrorLog.all
30
35
  scope = scope.where(application_id: @application_id) if @application_id.present?
31
36
  scope
32
37
  end
33
38
 
39
+ # Groups FIRST SEEN in the window, for the group lists below.
40
+ def base_query
41
+ app_scope.where("occurred_at >= ?", @start_date)
42
+ end
43
+
44
+ # EVENT figure: error types whose groups fired more than
45
+ # HIGH_FREQUENCY_THRESHOLD times in the window, by events in the window.
46
+ #
47
+ # Selecting groups by first-seen and summing their LIFETIME
48
+ # occurrence_count listed only errors born in the window -- a chronic
49
+ # error that fired all month was never "high frequency" once it was a
50
+ # month old. The threshold stays per group (not per type) so eleven
51
+ # one-off groups of one type do not qualify; it now counts this window.
34
52
  def high_frequency_errors
35
- # Errors with high occurrence count
36
- base_query
37
- .where("occurrence_count > ?", 10)
38
- .group(:error_type)
39
- .select("error_type,
40
- SUM(occurrence_count) as total_occurrences,
41
- MIN(first_seen_at) as first_occurrence,
42
- MAX(last_seen_at) as last_occurrence,
43
- COUNT(*) as unique_error_count")
44
- .order("total_occurrences DESC")
45
- .limit(10)
46
- .map do |error|
47
- first_seen = error.first_occurrence.is_a?(Time) ? error.first_occurrence : Time.parse(error.first_occurrence.to_s)
48
- last_seen = error.last_occurrence.is_a?(Time) ? error.last_occurrence : Time.parse(error.last_occurrence.to_s)
53
+ events_by_group = Queries::EventVolume.by_group_attribute(app_scope, :id, @start_date)
54
+ .select { |_, count| count > HIGH_FREQUENCY_THRESHOLD }
55
+ return [] if events_by_group.empty?
49
56
 
50
- {
51
- error_type: error.error_type,
52
- total_occurrences: error.total_occurrences,
53
- first_seen: first_seen,
54
- last_seen: last_seen,
55
- duration_days: ((last_seen - first_seen) / 1.day).round,
56
- still_active: last_seen > 24.hours.ago
57
- }
58
- end
57
+ groups = ErrorLog.where(id: events_by_group.keys)
58
+ .pluck(:id, :error_type, :first_seen_at, :last_seen_at, :occurred_at)
59
+
60
+ groups.group_by { |_, error_type, *| error_type }
61
+ .map do |error_type, rows|
62
+ # first_seen_at/last_seen_at are NULL on rows from before those
63
+ # columns; occurred_at is the only timestamp such a row has.
64
+ first_seen = rows.map { |_, _, first, _, occurred| first || occurred }.min
65
+ last_seen = rows.map { |_, _, _, last, occurred| last || occurred }.max
66
+
67
+ {
68
+ error_type: error_type,
69
+ total_occurrences: rows.sum { |id, *| events_by_group[id] },
70
+ first_seen: first_seen,
71
+ last_seen: last_seen,
72
+ duration_days: ((last_seen - first_seen) / 1.day).round,
73
+ still_active: last_seen > 24.hours.ago
74
+ }
75
+ end
76
+ .sort_by { |entry| [ -entry[:total_occurrences], entry[:error_type].to_s ] }
77
+ .first(10)
59
78
  end
60
79
 
61
80
  def persistent_errors
@@ -29,30 +29,48 @@ module RailsErrorDashboard
29
29
 
30
30
  private
31
31
 
32
- def base_scope
33
- scope = ErrorLog.where("occurred_at >= ?", @start_date)
34
- .where.not(user_id: nil)
32
+ # Every group of the selected application, NOT cut by date. EventVolume
33
+ # and the row samples need this: an error first seen before the window
34
+ # that recurs inside it is still this window's error.
35
+ def app_scope
36
+ scope = ErrorLog.all
35
37
  scope = scope.where(application_id: @application_id) if @application_id.present?
36
38
  scope
37
39
  end
38
40
 
41
+ # Groups FIRST SEEN in the window with a user -- the group-level fallback
42
+ # for errors whose events predate occurrence tracking (see
43
+ # distinct_users_by_type).
44
+ def base_scope
45
+ app_scope.where("occurred_at >= ?", @start_date).where.not(user_id: nil)
46
+ end
47
+
39
48
  def build_entries
40
49
  # Distinct users per error type, from OCCURRENCE rows: the group's
41
50
  # user_id is overwritten by each new occurrence, so counting it
42
51
  # distinct across groups reported five users hitting one error as one.
43
52
  user_counts = distinct_users_by_type
44
53
 
45
- # EVENTS per type, not groups. occurrence_count is exact and includes
46
- # storm-shed events, which create no occurrence row.
47
- occurrence_counts = base_scope
48
- .group(:error_type)
49
- .sum(:occurrence_count)
54
+ # EVENTS per type in the window, each counted where it happened --
55
+ # occurrence rows, storm-shed buckets and the untracked remainder.
56
+ # Summing the lifetime occurrence_count of groups born in the window
57
+ # gave an August error recurring today zero occurrences on a row that
58
+ # still listed its users.
59
+ occurrence_counts = Queries::EventVolume.by_group_attribute(app_scope, :error_type, @start_date)
50
60
 
51
61
  total_users = effective_total_users
52
62
 
53
63
  user_counts.map do |error_type, unique_users|
54
64
  occurrences = occurrence_counts[error_type] || 0
55
- sample = base_scope.where(error_type: error_type).order(occurred_at: :desc).first
65
+ # The row's message, severity and link: the most recently seen group
66
+ # of this type. Taken from app_scope for the same reason as the
67
+ # counts -- a first-seen scope has no sample for an older error, and
68
+ # the row rendered with no message and nothing to click.
69
+ # COALESCE, not a bare last_seen_at DESC: PostgreSQL sorts NULLs
70
+ # FIRST in descending order, so a row from before the column existed
71
+ # would outrank every live one there.
72
+ sample = app_scope.where(error_type: error_type).where.not(user_id: nil)
73
+ .order(Arel.sql("COALESCE(last_seen_at, occurred_at) DESC"), id: :desc).first
56
74
  impact_pct = total_users && total_users > 0 ? (unique_users.to_f / total_users * 100).round(1) : nil
57
75
 
58
76
  {
@@ -62,7 +80,8 @@ module RailsErrorDashboard
62
80
  total_occurrences: occurrences,
63
81
  impact_percentage: impact_pct,
64
82
  severity: sample&.severity,
65
- last_seen: sample&.occurred_at,
83
+ # "Last Seen" -- occurred_at is FIRST-seen and never moves.
84
+ last_seen: sample&.last_seen_at || sample&.occurred_at,
66
85
  id: sample&.id
67
86
  }
68
87
  end.sort_by { |e| -e[:unique_users] }
@@ -56,17 +56,39 @@ module RailsErrorDashboard
56
56
  I18nStore.translate(PERIODS[@period][:label_key], locale: @locale)
57
57
  end
58
58
 
59
- def base_scope
60
- scope = ErrorLog.where("occurred_at >= ?", @start_date)
59
+ # Two counting units, kept distinct on purpose (as in AnalyticsStats).
60
+ #
61
+ # EVENT figures -> Queries::EventVolume over app_scope: how much
62
+ # erroring happened in the period. Occurrences, the
63
+ # top-errors counts and the period comparison.
64
+ # GROUP figures -> base_scope, groups FIRST SEEN in the period: new,
65
+ # resolved, unresolved, critical/high, resolution rate
66
+ # and the critical-unresolved list.
67
+ #
68
+ # Summing the lifetime counts of groups born in the period reported an
69
+ # unresolved August error that fired all day today as zero occurrences,
70
+ # absent from Top Errors, with an empty comparison.
71
+
72
+ # Every group of the application, NOT cut by date -- what EventVolume
73
+ # must be handed, or it never sees an older group's recurrences.
74
+ def app_scope
75
+ scope = ErrorLog.all
61
76
  scope = scope.where(application_id: @application_id) if @application_id.present?
62
77
  scope
63
78
  end
64
79
 
80
+ def base_scope
81
+ app_scope.where("occurred_at >= ?", @start_date)
82
+ end
83
+
65
84
  def build_stats
66
85
  scope = base_scope
67
86
 
68
- new_errors = scope.where("occurrence_count <= 1").count
69
- total_occurrences = scope.sum(:occurrence_count)
87
+ # GROUP figure: errors first seen in the period, however often they
88
+ # fired. `occurrence_count <= 1` counted "seen once", so a brand-new
89
+ # error that fired twice was missing from the headline and the subject.
90
+ new_errors = scope.count
91
+ total_occurrences = Queries::EventVolume.in_window(app_scope, @start_date)
70
92
  resolved = scope.where(resolved: true).count
71
93
  unresolved = scope.where(resolved: false).count
72
94
  # Severity is computed from error_type via SeverityClassifier (not a DB column).
@@ -91,15 +113,21 @@ module RailsErrorDashboard
91
113
  { new_errors: 0, total_occurrences: 0, resolved: 0, unresolved: 0, critical_high: 0, resolution_rate: 0 }
92
114
  end
93
115
 
116
+ # Unresolved error types ranked by their events in the period. This used
117
+ # to count GROUPS per type (a type with five groups scored 5 however
118
+ # often each fired), and only groups born in the period.
94
119
  def top_errors
95
- base_scope
96
- .where(resolved: false)
97
- .group(:error_type)
98
- .order("count_all DESC")
99
- .limit(5)
100
- .count
120
+ unresolved = app_scope.where(resolved: false)
121
+
122
+ Queries::EventVolume.by_group_attribute(unresolved, :error_type, @start_date)
123
+ .select { |_, count| count.positive? }
124
+ .sort_by { |error_type, count| [ -count, error_type.to_s ] }
125
+ .first(5)
101
126
  .map do |error_type, count|
102
- sample = base_scope.where(error_type: error_type).order(occurred_at: :desc).first
127
+ # Most recently seen first; COALESCE because PostgreSQL sorts NULLs
128
+ # first in descending order.
129
+ sample = unresolved.where(error_type: error_type)
130
+ .order(Arel.sql("COALESCE(last_seen_at, occurred_at) DESC"), id: :desc).first
103
131
  {
104
132
  error_type: error_type,
105
133
  message: sample&.message.to_s.truncate(100),
@@ -137,10 +165,11 @@ module RailsErrorDashboard
137
165
  previous_start = (@days * 2).days.ago
138
166
  previous_end = @start_date
139
167
 
140
- current_count = base_scope.count
141
- previous_scope = ErrorLog.where("occurred_at >= ? AND occurred_at < ?", previous_start, previous_end)
142
- previous_scope = previous_scope.where(application_id: @application_id) if @application_id.present?
143
- previous_count = previous_scope.count
168
+ # Events in each period, counted where they happened. Counting groups
169
+ # born in each period measured how many NEW errors appeared, not
170
+ # whether the application is erroring more or less.
171
+ current_count = Queries::EventVolume.in_window(app_scope, @start_date)
172
+ previous_count = Queries::EventVolume.in_window(app_scope, previous_start, previous_end)
144
173
 
145
174
  delta = current_count - previous_count
146
175
  percentage = previous_count > 0 ? ((delta.to_f / previous_count) * 100).round(1) : nil
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.14.0"
2
+ VERSION = "0.14.1"
3
3
  end
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.14.0
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -599,7 +599,7 @@ metadata:
599
599
  funding_uri: https://github.com/sponsors/AnjanJ
600
600
  post_install_message: |
601
601
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
602
- RED (Rails Error Dashboard) v0.14.0
602
+ RED (Rails Error Dashboard) v0.14.1
603
603
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
604
604
 
605
605
  First install: