solid_errors 0.8.0 → 0.9.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/README.md +79 -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/layouts/solid_errors/_style.html.erb +185 -0
- data/app/views/solid_errors/error_mailer/error_occurred.html.erb +0 -1
- data/app/views/solid_errors/occurrences/_collection.html.erb +4 -2
- 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 +40 -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: 1d793218b26cf26f6f6d6a4a87f8e14088cf555739d5ea007894a75cef13646a
|
|
4
|
+
data.tar.gz: a4bfc87e5a230db9ba392991ac37624ea58e5027a487260e9c3982eb13b4ef8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 230583bf528c6765a6205fcfbb1981d935fb866c28126fec475800dc0a11aab1e1a0aaad4a03f631e5b58a166d3d4a805f288b0b4e270ae95a949b0dd4bca7dc
|
|
7
|
+
data.tar.gz: 903a70c2663cf40335e5697ee882728e8aeb56e34519be322fa78be3a3505a2255f16eb78552ad0d7ba2d5bd6602ff8204beeaf63041a0b1b5c65bbf998d32aa
|
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,24 @@ 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 which one it was: the controller action (`UsersController#show`), or the job's class, id and queue.
|
|
161
|
+
|
|
162
|
+
Request params and job arguments are not recorded unless you opt in, because they often carry user content that `config.filter_parameters` can't catch: it matches keys, so positional job arguments, mail delivery jobs (the mail's recipients and body) and webhook payloads would be stored and emailed as they are.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
config.solid_errors.record_params = true # the request's params, with filter_parameters applied
|
|
166
|
+
config.solid_errors.record_arguments = true # the job's arguments, with filter_parameters applied
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Context you set yourself takes precedence. For example, `Rails.error.set_context(params: ...)` in a `before_action` replaces the recorded params.
|
|
170
|
+
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
154
175
|
### Configuration
|
|
155
176
|
|
|
156
177
|
You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently, the following configuration options are available:
|
|
@@ -164,8 +185,10 @@ You can configure Solid Errors via the Rails configuration object, under the `so
|
|
|
164
185
|
* `email_to` - The email address(es) to send a notification to. See [Email notifications](#email-notifications) for more information.
|
|
165
186
|
* `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
187
|
* `email_subject_prefix` - Prefix added to the subject line for email notifications. See [Email notifications](#email-notifications) for more information.
|
|
188
|
+
* `email_subject` - A proc that builds the subject line for email notifications. See [Email notifications](#email-notifications) for more information.
|
|
167
189
|
* `base_controller_class` - Specify a different controller as the base class for the Solid Errors controller. See [Authentication](#authentication) for more information.
|
|
168
190
|
* `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.
|
|
191
|
+
* `record_params` / `record_arguments` - Whether to record request params and job arguments with each occurrence. Both default to `false`. See [What each occurrence records](#what-each-occurrence-records).
|
|
169
192
|
* `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"]`.
|
|
170
193
|
|
|
171
194
|
### Database Configuration
|
|
@@ -230,6 +253,10 @@ config.solid_errors.enabled = false
|
|
|
230
253
|
|
|
231
254
|
When disabled, Solid Errors will not subscribe to the Rails error reporter and no errors will be recorded.
|
|
232
255
|
|
|
256
|
+
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.
|
|
257
|
+
|
|
258
|
+
Configuration belongs in `config/environments/<environment>.rb`, not in `config/<environment>.rb`, which Rails does not load.
|
|
259
|
+
|
|
233
260
|
#### Authentication
|
|
234
261
|
|
|
235
262
|
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 +299,14 @@ Solid Errors _can_ send email notifications whenever an error occurs, if your ap
|
|
|
272
299
|
|
|
273
300
|
There are two ways to configure email notifications. First, you can use environment variables:
|
|
274
301
|
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
302
|
+
```sh
|
|
303
|
+
SOLIDERRORS_SEND_EMAILS=true # defaults to false
|
|
304
|
+
SOLIDERRORS_EMAIL_FROM=errors@myapp.com # defaults to solid_errors@noreply.com
|
|
305
|
+
SOLIDERRORS_EMAIL_TO=devs@myapp.com # no default, must be set
|
|
306
|
+
SOLIDERRORS_EMAIL_SUBJECT_PREFIX="[Application name][Environment]" # no default, optional
|
|
280
307
|
```
|
|
281
308
|
|
|
282
|
-
Second, you can set the values via the configuration object:
|
|
309
|
+
Second, you can set the values via the configuration object, which takes precedence over the environment variables:
|
|
283
310
|
|
|
284
311
|
```ruby
|
|
285
312
|
# Set authentication credentials and optional subject prefix for Solid Errors
|
|
@@ -297,6 +324,32 @@ To be emailed only when an error is new, or when a resolved error occurs again,
|
|
|
297
324
|
config.solid_errors.email_every_occurrence = false # defaults to true
|
|
298
325
|
```
|
|
299
326
|
|
|
327
|
+
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.
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
config.solid_errors.email_subject = ->(error:, occurrence:, reason:) {
|
|
331
|
+
# Counting up to this occurrence, rather than all of them, gives the same answer however late the email is sent
|
|
332
|
+
count = error.occurrences.where(id: ..occurrence.id).count
|
|
333
|
+
subject = "#{error.severity_emoji} #{error.exception_class}"
|
|
334
|
+
|
|
335
|
+
case reason
|
|
336
|
+
when :reopened then "🔄 REOPENED: #{subject}"
|
|
337
|
+
when :resolved then "✅ RESOLVED: #{error.exception_class}"
|
|
338
|
+
else count.in?([10, 100, 1_000]) ? "#{subject} (#{count.ordinalize} occurrence)" : subject
|
|
339
|
+
end
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
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:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
# config/initializers/solid_errors.rb
|
|
347
|
+
ActiveSupport::Notifications.subscribe("error_resolved.solid_errors") do |event|
|
|
348
|
+
occurrence = event.payload[:error].occurrences.last
|
|
349
|
+
SolidErrors::ErrorMailer.error_occurred(occurrence, :resolved).deliver_later if occurrence && SolidErrors.send_emails?
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
300
353
|
#### Lifecycle events
|
|
301
354
|
|
|
302
355
|
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 +383,21 @@ config.solid_errors.destroy_after = 30.days
|
|
|
330
383
|
config.solid_errors.destroy_after = 6.months
|
|
331
384
|
```
|
|
332
385
|
|
|
386
|
+
#### Checking the schema after an upgrade
|
|
387
|
+
|
|
388
|
+
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:
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# test/solid_errors_test.rb
|
|
392
|
+
require "test_helper"
|
|
393
|
+
|
|
394
|
+
class SolidErrorsTest < ActiveSupport::TestCase
|
|
395
|
+
test "the errors database is up to date" do
|
|
396
|
+
assert SolidErrors.migrated?
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
333
401
|
### Examples
|
|
334
402
|
|
|
335
403
|
There are only two screens in the dashboard.
|
|
@@ -412,6 +480,11 @@ You can always take control of the views by creating your own views and/or parti
|
|
|
412
480
|
</html>
|
|
413
481
|
```
|
|
414
482
|
|
|
483
|
+
|
|
484
|
+
### Locales
|
|
485
|
+
|
|
486
|
+
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.
|
|
487
|
+
|
|
415
488
|
## Sponsors
|
|
416
489
|
|
|
417
490
|
<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
|
|
@@ -1190,4 +1190,189 @@
|
|
|
1190
1190
|
padding-right: 0.75rem
|
|
1191
1191
|
}
|
|
1192
1192
|
}
|
|
1193
|
+
|
|
1194
|
+
/*
|
|
1195
|
+
Dark mode follows the operating system. Rather than adding `dark:` variants to
|
|
1196
|
+
the markup, it remaps the light utilities above to their dark counterparts, so
|
|
1197
|
+
the views and the classes the backtrace toggle swaps stay the same.
|
|
1198
|
+
*/
|
|
1199
|
+
|
|
1200
|
+
:root{
|
|
1201
|
+
color-scheme: light dark
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
@media (prefers-color-scheme: dark){
|
|
1205
|
+
*, ::before, ::after{
|
|
1206
|
+
border-color: #374151
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
body{
|
|
1210
|
+
background-color: rgb(17 24 39);
|
|
1211
|
+
color: rgb(229 231 235)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.divide-gray-300 > :not([hidden]) ~ :not([hidden]){
|
|
1215
|
+
--tw-divide-opacity: 1;
|
|
1216
|
+
border-color: rgb(55 65 81 / var(--tw-divide-opacity))
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.border-black{
|
|
1220
|
+
--tw-border-opacity: 1;
|
|
1221
|
+
border-color: rgb(209 213 219 / var(--tw-border-opacity))
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.border-blue-500{
|
|
1225
|
+
--tw-border-opacity: 1;
|
|
1226
|
+
border-color: rgb(96 165 250 / var(--tw-border-opacity))
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.border-gray-300{
|
|
1230
|
+
--tw-border-opacity: 1;
|
|
1231
|
+
border-color: rgb(75 85 99 / var(--tw-border-opacity))
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
.border-red-500{
|
|
1235
|
+
--tw-border-opacity: 1;
|
|
1236
|
+
border-color: rgb(248 113 113 / var(--tw-border-opacity))
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.bg-gray-100{
|
|
1240
|
+
--tw-bg-opacity: 1;
|
|
1241
|
+
background-color: rgb(31 41 55 / var(--tw-bg-opacity))
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.bg-green-50{
|
|
1245
|
+
--tw-bg-opacity: 1;
|
|
1246
|
+
background-color: rgb(5 46 22 / var(--tw-bg-opacity))
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.bg-red-50{
|
|
1250
|
+
--tw-bg-opacity: 1;
|
|
1251
|
+
background-color: rgb(69 10 10 / var(--tw-bg-opacity))
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.bg-white{
|
|
1255
|
+
--tw-bg-opacity: 1;
|
|
1256
|
+
background-color: rgb(17 24 39 / var(--tw-bg-opacity))
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.text-black{
|
|
1260
|
+
--tw-text-opacity: 1;
|
|
1261
|
+
color: rgb(243 244 246 / var(--tw-text-opacity))
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.bg-blue-100{
|
|
1265
|
+
background-color: rgb(30 58 138 / 0.5)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.bg-red-100{
|
|
1269
|
+
background-color: rgb(127 29 29 / 0.5)
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
.bg-yellow-100{
|
|
1273
|
+
background-color: rgb(113 63 18 / 0.5)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.bg-green-100{
|
|
1277
|
+
background-color: rgb(20 83 45 / 0.5)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.bg-violet-100{
|
|
1281
|
+
background-color: rgb(76 29 149 / 0.5)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.text-blue-500{
|
|
1285
|
+
--tw-text-opacity: 1;
|
|
1286
|
+
color: rgb(96 165 250 / var(--tw-text-opacity))
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.text-gray-500{
|
|
1290
|
+
--tw-text-opacity: 1;
|
|
1291
|
+
color: rgb(156 163 175 / var(--tw-text-opacity))
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.text-gray-900{
|
|
1295
|
+
--tw-text-opacity: 1;
|
|
1296
|
+
color: rgb(243 244 246 / var(--tw-text-opacity))
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.text-green-500{
|
|
1300
|
+
--tw-text-opacity: 1;
|
|
1301
|
+
color: rgb(74 222 128 / var(--tw-text-opacity))
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.text-red-500{
|
|
1305
|
+
--tw-text-opacity: 1;
|
|
1306
|
+
color: rgb(248 113 113 / var(--tw-text-opacity))
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.text-blue-800{
|
|
1310
|
+
--tw-text-opacity: 1;
|
|
1311
|
+
color: rgb(191 219 254 / var(--tw-text-opacity))
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.text-red-800{
|
|
1315
|
+
--tw-text-opacity: 1;
|
|
1316
|
+
color: rgb(254 202 202 / var(--tw-text-opacity))
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.text-yellow-800{
|
|
1320
|
+
--tw-text-opacity: 1;
|
|
1321
|
+
color: rgb(254 240 138 / var(--tw-text-opacity))
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
.text-green-800{
|
|
1325
|
+
--tw-text-opacity: 1;
|
|
1326
|
+
color: rgb(187 247 208 / var(--tw-text-opacity))
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.text-violet-800{
|
|
1330
|
+
--tw-text-opacity: 1;
|
|
1331
|
+
color: rgb(221 214 254 / var(--tw-text-opacity))
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.even\:bg-gray-50:nth-child(even){
|
|
1335
|
+
background-color: rgb(31 41 55 / 0.5)
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.hover\:bg-gray-50:hover{
|
|
1339
|
+
--tw-bg-opacity: 1;
|
|
1340
|
+
background-color: rgb(31 41 55 / var(--tw-bg-opacity))
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.hover\:ring-blue-200:hover{
|
|
1344
|
+
--tw-ring-opacity: 1;
|
|
1345
|
+
--tw-ring-color: rgb(30 58 138 / var(--tw-ring-opacity))
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.hover\:ring-gray-200:hover{
|
|
1349
|
+
--tw-ring-opacity: 1;
|
|
1350
|
+
--tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity))
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.hover\:ring-red-200:hover{
|
|
1354
|
+
--tw-ring-opacity: 1;
|
|
1355
|
+
--tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity))
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
.hover\:bg-black:hover{
|
|
1359
|
+
--tw-bg-opacity: 1;
|
|
1360
|
+
background-color: rgb(243 244 246 / var(--tw-bg-opacity))
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.hover\:text-white:hover{
|
|
1364
|
+
--tw-text-opacity: 1;
|
|
1365
|
+
color: rgb(17 24 39 / var(--tw-text-opacity))
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
.hover\:text-gray-700:hover{
|
|
1369
|
+
--tw-text-opacity: 1;
|
|
1370
|
+
color: rgb(229 231 235 / var(--tw-text-opacity))
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
.hover\:border-gray-300:hover{
|
|
1374
|
+
--tw-border-opacity: 1;
|
|
1375
|
+
border-color: rgb(107 114 128 / var(--tw-border-opacity))
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1193
1378
|
</style>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<p class="text-right">
|
|
11
11
|
<% page_count = page.count %>
|
|
12
12
|
<% if page_count.zero? %>
|
|
13
|
-
No
|
|
13
|
+
No occurrences found
|
|
14
14
|
<% elsif page.pages == 1 %>
|
|
15
15
|
<strong><%= page_count %></strong> total
|
|
16
16
|
<% else %>
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
collection: occurrences, as: :occurrence %>
|
|
54
54
|
<% else %>
|
|
55
55
|
<div class="flex items-center justify-center gap-2">
|
|
56
|
-
|
|
56
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ul" viewBox="0 0 16 16">
|
|
57
|
+
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
|
|
58
|
+
</svg>
|
|
57
59
|
<span class="text-2xl">No occurrences yet…</span>
|
|
58
60
|
</div>
|
|
59
61
|
<% end %>
|
|
@@ -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") if SolidErrors.record_params
|
|
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] if SolidErrors.record_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,13 +11,19 @@ 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: []
|
|
24
|
+
# Request params and job arguments carry user content that filter_parameters' key matching misses
|
|
25
|
+
mattr_accessor :record_params, default: false
|
|
26
|
+
mattr_accessor :record_arguments, default: false
|
|
21
27
|
mattr_writer :url_helper_name, :url_options
|
|
22
28
|
|
|
23
29
|
class << self
|
|
@@ -33,6 +39,34 @@ module SolidErrors
|
|
|
33
39
|
@password ||= ENV["SOLIDERRORS_PASSWORD"] || @@password
|
|
34
40
|
end
|
|
35
41
|
|
|
42
|
+
# Each email setting falls back to its SOLIDERRORS_* environment variable when not configured
|
|
43
|
+
def send_emails
|
|
44
|
+
@@send_emails.nil? ? ActiveModel::Type::Boolean.new.cast(ENV["SOLIDERRORS_SEND_EMAILS"]) || false : @@send_emails
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def email_from
|
|
48
|
+
@@email_from || ENV["SOLIDERRORS_EMAIL_FROM"] || "solid_errors@noreply.com"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def email_to
|
|
52
|
+
@@email_to || ENV["SOLIDERRORS_EMAIL_TO"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def email_subject_prefix
|
|
56
|
+
@@email_subject_prefix || ENV["SOLIDERRORS_EMAIL_SUBJECT_PREFIX"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether the errors database has every table and column this version reads and writes.
|
|
60
|
+
# Assert it in your test suite to catch a schema change you haven't applied yet.
|
|
61
|
+
def migrated?
|
|
62
|
+
{
|
|
63
|
+
SolidErrors::Error => %w[exception_class message severity source resolved_at fingerprint created_at updated_at],
|
|
64
|
+
SolidErrors::Occurrence => %w[error_id backtrace context created_at updated_at]
|
|
65
|
+
}.all? do |model, columns|
|
|
66
|
+
model.table_exists? && (columns - model.column_names).empty?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
36
70
|
def send_emails?
|
|
37
71
|
send_emails && email_to.present? && !ActiveSupport::IsolatedExecutionState[:solid_errors_without_emails]
|
|
38
72
|
end
|
|
@@ -47,11 +81,11 @@ module SolidErrors
|
|
|
47
81
|
end
|
|
48
82
|
|
|
49
83
|
def url_helper_name
|
|
50
|
-
|
|
84
|
+
@@url_helper_name ||= detect_url_helper_name
|
|
51
85
|
end
|
|
52
86
|
|
|
53
87
|
def url_options
|
|
54
|
-
|
|
88
|
+
@@url_options ||= detect_url_options
|
|
55
89
|
end
|
|
56
90
|
|
|
57
91
|
private
|