solid_errors 0.8.0 → 0.9.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/README.md +71 -6
- data/app/controllers/solid_errors/errors_controller.rb +5 -2
- data/app/mailers/solid_errors/error_mailer.rb +4 -2
- data/app/models/solid_errors/occurrence.rb +2 -1
- data/app/views/solid_errors/error_mailer/error_occurred.html.erb +0 -1
- data/lib/solid_errors/sanitizer.rb +13 -0
- data/lib/solid_errors/subscriber.rb +21 -1
- data/lib/solid_errors/version.rb +1 -1
- data/lib/solid_errors.rb +37 -6
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 389a4c6c9c7910c471dfc93caf7003790e1afa6c0a6781f010d6453b99731ec2
|
|
4
|
+
data.tar.gz: 0ad0508538e6ff2f523a865e4230ff371400a56b8e897b6078c47570dd71eccc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c91d2c051098cd1f3530592f76f85b9dffe8aa264f693950673f1ac8110a09f1c0b6afa86ec7eb3b071d52825899cf71cf17e75ad01f394639446df4fbf21fbd
|
|
7
|
+
data.tar.gz: 7947a0212c04437d205c822da7a0a96fccba64907d5ad3a062afe6919934767d4d533da0f977e6d48218b7c7f6bd9d992a10cf5d157c2a661154edb0921488bc
|
data/README.md
CHANGED
|
@@ -79,6 +79,9 @@ production:
|
|
|
79
79
|
|
|
80
80
|
Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
|
|
81
81
|
|
|
82
|
+
> [!WARNING]
|
|
83
|
+
> Use `db:prepare`, not `db:create` followed by `db:migrate`. Solid Errors ships a schema rather than migrations, so `db:migrate` leaves the errors database empty and then dumps that empty schema over `db/errors_schema.rb`. If that has happened, copy the schema back from [the install template](lib/generators/solid_errors/install/templates/db/errors_schema.rb) and run `db:prepare` again.
|
|
84
|
+
|
|
82
85
|
Then mount the engine in your `config/routes.rb` file:
|
|
83
86
|
```ruby
|
|
84
87
|
authenticate :user, -> (user) { user.admin? } do
|
|
@@ -151,6 +154,17 @@ Rails.error.handle(context: { user_id: user.id }, severity: :info) do
|
|
|
151
154
|
end
|
|
152
155
|
```
|
|
153
156
|
|
|
157
|
+
|
|
158
|
+
### What each occurrence records
|
|
159
|
+
|
|
160
|
+
Every occurrence stores the backtrace and the error's context. Rails adds the current request or job to that context, and Solid Errors records it as:
|
|
161
|
+
|
|
162
|
+
* **Requests:** the controller action (`UsersController#show`) and its params, with your `config.filter_parameters` applied.
|
|
163
|
+
* **Background jobs:** the job class, its id and queue, and its arguments, also filtered.
|
|
164
|
+
|
|
165
|
+
Any Active Record object in the context is stored as its [GlobalID](https://github.com/rails/globalid) (`gid://my-app/User/1`), so you can tell which record was involved. Add your own context with `Rails.error.set_context(...)` or the `context:` option shown above.
|
|
166
|
+
|
|
167
|
+
Errors raised in background jobs reach Solid Errors when your job backend runs jobs inside the Rails executor, as Solid Queue, Sidekiq and GoodJob do. You don't need any extra setup for this.
|
|
154
168
|
### Configuration
|
|
155
169
|
|
|
156
170
|
You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently, the following configuration options are available:
|
|
@@ -164,6 +178,7 @@ You can configure Solid Errors via the Rails configuration object, under the `so
|
|
|
164
178
|
* `email_to` - The email address(es) to send a notification to. See [Email notifications](#email-notifications) for more information.
|
|
165
179
|
* `email_every_occurrence` - Whether to email every occurrence (the default), or only new errors and resolved errors that occur again. See [Email notifications](#email-notifications) for more information.
|
|
166
180
|
* `email_subject_prefix` - Prefix added to the subject line for email notifications. See [Email notifications](#email-notifications) for more information.
|
|
181
|
+
* `email_subject` - A proc that builds the subject line for email notifications. See [Email notifications](#email-notifications) for more information.
|
|
167
182
|
* `base_controller_class` - Specify a different controller as the base class for the Solid Errors controller. See [Authentication](#authentication) for more information.
|
|
168
183
|
* `destroy_after` - If set, Solid Errors will periodically destroy resolved records that are older than the value specified. See [Automatically destroying old records](#automatically-destroying-old-records) for more information.
|
|
169
184
|
* `ignored_errors` - Error classes, or their names, to never record, in addition to the built-in list in `SolidErrors::Subscriber::IGNORED_ERRORS`. For example, `config.solid_errors.ignored_errors = ["ActiveStorage::FileNotFoundError"]`.
|
|
@@ -230,6 +245,10 @@ config.solid_errors.enabled = false
|
|
|
230
245
|
|
|
231
246
|
When disabled, Solid Errors will not subscribe to the Rails error reporter and no errors will be recorded.
|
|
232
247
|
|
|
248
|
+
Disable it in every environment that loads the gem but has no errors database, such as development or test. Otherwise Solid Errors records errors to the primary database, fails with `Could not find table 'solid_errors'`, and in development and test that failure can hide the error you were looking for.
|
|
249
|
+
|
|
250
|
+
Configuration belongs in `config/environments/<environment>.rb`, not in `config/<environment>.rb`, which Rails does not load.
|
|
251
|
+
|
|
233
252
|
#### Authentication
|
|
234
253
|
|
|
235
254
|
Solid Errors does not restrict access out of the box. You must secure the dashboard yourself. However, it does provide basic HTTP authentication that can be used with basic authentication or Devise. All you need to do is setup a username and password.
|
|
@@ -272,14 +291,14 @@ Solid Errors _can_ send email notifications whenever an error occurs, if your ap
|
|
|
272
291
|
|
|
273
292
|
There are two ways to configure email notifications. First, you can use environment variables:
|
|
274
293
|
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
294
|
+
```sh
|
|
295
|
+
SOLIDERRORS_SEND_EMAILS=true # defaults to false
|
|
296
|
+
SOLIDERRORS_EMAIL_FROM=errors@myapp.com # defaults to solid_errors@noreply.com
|
|
297
|
+
SOLIDERRORS_EMAIL_TO=devs@myapp.com # no default, must be set
|
|
298
|
+
SOLIDERRORS_EMAIL_SUBJECT_PREFIX="[Application name][Environment]" # no default, optional
|
|
280
299
|
```
|
|
281
300
|
|
|
282
|
-
Second, you can set the values via the configuration object:
|
|
301
|
+
Second, you can set the values via the configuration object, which takes precedence over the environment variables:
|
|
283
302
|
|
|
284
303
|
```ruby
|
|
285
304
|
# Set authentication credentials and optional subject prefix for Solid Errors
|
|
@@ -297,6 +316,32 @@ To be emailed only when an error is new, or when a resolved error occurs again,
|
|
|
297
316
|
config.solid_errors.email_every_occurrence = false # defaults to true
|
|
298
317
|
```
|
|
299
318
|
|
|
319
|
+
The subject names the error, and says when a resolved error has occurred again (`🔥 TimeoutError (reopened)`). To build it yourself, set `email_subject` to a proc. It is called with the error, the occurrence, and `reason`: `:created` or `:reopened` when the occurrence opened its error, `nil` for any other. `email_subject_prefix` is still added in front of what it returns.
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
config.solid_errors.email_subject = ->(error:, occurrence:, reason:) {
|
|
323
|
+
# Counting up to this occurrence, rather than all of them, gives the same answer however late the email is sent
|
|
324
|
+
count = error.occurrences.where(id: ..occurrence.id).count
|
|
325
|
+
subject = "#{error.severity_emoji} #{error.exception_class}"
|
|
326
|
+
|
|
327
|
+
case reason
|
|
328
|
+
when :reopened then "🔄 REOPENED: #{subject}"
|
|
329
|
+
when :resolved then "✅ RESOLVED: #{error.exception_class}"
|
|
330
|
+
else count.in?([10, 100, 1_000]) ? "#{subject} (#{count.ordinalize} occurrence)" : subject
|
|
331
|
+
end
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The error email can also be sent for events of your own, such as an error being resolved or occurring unusually often. Whatever reason you pass reaches `email_subject`, as `:resolved` does in the example above:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
# config/initializers/solid_errors.rb
|
|
339
|
+
ActiveSupport::Notifications.subscribe("error_resolved.solid_errors") do |event|
|
|
340
|
+
occurrence = event.payload[:error].occurrences.last
|
|
341
|
+
SolidErrors::ErrorMailer.error_occurred(occurrence, :resolved).deliver_later if occurrence && SolidErrors.send_emails?
|
|
342
|
+
end
|
|
343
|
+
```
|
|
344
|
+
|
|
300
345
|
#### Lifecycle events
|
|
301
346
|
|
|
302
347
|
Solid Errors publishes [Active Support notifications](https://guides.rubyonrails.org/active_support_instrumentation.html) once each change is committed, so you can send errors to Slack, a webhook or anywhere else:
|
|
@@ -330,6 +375,21 @@ config.solid_errors.destroy_after = 30.days
|
|
|
330
375
|
config.solid_errors.destroy_after = 6.months
|
|
331
376
|
```
|
|
332
377
|
|
|
378
|
+
#### Checking the schema after an upgrade
|
|
379
|
+
|
|
380
|
+
When a release changes the schema, [UPGRADE.md](UPGRADE.md) describes the migration. To have your test suite fail until you've applied it, assert that the errors database has everything the installed version uses:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# test/solid_errors_test.rb
|
|
384
|
+
require "test_helper"
|
|
385
|
+
|
|
386
|
+
class SolidErrorsTest < ActiveSupport::TestCase
|
|
387
|
+
test "the errors database is up to date" do
|
|
388
|
+
assert SolidErrors.migrated?
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
333
393
|
### Examples
|
|
334
394
|
|
|
335
395
|
There are only two screens in the dashboard.
|
|
@@ -412,6 +472,11 @@ You can always take control of the views by creating your own views and/or parti
|
|
|
412
472
|
</html>
|
|
413
473
|
```
|
|
414
474
|
|
|
475
|
+
|
|
476
|
+
### Locales
|
|
477
|
+
|
|
478
|
+
The dashboard is written in English and renders in English whenever your app allows the `:en` locale. If `config.i18n.available_locales` leaves out `:en`, the dashboard renders in your app's locale instead, and you will need to provide the `datetime.distance_in_words.short` translations from [`config/locales/en.yml`](config/locales/en.yml) in that locale.
|
|
479
|
+
|
|
415
480
|
## Sponsors
|
|
416
481
|
|
|
417
482
|
<p align="center">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module SolidErrors
|
|
2
2
|
class ErrorsController < ApplicationController
|
|
3
|
-
around_action :
|
|
3
|
+
around_action :use_english_when_available
|
|
4
4
|
|
|
5
5
|
before_action :set_error, only: %i[show update destroy]
|
|
6
6
|
|
|
@@ -53,7 +53,10 @@ module SolidErrors
|
|
|
53
53
|
@error = Error.find(params[:id])
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# The dashboard's strings ship in English only; apps that don't allow :en keep their own locale
|
|
57
|
+
def use_english_when_available(&action)
|
|
58
|
+
return action.call unless I18n.locale_available?(:en)
|
|
59
|
+
|
|
57
60
|
I18n.with_locale(:en, &action)
|
|
58
61
|
end
|
|
59
62
|
|
|
@@ -12,13 +12,15 @@ module SolidErrors
|
|
|
12
12
|
helper_method :occurrence_url
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# reason is passed on to SolidErrors.email_subject: :created or :reopened when the occurrence
|
|
16
|
+
# opened its error, or whatever an app sending this email for its own events passes
|
|
17
|
+
def error_occurred(occurrence, reason = nil)
|
|
16
18
|
unless defined?(ActionMailer::Base)
|
|
17
19
|
raise "ActionMailer is not available. Make sure that you require \"action_mailer/railtie\" in application.rb"
|
|
18
20
|
end
|
|
19
21
|
@occurrence = occurrence
|
|
20
22
|
@error = occurrence.error
|
|
21
|
-
subject =
|
|
23
|
+
subject = SolidErrors.email_subject.call(error: @error, occurrence: @occurrence, reason: reason&.to_sym)
|
|
22
24
|
if SolidErrors.email_subject_prefix.present?
|
|
23
25
|
subject = [SolidErrors.email_subject_prefix, subject].join(" ").squish!
|
|
24
26
|
end
|
|
@@ -35,7 +35,8 @@ module SolidErrors
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def send_email
|
|
38
|
-
|
|
38
|
+
# Passed along rather than read off the occurrence, which the mailer job loads afresh without it
|
|
39
|
+
ErrorMailer.error_occurred(self, opens_error).deliver_later
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def clear_resolved_errors
|
|
@@ -49,6 +49,10 @@ module SolidErrors
|
|
|
49
49
|
when String
|
|
50
50
|
sanitize_string(data)
|
|
51
51
|
else # all other objects
|
|
52
|
+
if (global_id = global_id(data))
|
|
53
|
+
return global_id
|
|
54
|
+
end
|
|
55
|
+
|
|
52
56
|
klass = data.class
|
|
53
57
|
|
|
54
58
|
begin
|
|
@@ -67,6 +71,15 @@ module SolidErrors
|
|
|
67
71
|
|
|
68
72
|
attr_reader :max_depth
|
|
69
73
|
|
|
74
|
+
# A saved record is identified by its GlobalID (gid://app/User/1) instead of just its class
|
|
75
|
+
def global_id(object)
|
|
76
|
+
return unless object.respond_to?(:to_global_id)
|
|
77
|
+
|
|
78
|
+
object.to_global_id.to_s
|
|
79
|
+
rescue
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
70
83
|
def basic_object?(object)
|
|
71
84
|
object.respond_to?(:to_s)
|
|
72
85
|
false
|
|
@@ -42,11 +42,31 @@ module SolidErrors
|
|
|
42
42
|
SolidErrors::Occurrence.create(
|
|
43
43
|
error_id: record.id,
|
|
44
44
|
backtrace: error.backtrace&.join("\n"),
|
|
45
|
-
context: s(context),
|
|
45
|
+
context: s(readable_context(context)),
|
|
46
46
|
opens_error: opens_error
|
|
47
47
|
)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Rails puts the current controller or job in the context; keep what identifies the work, not the object
|
|
51
|
+
def readable_context(context)
|
|
52
|
+
context = context.dup
|
|
53
|
+
if defined?(ActionController::Metal) && (controller = context[:controller]).is_a?(ActionController::Metal)
|
|
54
|
+
context[:controller] = "#{controller.class.name}##{controller.action_name}"
|
|
55
|
+
context[:params] ||= controller.request.filtered_parameters.except("controller", "action")
|
|
56
|
+
end
|
|
57
|
+
if defined?(ActiveJob::Base) && (job = context[:job]).is_a?(ActiveJob::Base)
|
|
58
|
+
context[:job] = job.class.name
|
|
59
|
+
context[:job_id] ||= job.job_id
|
|
60
|
+
context[:queue] ||= job.queue_name
|
|
61
|
+
context[:arguments] ||= parameter_filter.filter(arguments: s(job.arguments))[:arguments]
|
|
62
|
+
end
|
|
63
|
+
context
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parameter_filter
|
|
67
|
+
ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
|
68
|
+
end
|
|
69
|
+
|
|
50
70
|
def s(data)
|
|
51
71
|
Sanitizer.sanitize(data)
|
|
52
72
|
end
|
data/lib/solid_errors/version.rb
CHANGED
data/lib/solid_errors.rb
CHANGED
|
@@ -11,11 +11,14 @@ module SolidErrors
|
|
|
11
11
|
mattr_accessor :base_controller_class, default: "::ActionController::Base"
|
|
12
12
|
mattr_writer :username
|
|
13
13
|
mattr_writer :password
|
|
14
|
-
|
|
15
|
-
mattr_accessor :email_from, default: "solid_errors@noreply.com"
|
|
16
|
-
mattr_accessor :email_to
|
|
17
|
-
mattr_accessor :email_subject_prefix
|
|
14
|
+
mattr_writer :send_emails, :email_from, :email_to, :email_subject_prefix, default: nil
|
|
18
15
|
mattr_accessor :email_every_occurrence, default: true
|
|
16
|
+
# Builds the error email's subject, before email_subject_prefix. reason is :created or :reopened when
|
|
17
|
+
# the occurrence opened its error, nil for any other occurrence, or the app's own reason when it sends the email itself
|
|
18
|
+
mattr_accessor :email_subject, default: ->(error:, occurrence:, reason:) {
|
|
19
|
+
subject = "#{error.severity_emoji} #{error.exception_class}"
|
|
20
|
+
(reason == :reopened) ? "#{subject} (reopened)" : subject
|
|
21
|
+
}
|
|
19
22
|
mattr_accessor :destroy_after
|
|
20
23
|
mattr_accessor :ignored_errors, default: []
|
|
21
24
|
mattr_writer :url_helper_name, :url_options
|
|
@@ -33,6 +36,34 @@ module SolidErrors
|
|
|
33
36
|
@password ||= ENV["SOLIDERRORS_PASSWORD"] || @@password
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
# Each email setting falls back to its SOLIDERRORS_* environment variable when not configured
|
|
40
|
+
def send_emails
|
|
41
|
+
@@send_emails.nil? ? ActiveModel::Type::Boolean.new.cast(ENV["SOLIDERRORS_SEND_EMAILS"]) || false : @@send_emails
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def email_from
|
|
45
|
+
@@email_from || ENV["SOLIDERRORS_EMAIL_FROM"] || "solid_errors@noreply.com"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def email_to
|
|
49
|
+
@@email_to || ENV["SOLIDERRORS_EMAIL_TO"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def email_subject_prefix
|
|
53
|
+
@@email_subject_prefix || ENV["SOLIDERRORS_EMAIL_SUBJECT_PREFIX"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether the errors database has every table and column this version reads and writes.
|
|
57
|
+
# Assert it in your test suite to catch a schema change you haven't applied yet.
|
|
58
|
+
def migrated?
|
|
59
|
+
{
|
|
60
|
+
SolidErrors::Error => %w[exception_class message severity source resolved_at fingerprint created_at updated_at],
|
|
61
|
+
SolidErrors::Occurrence => %w[error_id backtrace context created_at updated_at]
|
|
62
|
+
}.all? do |model, columns|
|
|
63
|
+
model.table_exists? && (columns - model.column_names).empty?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
36
67
|
def send_emails?
|
|
37
68
|
send_emails && email_to.present? && !ActiveSupport::IsolatedExecutionState[:solid_errors_without_emails]
|
|
38
69
|
end
|
|
@@ -47,11 +78,11 @@ module SolidErrors
|
|
|
47
78
|
end
|
|
48
79
|
|
|
49
80
|
def url_helper_name
|
|
50
|
-
|
|
81
|
+
@@url_helper_name ||= detect_url_helper_name
|
|
51
82
|
end
|
|
52
83
|
|
|
53
84
|
def url_options
|
|
54
|
-
|
|
85
|
+
@@url_options ||= detect_url_options
|
|
55
86
|
end
|
|
56
87
|
|
|
57
88
|
private
|