rails_error_dashboard 0.12.0 → 0.13.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.
- checksums.yaml +4 -4
- data/app/controllers/rails_error_dashboard/application_controller.rb +92 -11
- data/app/controllers/rails_error_dashboard/errors_controller.rb +111 -34
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +3 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +46 -1
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +8 -0
- data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +71 -0
- data/app/jobs/rails_error_dashboard/notification_burst_summary_job.rb +55 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +68 -1
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +8 -44
- data/app/models/rails_error_dashboard/error_baseline.rb +12 -9
- data/app/models/rails_error_dashboard/error_comment.rb +0 -5
- data/app/models/rails_error_dashboard/error_log.rb +9 -3
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/app/models/rails_error_dashboard/error_occurrence.rb +9 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +11 -3
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +18 -11
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +22 -8
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +15 -6
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +14 -8
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +6 -1
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/releases.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
- data/config/locales/de.yml +28 -0
- data/config/locales/en.yml +35 -0
- data/config/locales/es.yml +28 -0
- data/config/locales/fr.yml +28 -0
- data/config/locales/it.yml +28 -0
- data/config/locales/ja.yml +28 -0
- data/config/locales/pl.yml +28 -0
- data/config/locales/pt-BR.yml +28 -0
- data/config/locales/ru.yml +28 -0
- data/config/locales/uk.yml +28 -0
- data/config/locales/zh-CN.yml +28 -0
- data/db/migrate/20260917000001_add_last_notified_at_to_error_logs.rb +40 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -1
- data/lib/rails_error_dashboard/commands/assign_error.rb +12 -2
- data/lib/rails_error_dashboard/commands/backfill_environments.rb +2 -0
- data/lib/rails_error_dashboard/commands/backfill_resolved_at.rb +42 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -0
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +39 -7
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +3 -1
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +27 -4
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +5 -2
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +1 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +82 -23
- data/lib/rails_error_dashboard/commands/mute_error.rb +1 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -0
- data/lib/rails_error_dashboard/commands/scrub_invalid_encoding.rb +104 -0
- data/lib/rails_error_dashboard/commands/snooze_error.rb +34 -10
- data/lib/rails_error_dashboard/commands/unmute_error.rb +1 -0
- data/lib/rails_error_dashboard/commands/update_error_priority.rb +23 -2
- data/lib/rails_error_dashboard/commands/update_error_status.rb +33 -6
- data/lib/rails_error_dashboard/configuration.rb +19 -1
- data/lib/rails_error_dashboard/engine.rb +15 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +107 -26
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +55 -50
- data/lib/rails_error_dashboard/queries/error_correlation.rb +6 -3
- data/lib/rails_error_dashboard/queries/errors_list.rb +15 -2
- data/lib/rails_error_dashboard/queries/similar_errors.rb +1 -1
- data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +43 -18
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +3 -1
- data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +3 -1
- data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +13 -2
- data/lib/rails_error_dashboard/services/diagnostic_dump_generator.rb +5 -3
- data/lib/rails_error_dashboard/services/encoding_sanitizer.rb +80 -0
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +180 -32
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -3
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +13 -0
- data/lib/rails_error_dashboard/services/exception_filter.rb +55 -0
- data/lib/rails_error_dashboard/services/git_head_reader.rb +102 -0
- data/lib/rails_error_dashboard/services/notification_throttler.rb +172 -28
- data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +47 -1
- data/lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb +59 -3
- data/lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb +25 -2
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +32 -5
- data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +99 -23
- data/lib/rails_error_dashboard/services/url_safety.rb +40 -0
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +11 -2
- data/lib/rails_error_dashboard/value_objects/error_context.rb +3 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +31 -0
- data/lib/tasks/error_dashboard.rake +54 -4
- metadata +10 -2
|
@@ -25,6 +25,17 @@ module RailsErrorDashboard
|
|
|
25
25
|
RAISE_THREAD_KEY = :red_swallowed_raises
|
|
26
26
|
RESCUE_THREAD_KEY = :red_swallowed_rescues
|
|
27
27
|
FLUSH_THREAD_KEY = :red_swallowed_last_flush
|
|
28
|
+
# Monotonic time at which this thread's buffer stopped being empty. The
|
|
29
|
+
# flush is due one interval after THAT, whether or not anything else
|
|
30
|
+
# happens on the thread.
|
|
31
|
+
DEADLINE_THREAD_KEY = :red_swallowed_armed_at
|
|
32
|
+
|
|
33
|
+
# Where evicted counts go, so eviction bounds memory without losing the
|
|
34
|
+
# total. The flush command splits keys on "|" and "->", so these persist as
|
|
35
|
+
# ordinary rows under the class name "[overflow]".
|
|
36
|
+
OVERFLOW_LABEL = "[overflow]"
|
|
37
|
+
RAISE_OVERFLOW_KEY = "#{OVERFLOW_LABEL}|#{OVERFLOW_LABEL}".freeze
|
|
38
|
+
RESCUE_OVERFLOW_KEY = "#{OVERFLOW_LABEL}|#{OVERFLOW_LABEL}->#{OVERFLOW_LABEL}".freeze
|
|
28
39
|
RAISE_LOC_IVAR = :@_red_raise_loc
|
|
29
40
|
|
|
30
41
|
# Flow-control exceptions that are commonly raised/rescued in normal Rails operation.
|
|
@@ -95,7 +106,7 @@ module RailsErrorDashboard
|
|
|
95
106
|
end
|
|
96
107
|
|
|
97
108
|
# Force flush the current thread's counters (used by job and tests)
|
|
98
|
-
def flush!
|
|
109
|
+
def flush!(sync: false)
|
|
99
110
|
raises = Thread.current[RAISE_THREAD_KEY]
|
|
100
111
|
rescues = Thread.current[RESCUE_THREAD_KEY]
|
|
101
112
|
return if raises.nil? && rescues.nil?
|
|
@@ -107,14 +118,42 @@ module RailsErrorDashboard
|
|
|
107
118
|
raises&.clear
|
|
108
119
|
rescues&.clear
|
|
109
120
|
Thread.current[FLUSH_THREAD_KEY] = Time.now.to_f
|
|
121
|
+
Thread.current[DEADLINE_THREAD_KEY] = nil
|
|
110
122
|
|
|
111
|
-
dispatch_flush(raise_snapshot, rescue_snapshot)
|
|
123
|
+
dispatch_flush(raise_snapshot, rescue_snapshot, sync: sync)
|
|
112
124
|
rescue => e
|
|
113
125
|
RailsErrorDashboard::Logger.debug(
|
|
114
126
|
"[RailsErrorDashboard] SwallowedExceptionTracker.flush! failed: #{e.class} - #{e.message}"
|
|
115
127
|
)
|
|
116
128
|
end
|
|
117
129
|
|
|
130
|
+
# Drain this thread's buffer at the end of a unit of work (a request or a
|
|
131
|
+
# job) once it has been waiting for flush_interval.
|
|
132
|
+
#
|
|
133
|
+
# Without this the ONLY in-process drain was maybe_flush! inside the
|
|
134
|
+
# :rescue callback, so a buffer could only ever be flushed by a LATER
|
|
135
|
+
# rescue on the SAME thread: a swallowed exception that happened once
|
|
136
|
+
# stayed invisible until the process exited, and counts on a Puma thread
|
|
137
|
+
# that retired were lost. Same defect, same fix, as
|
|
138
|
+
# RackAttackTracker#flush_if_due!.
|
|
139
|
+
#
|
|
140
|
+
# Wired to Rails.application.executor.to_complete, which fires after the
|
|
141
|
+
# response body is closed, so this never delays a request. Deadline-gated:
|
|
142
|
+
# the usual cost is one thread-local read.
|
|
143
|
+
def flush_if_due!
|
|
144
|
+
return unless flush_due?
|
|
145
|
+
|
|
146
|
+
# sync: the response is already sent, and one upsert per interval is
|
|
147
|
+
# cheaper than enqueueing a job to do it.
|
|
148
|
+
flush!(sync: true)
|
|
149
|
+
nil
|
|
150
|
+
rescue => e
|
|
151
|
+
RailsErrorDashboard::Logger.debug(
|
|
152
|
+
"[RailsErrorDashboard] SwallowedExceptionTracker.flush_if_due! failed: #{e.class} - #{e.message}"
|
|
153
|
+
)
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
118
157
|
# Read current thread's counters (for testing/inspection)
|
|
119
158
|
def current_raises
|
|
120
159
|
Thread.current[RAISE_THREAD_KEY] || {}
|
|
@@ -129,6 +168,7 @@ module RailsErrorDashboard
|
|
|
129
168
|
Thread.current[RAISE_THREAD_KEY] = nil
|
|
130
169
|
Thread.current[RESCUE_THREAD_KEY] = nil
|
|
131
170
|
Thread.current[FLUSH_THREAD_KEY] = nil
|
|
171
|
+
Thread.current[DEADLINE_THREAD_KEY] = nil
|
|
132
172
|
end
|
|
133
173
|
|
|
134
174
|
private
|
|
@@ -152,11 +192,9 @@ module RailsErrorDashboard
|
|
|
152
192
|
class_name = exception.class.name || exception.class.to_s
|
|
153
193
|
key = "#{class_name}|#{location}"
|
|
154
194
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
# 5. LRU eviction if over capacity
|
|
159
|
-
evict_oldest!(raises) if raises.size > max_cache_size
|
|
195
|
+
# 4b/5. Count it, most-recent-last, evicting into the overflow bucket
|
|
196
|
+
increment!((Thread.current[RAISE_THREAD_KEY] ||= {}), key, RAISE_OVERFLOW_KEY)
|
|
197
|
+
arm_deadline!
|
|
160
198
|
end
|
|
161
199
|
|
|
162
200
|
# TracePoint(:rescue) callback
|
|
@@ -181,11 +219,9 @@ module RailsErrorDashboard
|
|
|
181
219
|
class_name = exception.class.name || exception.class.to_s
|
|
182
220
|
key = "#{class_name}|#{raise_loc}->#{rescue_loc}"
|
|
183
221
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# 5. LRU eviction if over capacity
|
|
188
|
-
evict_oldest!(rescues) if rescues.size > max_cache_size
|
|
222
|
+
# 4b/5. Count it, most-recent-last, evicting into the overflow bucket
|
|
223
|
+
increment!((Thread.current[RESCUE_THREAD_KEY] ||= {}), key, RESCUE_OVERFLOW_KEY)
|
|
224
|
+
arm_deadline!
|
|
189
225
|
|
|
190
226
|
# 6. Maybe flush
|
|
191
227
|
maybe_flush!
|
|
@@ -210,21 +246,60 @@ module RailsErrorDashboard
|
|
|
210
246
|
false
|
|
211
247
|
end
|
|
212
248
|
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
249
|
+
# Count one event. delete-and-reinsert moves the key to the END of the
|
|
250
|
+
# Hash, so insertion order IS recency order and "oldest" below means least
|
|
251
|
+
# recently UPDATED. A plain `hash[key] += 1` leaves a key where it was
|
|
252
|
+
# first inserted, which made the busiest key in the process the first one
|
|
253
|
+
# evicted.
|
|
254
|
+
def increment!(hash, key, overflow_key)
|
|
255
|
+
hash[key] = hash.delete(key).to_i + 1
|
|
256
|
+
|
|
257
|
+
# Loops because the overflow bucket takes a slot of its own once it
|
|
258
|
+
# exists. evict_oldest! returns false when only that bucket is left,
|
|
259
|
+
# which terminates the loop even with max_cache_size misconfigured to 0.
|
|
260
|
+
while hash.size > max_cache_size
|
|
261
|
+
break unless evict_oldest!(hash, overflow_key)
|
|
262
|
+
end
|
|
217
263
|
end
|
|
218
264
|
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
265
|
+
# Evict the least recently updated key, folding its count into the
|
|
266
|
+
# overflow bucket so the total is conserved. The overflow bucket itself is
|
|
267
|
+
# never the victim. Mirrors RackAttackTracker#evict_oldest!.
|
|
268
|
+
# @return [Boolean] false once only the overflow bucket remains
|
|
269
|
+
def evict_oldest!(hash, overflow_key)
|
|
270
|
+
oldest_key = hash.each_key.find { |k| k != overflow_key }
|
|
271
|
+
return false unless oldest_key
|
|
224
272
|
|
|
225
|
-
|
|
273
|
+
dropped = hash.delete(oldest_key).to_i
|
|
274
|
+
hash[overflow_key] = hash[overflow_key].to_i + dropped if dropped.positive?
|
|
275
|
+
true
|
|
276
|
+
end
|
|
226
277
|
|
|
227
|
-
|
|
278
|
+
# Start the flush clock when the buffer becomes non-empty; a later event
|
|
279
|
+
# never pushes it out. The old guard (`last_flush ||= now`) was first
|
|
280
|
+
# evaluated by the SECOND event on a thread, so a lone event had no
|
|
281
|
+
# deadline at all.
|
|
282
|
+
def arm_deadline!
|
|
283
|
+
Thread.current[DEADLINE_THREAD_KEY] ||= monotonic_now
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Monotonic: Time.now can jump backwards and would defer the flush.
|
|
287
|
+
def flush_due?
|
|
288
|
+
armed_at = Thread.current[DEADLINE_THREAD_KEY]
|
|
289
|
+
return false if armed_at.nil?
|
|
290
|
+
|
|
291
|
+
(monotonic_now - armed_at) >= RailsErrorDashboard.configuration.swallowed_exception_flush_interval.to_f
|
|
292
|
+
rescue => e
|
|
293
|
+
false
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def monotonic_now
|
|
297
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Cheap periodic flush check on the :rescue path
|
|
301
|
+
def maybe_flush!
|
|
302
|
+
flush! if flush_due?
|
|
228
303
|
end
|
|
229
304
|
|
|
230
305
|
# Dispatch flush asynchronously via background job (zero I/O in request path).
|
|
@@ -263,6 +338,7 @@ module RailsErrorDashboard
|
|
|
263
338
|
thread[RAISE_THREAD_KEY] = nil
|
|
264
339
|
thread[RESCUE_THREAD_KEY] = nil
|
|
265
340
|
thread[FLUSH_THREAD_KEY] = nil
|
|
341
|
+
thread[DEADLINE_THREAD_KEY] = nil
|
|
266
342
|
|
|
267
343
|
dispatch_flush(raise_snapshot, rescue_snapshot, sync: true)
|
|
268
344
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module RailsErrorDashboard
|
|
6
|
+
module Services
|
|
7
|
+
# Pure algorithm: decide whether a string is safe to use as a link target
|
|
8
|
+
#
|
|
9
|
+
# A URL that ends up in an `href` must be absolute http(s). Rails' `link_to`
|
|
10
|
+
# and ERB escaping stop attribute breakout, but neither rejects a scheme such
|
|
11
|
+
# as `javascript:` — that has to be an allowlist, and it has to be the same
|
|
12
|
+
# rule where the value is written and where it is rendered.
|
|
13
|
+
#
|
|
14
|
+
# The value is judged exactly as given (no stripping), so what passes here is
|
|
15
|
+
# byte-for-byte what the caller goes on to store or render.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# UrlSafety.http_url?("https://github.com/a/b/issues/1") # => true
|
|
19
|
+
# UrlSafety.http_url?("javascript:alert(1)") # => false
|
|
20
|
+
class UrlSafety
|
|
21
|
+
# Browsers strip TAB/CR/LF from inside a URL before resolving the scheme,
|
|
22
|
+
# so "java\tscript:" is live. No legitimate URL contains these or a space.
|
|
23
|
+
UNSAFE_CHARACTERS = /[[:cntrl:][:space:]]/
|
|
24
|
+
|
|
25
|
+
# @param value [String, nil]
|
|
26
|
+
# @return [Boolean] true only for an absolute http:// or https:// URL with a host
|
|
27
|
+
def self.http_url?(value)
|
|
28
|
+
return false unless value.is_a?(String)
|
|
29
|
+
return false if value.empty? || value.match?(UNSAFE_CHARACTERS)
|
|
30
|
+
|
|
31
|
+
uri = URI.parse(value)
|
|
32
|
+
# URI::HTTPS is a subclass of URI::HTTP
|
|
33
|
+
uri.is_a?(URI::HTTP) && uri.host.present?
|
|
34
|
+
rescue URI::InvalidURIError, ArgumentError, EncodingError
|
|
35
|
+
# ArgumentError/EncodingError: a string with invalid bytes cannot be matched
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -78,11 +78,20 @@ module RailsErrorDashboard
|
|
|
78
78
|
|
|
79
79
|
if existing
|
|
80
80
|
# Link this record to the existing issue instead of creating a new one
|
|
81
|
-
|
|
81
|
+
link = {
|
|
82
82
|
external_issue_url: existing.external_issue_url,
|
|
83
83
|
external_issue_number: existing.external_issue_number,
|
|
84
84
|
external_issue_provider: existing.external_issue_provider
|
|
85
|
-
|
|
85
|
+
}
|
|
86
|
+
# The repository is half the issue's identity. Without it
|
|
87
|
+
# IssueTrackerClient.for_error falls back to the CONFIGURED repo,
|
|
88
|
+
# and every later comment/close/reopen for this row would hit
|
|
89
|
+
# whatever issue has that number there. Same column guard as
|
|
90
|
+
# LinkExistingIssue.
|
|
91
|
+
if ErrorLog.column_names.include?("external_issue_repo")
|
|
92
|
+
link[:external_issue_repo] = existing.external_issue_repo
|
|
93
|
+
end
|
|
94
|
+
error_log.update_columns(link)
|
|
86
95
|
return false
|
|
87
96
|
end
|
|
88
97
|
|
|
@@ -125,7 +125,9 @@ module RailsErrorDashboard
|
|
|
125
125
|
# has :request_params as a JSON string but no :request object.
|
|
126
126
|
return @context[:request_params] if params.empty? && @context[:request_params].present?
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
# Params read off a live request or job object never passed through the
|
|
129
|
+
# context scrub LogError does, and to_json raises on an invalid byte.
|
|
130
|
+
Services::EncodingSanitizer.scrub_deep(params).to_json
|
|
129
131
|
end
|
|
130
132
|
|
|
131
133
|
def extract_user_agent
|
|
@@ -56,11 +56,14 @@ require "rails_error_dashboard/services/pearson_correlation"
|
|
|
56
56
|
require "rails_error_dashboard/services/statistical_classifier"
|
|
57
57
|
require "rails_error_dashboard/services/source_code_reader"
|
|
58
58
|
require "rails_error_dashboard/services/git_blame_reader"
|
|
59
|
+
require "rails_error_dashboard/services/git_head_reader"
|
|
59
60
|
require "rails_error_dashboard/services/github_link_generator"
|
|
60
61
|
require "rails_error_dashboard/services/cause_chain_extractor"
|
|
61
62
|
require "rails_error_dashboard/services/environment_snapshot"
|
|
62
63
|
require "rails_error_dashboard/services/system_health_snapshot"
|
|
64
|
+
require "rails_error_dashboard/services/encoding_sanitizer"
|
|
63
65
|
require "rails_error_dashboard/services/sensitive_data_filter"
|
|
66
|
+
require "rails_error_dashboard/services/url_safety"
|
|
64
67
|
require "rails_error_dashboard/services/notification_throttler"
|
|
65
68
|
require "rails_error_dashboard/services/breadcrumb_collector"
|
|
66
69
|
require "rails_error_dashboard/services/n_plus_one_detector"
|
|
@@ -132,6 +135,8 @@ require "rails_error_dashboard/commands/upsert_cascade_pattern"
|
|
|
132
135
|
require "rails_error_dashboard/commands/upsert_baseline"
|
|
133
136
|
require "rails_error_dashboard/commands/flush_swallowed_exceptions"
|
|
134
137
|
require "rails_error_dashboard/commands/flush_rack_attack_events"
|
|
138
|
+
require "rails_error_dashboard/commands/scrub_invalid_encoding"
|
|
139
|
+
require "rails_error_dashboard/commands/backfill_resolved_at"
|
|
135
140
|
require "rails_error_dashboard/queries/errors_list"
|
|
136
141
|
require "rails_error_dashboard/queries/dashboard_stats"
|
|
137
142
|
require "rails_error_dashboard/queries/analytics_stats"
|
|
@@ -177,6 +182,32 @@ module RailsErrorDashboard
|
|
|
177
182
|
end
|
|
178
183
|
end
|
|
179
184
|
|
|
185
|
+
# The commit this process is running, read from .git once and remembered.
|
|
186
|
+
#
|
|
187
|
+
# LogError stamps every error with a SHA. When neither config.git_sha nor a
|
|
188
|
+
# platform ENV variable supplies one it used to run `git rev-parse` in a
|
|
189
|
+
# subprocess PER CAPTURED ERROR. The answer cannot change while the process
|
|
190
|
+
# lives, so it is resolved once (the engine does it at boot, so normally
|
|
191
|
+
# not even the first capture pays for it) and nil is remembered too: an app
|
|
192
|
+
# with no repository must not go looking again for every error.
|
|
193
|
+
#
|
|
194
|
+
# @return [String, nil] short SHA
|
|
195
|
+
def self.detected_git_sha
|
|
196
|
+
return @detected_git_sha if defined?(@detected_git_sha)
|
|
197
|
+
|
|
198
|
+
@detected_git_sha = begin
|
|
199
|
+
Services::GitHeadReader.call(defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : nil)
|
|
200
|
+
rescue => e
|
|
201
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Could not detect git SHA: #{e.class}: #{e.message}")
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Forget the memo (specs).
|
|
207
|
+
def self.reset_detected_git_sha!
|
|
208
|
+
remove_instance_variable(:@detected_git_sha) if defined?(@detected_git_sha)
|
|
209
|
+
end
|
|
210
|
+
|
|
180
211
|
# Register a plugin
|
|
181
212
|
# @param plugin [Plugin] The plugin instance to register
|
|
182
213
|
# @return [Boolean] True if registered successfully, false otherwise
|
|
@@ -459,6 +459,8 @@ namespace :error_dashboard do
|
|
|
459
459
|
puts "\nDeleting errors..."
|
|
460
460
|
start_time = Time.current
|
|
461
461
|
deleted = scope.delete_all
|
|
462
|
+
# delete_all skips callbacks, and the stat cards are cached.
|
|
463
|
+
RailsErrorDashboard::Services::AnalyticsCacheManager.clear
|
|
462
464
|
elapsed = (Time.current - start_time).round(2)
|
|
463
465
|
|
|
464
466
|
puts "\n✓ Cleanup complete!"
|
|
@@ -492,7 +494,7 @@ namespace :error_dashboard do
|
|
|
492
494
|
application_id: app.id,
|
|
493
495
|
dump_data: dump.to_json,
|
|
494
496
|
captured_at: Time.current,
|
|
495
|
-
note: ENV["NOTE"]
|
|
497
|
+
note: RailsErrorDashboard::Services::EncodingSanitizer.scrub(ENV["NOTE"])
|
|
496
498
|
)
|
|
497
499
|
|
|
498
500
|
puts "\n" + JSON.pretty_generate(dump)
|
|
@@ -502,7 +504,7 @@ namespace :error_dashboard do
|
|
|
502
504
|
puts "\n" + "=" * 70 + "\n"
|
|
503
505
|
end
|
|
504
506
|
|
|
505
|
-
desc "Run retention cleanup (delete errors
|
|
507
|
+
desc "Run retention cleanup (delete errors not seen for retention_days)"
|
|
506
508
|
task retention_cleanup: :environment do
|
|
507
509
|
config = RailsErrorDashboard.configuration
|
|
508
510
|
|
|
@@ -517,14 +519,15 @@ namespace :error_dashboard do
|
|
|
517
519
|
end
|
|
518
520
|
|
|
519
521
|
cutoff = config.retention_days.days.ago
|
|
520
|
-
|
|
522
|
+
# The job's own selection, so the number shown is the number deleted.
|
|
523
|
+
count = RailsErrorDashboard::RetentionCleanupJob.expired_scope(cutoff).count
|
|
521
524
|
|
|
522
525
|
puts "\n Retention policy: #{config.retention_days} days"
|
|
523
526
|
puts " Cutoff date: #{cutoff.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
524
527
|
puts " Errors to delete: #{count}"
|
|
525
528
|
|
|
526
529
|
if count.zero?
|
|
527
|
-
puts "\n No errors
|
|
530
|
+
puts "\n No errors unseen for more than #{config.retention_days} days"
|
|
528
531
|
puts "\n" + "=" * 80 + "\n"
|
|
529
532
|
next
|
|
530
533
|
end
|
|
@@ -550,6 +553,53 @@ namespace :error_dashboard do
|
|
|
550
553
|
puts "\n" + "=" * 80 + "\n"
|
|
551
554
|
end
|
|
552
555
|
|
|
556
|
+
desc "Repair rows stored with invalid UTF-8 or NUL bytes (run once after upgrading on SQLite/MySQL)"
|
|
557
|
+
task scrub_invalid_encoding: :environment do
|
|
558
|
+
puts "Scanning error logs and occurrences for invalid bytes..."
|
|
559
|
+
result = RailsErrorDashboard::Commands::ScrubInvalidEncoding.call
|
|
560
|
+
|
|
561
|
+
puts " scanned: #{result[:scanned]}"
|
|
562
|
+
puts " repaired: #{result[:repaired]}"
|
|
563
|
+
puts " unreadable: #{result[:unreadable].size}"
|
|
564
|
+
result[:unreadable].each { |row| puts " - #{row}" }
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
desc "Backfill resolved_at for errors resolved through the status workflow (run once after upgrading)"
|
|
568
|
+
task backfill_resolved_at: :environment do
|
|
569
|
+
result = RailsErrorDashboard::Commands::BackfillResolvedAt.call
|
|
570
|
+
|
|
571
|
+
puts "Backfilled resolved_at on #{result[:updated]} resolved error(s)."
|
|
572
|
+
puts "MTTR now includes them, so expect the figure to rise to its true value." if result[:updated].positive?
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
desc "Replace raw session IDs stored on occurrences with keyed digests (idempotent)"
|
|
576
|
+
task digest_session_ids: :environment do
|
|
577
|
+
filter = RailsErrorDashboard::Services::SensitiveDataFilter
|
|
578
|
+
prefix = filter::SESSION_DIGEST_PREFIX
|
|
579
|
+
scope = RailsErrorDashboard::ErrorOccurrence
|
|
580
|
+
.where.not(session_id: [ nil, "" ])
|
|
581
|
+
.where("session_id NOT LIKE ? OR LENGTH(session_id) <> 35", "#{prefix}%")
|
|
582
|
+
|
|
583
|
+
digested = 0
|
|
584
|
+
failed = 0
|
|
585
|
+
scope.in_batches(of: 1000) do |batch|
|
|
586
|
+
batch.pluck(:id, :session_id).each do |id, raw|
|
|
587
|
+
digest = filter.digest_session_id(raw)
|
|
588
|
+
next if digest == raw # already a digest that the SQL prefilter let through
|
|
589
|
+
|
|
590
|
+
if digest
|
|
591
|
+
RailsErrorDashboard::ErrorOccurrence.where(id: id).update_all(session_id: digest)
|
|
592
|
+
digested += 1
|
|
593
|
+
else
|
|
594
|
+
failed += 1
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
puts "Session IDs digested: #{digested}"
|
|
600
|
+
puts "Could not be digested (left unchanged): #{failed}" if failed.positive?
|
|
601
|
+
end
|
|
602
|
+
|
|
553
603
|
desc "Send error digest email (PERIOD=daily|weekly, APP_ID=optional)"
|
|
554
604
|
task send_digest: :environment do
|
|
555
605
|
period = ENV.fetch("PERIOD", "daily")
|
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.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -283,9 +283,11 @@ files:
|
|
|
283
283
|
- app/jobs/rails_error_dashboard/baseline_calculation_job.rb
|
|
284
284
|
- app/jobs/rails_error_dashboard/close_linked_issue_job.rb
|
|
285
285
|
- app/jobs/rails_error_dashboard/concerns/localized_job.rb
|
|
286
|
+
- app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb
|
|
286
287
|
- app/jobs/rails_error_dashboard/create_issue_job.rb
|
|
287
288
|
- app/jobs/rails_error_dashboard/discord_error_notification_job.rb
|
|
288
289
|
- app/jobs/rails_error_dashboard/email_error_notification_job.rb
|
|
290
|
+
- app/jobs/rails_error_dashboard/notification_burst_summary_job.rb
|
|
289
291
|
- app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb
|
|
290
292
|
- app/jobs/rails_error_dashboard/rack_attack_flush_job.rb
|
|
291
293
|
- app/jobs/rails_error_dashboard/reopen_linked_issue_job.rb
|
|
@@ -417,6 +419,7 @@ files:
|
|
|
417
419
|
- db/migrate/20260915000002_add_issue_repo_identity_to_error_logs.rb
|
|
418
420
|
- db/migrate/20260915000003_add_context_provenance_to_error_logs.rb
|
|
419
421
|
- db/migrate/20260915000004_create_storm_flush_batches.rb
|
|
422
|
+
- db/migrate/20260917000001_add_last_notified_at_to_error_logs.rb
|
|
420
423
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
421
424
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
422
425
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -427,6 +430,7 @@ files:
|
|
|
427
430
|
- lib/rails_error_dashboard/commands/add_error_comment.rb
|
|
428
431
|
- lib/rails_error_dashboard/commands/assign_error.rb
|
|
429
432
|
- lib/rails_error_dashboard/commands/backfill_environments.rb
|
|
433
|
+
- lib/rails_error_dashboard/commands/backfill_resolved_at.rb
|
|
430
434
|
- lib/rails_error_dashboard/commands/batch_delete_errors.rb
|
|
431
435
|
- lib/rails_error_dashboard/commands/batch_mute_errors.rb
|
|
432
436
|
- lib/rails_error_dashboard/commands/batch_resolve_errors.rb
|
|
@@ -443,6 +447,7 @@ files:
|
|
|
443
447
|
- lib/rails_error_dashboard/commands/log_error.rb
|
|
444
448
|
- lib/rails_error_dashboard/commands/mute_error.rb
|
|
445
449
|
- lib/rails_error_dashboard/commands/resolve_error.rb
|
|
450
|
+
- lib/rails_error_dashboard/commands/scrub_invalid_encoding.rb
|
|
446
451
|
- lib/rails_error_dashboard/commands/snooze_error.rb
|
|
447
452
|
- lib/rails_error_dashboard/commands/unassign_error.rb
|
|
448
453
|
- lib/rails_error_dashboard/commands/unmute_error.rb
|
|
@@ -517,6 +522,7 @@ files:
|
|
|
517
522
|
- lib/rails_error_dashboard/services/diagnostic_dump_generator.rb
|
|
518
523
|
- lib/rails_error_dashboard/services/digest_builder.rb
|
|
519
524
|
- lib/rails_error_dashboard/services/discord_payload_builder.rb
|
|
525
|
+
- lib/rails_error_dashboard/services/encoding_sanitizer.rb
|
|
520
526
|
- lib/rails_error_dashboard/services/environment_snapshot.rb
|
|
521
527
|
- lib/rails_error_dashboard/services/error_broadcaster.rb
|
|
522
528
|
- lib/rails_error_dashboard/services/error_hash_generator.rb
|
|
@@ -524,6 +530,7 @@ files:
|
|
|
524
530
|
- lib/rails_error_dashboard/services/error_notification_dispatcher.rb
|
|
525
531
|
- lib/rails_error_dashboard/services/exception_filter.rb
|
|
526
532
|
- lib/rails_error_dashboard/services/git_blame_reader.rb
|
|
533
|
+
- lib/rails_error_dashboard/services/git_head_reader.rb
|
|
527
534
|
- lib/rails_error_dashboard/services/github_issue_client.rb
|
|
528
535
|
- lib/rails_error_dashboard/services/github_link_generator.rb
|
|
529
536
|
- lib/rails_error_dashboard/services/gitlab_issue_client.rb
|
|
@@ -558,6 +565,7 @@ files:
|
|
|
558
565
|
- lib/rails_error_dashboard/services/storm_protection/gate.rb
|
|
559
566
|
- lib/rails_error_dashboard/services/swallowed_exception_tracker.rb
|
|
560
567
|
- lib/rails_error_dashboard/services/system_health_snapshot.rb
|
|
568
|
+
- lib/rails_error_dashboard/services/url_safety.rb
|
|
561
569
|
- lib/rails_error_dashboard/services/variable_serializer.rb
|
|
562
570
|
- lib/rails_error_dashboard/services/webhook_payload_builder.rb
|
|
563
571
|
- lib/rails_error_dashboard/subscribers/action_cable_subscriber.rb
|
|
@@ -585,7 +593,7 @@ metadata:
|
|
|
585
593
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
586
594
|
post_install_message: |
|
|
587
595
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
588
|
-
RED (Rails Error Dashboard) v0.
|
|
596
|
+
RED (Rails Error Dashboard) v0.13.0
|
|
589
597
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
590
598
|
|
|
591
599
|
First install:
|