rails_error_dashboard 0.13.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/app/jobs/rails_error_dashboard/async_error_logging_job.rb +10 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +54 -0
- data/app/jobs/rails_error_dashboard/storm_flush_job.rb +7 -4
- data/app/models/rails_error_dashboard/error_log.rb +10 -0
- data/app/models/rails_error_dashboard/event_count.rb +132 -0
- data/app/models/rails_error_dashboard/event_timing_gap.rb +55 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +47 -2
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +2 -0
- data/app/views/rails_error_dashboard/errors/overview.html.erb +12 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +3 -3
- data/config/locales/de.yml +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/es.yml +2 -0
- data/config/locales/fr.yml +2 -0
- data/config/locales/it.yml +2 -0
- data/config/locales/ja.yml +2 -0
- data/config/locales/pl.yml +2 -0
- data/config/locales/pt-BR.yml +2 -0
- data/config/locales/ru.yml +2 -0
- data/config/locales/uk.yml +2 -0
- data/config/locales/zh-CN.yml +2 -0
- data/db/migrate/20260919000001_create_event_counts.rb +71 -0
- data/db/migrate/20260920000001_add_buckets_incomplete_to_storm_events.rb +25 -0
- data/db/migrate/20260920000002_create_event_timing_gaps.rb +55 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +70 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +225 -8
- data/lib/rails_error_dashboard/commands/log_error.rb +213 -21
- data/lib/rails_error_dashboard/configuration.rb +20 -0
- data/lib/rails_error_dashboard/engine.rb +13 -0
- data/lib/rails_error_dashboard/manual_error_reporter.rb +16 -5
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +85 -27
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +167 -30
- data/lib/rails_error_dashboard/queries/error_correlation.rb +36 -27
- data/lib/rails_error_dashboard/queries/event_volume.rb +503 -0
- 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/breadcrumb_collector.rb +23 -0
- data/lib/rails_error_dashboard/services/digest_builder.rb +44 -15
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +64 -6
- data/lib/rails_error_dashboard/services/variable_serializer.rb +125 -10
- data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +111 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +37 -2
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- metadata +8 -2
|
@@ -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] }
|
|
@@ -77,6 +77,29 @@ module RailsErrorDashboard
|
|
|
77
77
|
nil
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Open a buffer ONLY if this thread has none, and say whether we opened
|
|
81
|
+
# it. The caller passes that answer back to clear_buffer_if_owned, so a
|
|
82
|
+
# job performed inline inside a request adds its crumbs to the request's
|
|
83
|
+
# trail and does not tear it down on the way out.
|
|
84
|
+
#
|
|
85
|
+
# Unconditional init/clear here would erase a surrounding request's
|
|
86
|
+
# buffer on every perform_now, the test adapter and the :inline queue.
|
|
87
|
+
# @return [Boolean] true when this caller opened the buffer
|
|
88
|
+
def self.init_buffer_unless_present
|
|
89
|
+
return false if Thread.current[THREAD_KEY]
|
|
90
|
+
|
|
91
|
+
init_buffer
|
|
92
|
+
true
|
|
93
|
+
rescue => e
|
|
94
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] BreadcrumbCollector.init_buffer_unless_present failed: #{e.message}")
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Tear down only what this caller opened (see init_buffer_unless_present).
|
|
99
|
+
def self.clear_buffer_if_owned(owned)
|
|
100
|
+
clear_buffer if owned
|
|
101
|
+
end
|
|
102
|
+
|
|
80
103
|
# Clear the ring buffer (end of request — MUST be called in ensure block)
|
|
81
104
|
def self.clear_buffer
|
|
82
105
|
Thread.current[THREAD_KEY] = nil
|
|
@@ -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
|