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 +4 -4
- data/lib/rails_error_dashboard/queries/error_correlation.rb +36 -27
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +119 -108
- data/lib/rails_error_dashboard/queries/recurring_issues.rb +44 -25
- data/lib/rails_error_dashboard/queries/user_impact_summary.rb +29 -10
- data/lib/rails_error_dashboard/services/digest_builder.rb +44 -15
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8dc068309c56704b429f7798ce4c3c83b628853179ebc2695c50fa684d2fae9
|
|
4
|
+
data.tar.gz: 64e3a06e8b60ce8f4c3fe649ca750f228a16e64405b7e239b8440f53e74e536d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
228
|
-
#
|
|
229
|
-
# the one panel
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
previous_errors =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
#
|
|
277
|
-
other_platforms =
|
|
278
|
-
.
|
|
279
|
-
.
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
43
|
-
# @return [Hash] Platform name =>
|
|
81
|
+
# Events per platform in the window.
|
|
82
|
+
# @return [Hash] Platform name => event count
|
|
44
83
|
def error_rate_by_platform
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
100
|
-
.where(platform: platform)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
170
|
-
total_occurrences:
|
|
171
|
-
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
|
-
#
|
|
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
|
-
|
|
212
|
+
days = (@start_date.to_date..Date.current)
|
|
186
213
|
|
|
187
|
-
platforms.
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,
|
|
46
|
-
# storm-shed
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
.
|
|
100
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
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.
|
|
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.
|
|
602
|
+
RED (Rails Error Dashboard) v0.14.1
|
|
603
603
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
604
604
|
|
|
605
605
|
First install:
|