solid_errors 0.7.0 → 0.8.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 +66 -8
- data/app/mailers/solid_errors/error_mailer.rb +20 -0
- data/app/models/solid_errors/backtrace.rb +1 -1
- data/app/models/solid_errors/error.rb +43 -0
- data/app/models/solid_errors/occurrence.rb +15 -2
- data/app/views/layouts/solid_errors/{_style.html → _style.html.erb} +1 -1
- data/app/views/layouts/solid_errors/application.html.erb +85 -1
- data/app/views/solid_errors/error_mailer/error_occurred.html.erb +160 -7
- data/app/views/solid_errors/error_mailer/error_occurred.text.erb +32 -0
- data/app/views/solid_errors/errors/_actions.html.erb +9 -0
- data/app/views/solid_errors/occurrences/_backtrace_line.html.erb +14 -0
- data/app/views/solid_errors/occurrences/_collection.html.erb +4 -4
- data/app/views/solid_errors/occurrences/_occurrence.html.erb +32 -15
- data/lib/solid_errors/engine.rb +1 -1
- data/lib/solid_errors/subscriber.rb +5 -2
- data/lib/solid_errors/version.rb +1 -1
- data/lib/solid_errors.rb +39 -1
- metadata +16 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11cd9651413ea528780669e5c01abec456118a34a70c93042a85a471c0d1fd07
|
|
4
|
+
data.tar.gz: 39f16c51d5bb62144c8b1b138c18fc6fa4dff4e601c635ed3046f9d37f1d960a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39341060f4ee0e9bfdc009fedd2176fb9bb7ae6c60aaddc175f8e4449d2274f7014f6df71b683865a94eb6c779700c4b0356a0addf2c435a40162027a45b5bec
|
|
7
|
+
data.tar.gz: 87f876f3bdcdf11dcdeac4ded4c6ae873c3f5a53c87aebe454ce925416a66ad19dc37d4984b9a6c326fe79e2c1bab16f6c20f6feb9f79381a70965e35d058030
|
data/README.md
CHANGED
|
@@ -24,13 +24,8 @@
|
|
|
24
24
|
</a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
Solid Errors is a DB-based, app-internal exception tracker for Rails applications, designed with simplicity and performance in mind. It uses the new [Rails error reporting API](https://guides.rubyonrails.org/error_reporting.html) to store uncaught exceptions in the database, and provides a simple UI for viewing and managing exceptions.
|
|
29
28
|
|
|
30
|
-
> [!WARNING]
|
|
31
|
-
> The current point release of Rails (7.1.3.2) has a bug which severely limits the utility of Solid Errors. Exceptions raised during a web request *are not* reported to Rails' error reporter. There is a fix in the `main` branch, but it has not been released in a new point release. As such, Solid Errors is **not** production-ready unless you are running Rails from the `main` branch or until a new point version is released and you upgrade.
|
|
32
|
-
> The original bug report can be found [here](https://github.com/rails/rails/issues/51002) and the pull request making the fix is [here](https://github.com/rails/rails/pull/51050). I will try to backport the fix into the gem directly, but I haven't quite figured it out yet.
|
|
33
|
-
|
|
34
29
|
|
|
35
30
|
## Installation
|
|
36
31
|
|
|
@@ -80,7 +75,7 @@ production:
|
|
|
80
75
|
```
|
|
81
76
|
|
|
82
77
|
> [!NOTE]
|
|
83
|
-
> Calling `bin/rails solid_errors:install` will automatically add `config.solid_errors.connects_to = { database: { writing: :errors } }` to `config/environments/production.rb`, so no additional configuration is needed there (although you must make sure that you use the `errors` name in `database.yml` for this to match!). But if you want to use Solid Errors in a different environment (like staging or even development), you'll have to manually add that `config.solid_errors.connects_to` line to the respective environment file. And, as always, make sure that the name you're using for the database in `config/database.yml` matches the name you use in `config.solid_errors.connects_to`.
|
|
78
|
+
> Calling `bin/rails generate solid_errors:install` will automatically add `config.solid_errors.connects_to = { database: { writing: :errors } }` to `config/environments/production.rb`, so no additional configuration is needed there (although you must make sure that you use the `errors` name in `database.yml` for this to match!). But if you want to use Solid Errors in a different environment (like staging or even development), you'll have to manually add that `config.solid_errors.connects_to` line to the respective environment file. And, as always, make sure that the name you're using for the database in `config/database.yml` matches the name you use in `config.solid_errors.connects_to`.
|
|
84
79
|
|
|
85
80
|
Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
|
|
86
81
|
|
|
@@ -158,17 +153,20 @@ end
|
|
|
158
153
|
|
|
159
154
|
### Configuration
|
|
160
155
|
|
|
161
|
-
You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently,
|
|
156
|
+
You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently, the following configuration options are available:
|
|
162
157
|
|
|
158
|
+
* `enabled` - Enable or disable error tracking. Defaults to `true`. See [Disabling error tracking](#disabling-error-tracking) for more information.
|
|
163
159
|
* `connects_to` - The database configuration to use for the Solid Errors database. See [Database Configuration](#database-configuration) for more information.
|
|
164
160
|
* `username` - The username to use for HTTP authentication. See [Authentication](#authentication) for more information.
|
|
165
161
|
* `password` - The password to use for HTTP authentication. See [Authentication](#authentication) for more information.
|
|
166
162
|
* `sends_email` - Whether or not to send emails when an error occurs. See [Email notifications](#email-notifications) for more information.
|
|
167
163
|
* `email_from` - The email address to send a notification from. See [Email notifications](#email-notifications) for more information.
|
|
168
164
|
* `email_to` - The email address(es) to send a notification to. See [Email notifications](#email-notifications) for more information.
|
|
165
|
+
* `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.
|
|
169
166
|
* `email_subject_prefix` - Prefix added to the subject line for email notifications. See [Email notifications](#email-notifications) for more information.
|
|
170
167
|
* `base_controller_class` - Specify a different controller as the base class for the Solid Errors controller. See [Authentication](#authentication) for more information.
|
|
171
168
|
* `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
|
+
* `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"]`.
|
|
172
170
|
|
|
173
171
|
### Database Configuration
|
|
174
172
|
|
|
@@ -196,6 +194,42 @@ Running Solid Errors in a separate database is recommended, but it's also possib
|
|
|
196
194
|
|
|
197
195
|
You won't have multiple databases, so `database.yml` doesn't need to have the errors database configuration.
|
|
198
196
|
|
|
197
|
+
#### Apps that use `schema_format = :sql`
|
|
198
|
+
|
|
199
|
+
The installer writes `db/errors_schema.rb`, but an app with `config.active_record.schema_format = :sql` looks for `db/errors_structure.sql` for the errors database.
|
|
200
|
+
|
|
201
|
+
On Rails 8.1 and later, keep the Ruby schema for the errors database only by setting `schema_format` on it in `config/database.yml`:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
production:
|
|
205
|
+
errors:
|
|
206
|
+
<<: *default
|
|
207
|
+
database: storage/production_errors.sqlite3
|
|
208
|
+
migrations_paths: db/errors_migrate
|
|
209
|
+
schema_format: ruby
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
On earlier Rails versions, create the errors database, load the Ruby schema into it, and dump it as SQL for your database adapter:
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
bin/rails db:create:errors
|
|
216
|
+
bin/rails runner 'ActiveRecord::Tasks::DatabaseTasks.load_schema(ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: "errors"), :ruby, "db/errors_schema.rb")'
|
|
217
|
+
bin/rails db:schema:dump:errors
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This writes `db/errors_structure.sql`, which you commit in place of `db/errors_schema.rb`.
|
|
221
|
+
|
|
222
|
+
#### Disabling error tracking
|
|
223
|
+
|
|
224
|
+
Solid Errors is enabled by default. If you want to disable error tracking in a specific environment, you can set `enabled` to `false`:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# config/environments/development.rb
|
|
228
|
+
config.solid_errors.enabled = false
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
When disabled, Solid Errors will not subscribe to the Rails error reporter and no errors will be recorded.
|
|
232
|
+
|
|
199
233
|
#### Authentication
|
|
200
234
|
|
|
201
235
|
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.
|
|
@@ -257,6 +291,31 @@ config.solid_errors.email_subject_prefix = "[#{Rails.application.name}][#{Rails.
|
|
|
257
291
|
|
|
258
292
|
If you have set `send_emails` to `true` and have set an `email_to` address, Solid Errors will send an email notification whenever an error occurs. If you have not set `send_emails` to `true` or have not set an `email_to` address, Solid Errors will not send any email notifications.
|
|
259
293
|
|
|
294
|
+
To be emailed only when an error is new, or when a resolved error occurs again, rather than for every occurrence:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
config.solid_errors.email_every_occurrence = false # defaults to true
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Lifecycle events
|
|
301
|
+
|
|
302
|
+
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:
|
|
303
|
+
|
|
304
|
+
| Event | When | Payload |
|
|
305
|
+
| --- | --- | --- |
|
|
306
|
+
| `error_created.solid_errors` | An error is recorded for the first time | `:error`, `:occurrence` |
|
|
307
|
+
| `error_reopened.solid_errors` | A resolved error occurs again | `:error`, `:occurrence` |
|
|
308
|
+
| `error_resolved.solid_errors` | An error is marked as resolved | `:error` |
|
|
309
|
+
| `occurrence_created.solid_errors` | Any occurrence is recorded, including those above | `:error`, `:occurrence` |
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
# config/initializers/solid_errors.rb
|
|
313
|
+
ActiveSupport::Notifications.subscribe(/\Aerror_(created|reopened)\.solid_errors\z/) do |event|
|
|
314
|
+
error = event.payload[:error]
|
|
315
|
+
SlackNotifier.post("#{error.exception_class}: #{error.message}")
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
260
319
|
#### Automatically destroying old records
|
|
261
320
|
|
|
262
321
|
Setting `destroy_after` to a duration will allow Solid Errors to be self-maintaining by peridically destroying **resolved** records that are older than that value. The value provided must respond to `.ago`.
|
|
@@ -381,4 +440,3 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
381
440
|
## Code of Conduct
|
|
382
441
|
|
|
383
442
|
Everyone interacting in the SolidErrors project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/fractaledmind/solid_errors/blob/main/CODE_OF_CONDUCT.md).
|
|
384
|
-
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
module SolidErrors
|
|
2
2
|
# adapted from: https://github.com/codergeek121/email_error_reporter/blob/main/lib/email_error_reporter/error_mailer.rb
|
|
3
3
|
class ErrorMailer < (defined?(ActionMailer::Base) ? ActionMailer::Base : Object)
|
|
4
|
+
if defined?(ActionMailer::Base)
|
|
5
|
+
# A failure here reported as usual would email again and fail again, without end
|
|
6
|
+
rescue_from StandardError do |exception|
|
|
7
|
+
SolidErrors.without_emails do
|
|
8
|
+
Rails.error.report(exception, handled: true, source: "solid_errors.error_mailer")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
helper_method :occurrence_url
|
|
13
|
+
end
|
|
14
|
+
|
|
4
15
|
def error_occurred(occurrence)
|
|
5
16
|
unless defined?(ActionMailer::Base)
|
|
6
17
|
raise "ActionMailer is not available. Make sure that you require \"action_mailer/railtie\" in application.rb"
|
|
@@ -17,5 +28,14 @@ module SolidErrors
|
|
|
17
28
|
to: SolidErrors.email_to
|
|
18
29
|
)
|
|
19
30
|
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Optional: an app without default_url_options[:host] still gets the email, just unlinked
|
|
35
|
+
def occurrence_url
|
|
36
|
+
public_send(SolidErrors.url_helper_name).error_url(@error, **SolidErrors.url_options, anchor: "occurrence_#{@occurrence.id}")
|
|
37
|
+
rescue ArgumentError, NoMethodError, ActionController::UrlGenerationError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
20
40
|
end
|
|
21
41
|
end
|
|
@@ -30,6 +30,8 @@ module SolidErrors
|
|
|
30
30
|
scope :resolved, -> { where.not(resolved_at: nil) }
|
|
31
31
|
scope :unresolved, -> { where(resolved_at: nil) }
|
|
32
32
|
|
|
33
|
+
after_update_commit :publish_resolved, if: -> { saved_change_to_resolved_at? && resolved? }
|
|
34
|
+
|
|
33
35
|
def severity_emoji
|
|
34
36
|
SEVERITY_TO_EMOJI[severity.to_sym]
|
|
35
37
|
end
|
|
@@ -53,5 +55,46 @@ module SolidErrors
|
|
|
53
55
|
def resolved?
|
|
54
56
|
resolved_at.present?
|
|
55
57
|
end
|
|
58
|
+
|
|
59
|
+
def to_markdown
|
|
60
|
+
md = ""
|
|
61
|
+
md << "## Exception: #{exception_class}\n"
|
|
62
|
+
md << "**Severity:** #{severity} | **Source:** #{source}\n\n"
|
|
63
|
+
md << "### Message\n#{message}\n"
|
|
64
|
+
|
|
65
|
+
occurrence = occurrences.order(created_at: :desc).first
|
|
66
|
+
return md unless occurrence
|
|
67
|
+
|
|
68
|
+
backtrace = occurrence.parsed_backtrace
|
|
69
|
+
|
|
70
|
+
md << "\n### Backtrace\n"
|
|
71
|
+
backtrace.lines.each do |line|
|
|
72
|
+
md << (line.filtered_method ? line.to_s : line.unparsed_line) << "\n"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
app_lines = backtrace.application_lines
|
|
76
|
+
if app_lines.any?
|
|
77
|
+
md << "\n### Source Context\n"
|
|
78
|
+
app_lines.each do |line|
|
|
79
|
+
md << "#### #{line}\n"
|
|
80
|
+
md << "```ruby\n"
|
|
81
|
+
line.source.each { |n, code| md << "#{n}: #{code.chomp}\n" }
|
|
82
|
+
md << "```\n\n"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if occurrence.context.present?
|
|
87
|
+
md << "### Request Context\n"
|
|
88
|
+
occurrence.context.each { |k, v| md << "- #{k}: #{v}\n" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
md
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def publish_resolved
|
|
97
|
+
ActiveSupport::Notifications.instrument("error_resolved.solid_errors", error: self)
|
|
98
|
+
end
|
|
56
99
|
end
|
|
57
100
|
end
|
|
@@ -2,7 +2,14 @@ module SolidErrors
|
|
|
2
2
|
class Occurrence < Record
|
|
3
3
|
belongs_to :error, class_name: "SolidErrors::Error"
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Adapters that report the json column as text (MariaDB) would otherwise store Hash#inspect
|
|
6
|
+
attribute :context, :json
|
|
7
|
+
|
|
8
|
+
# :created or :reopened, set by the subscriber when this occurrence opens its error
|
|
9
|
+
attr_accessor :opens_error
|
|
10
|
+
|
|
11
|
+
after_create_commit :send_email, if: -> { SolidErrors.send_emails? && (SolidErrors.email_every_occurrence || opens_error) }
|
|
12
|
+
after_create_commit :publish_events
|
|
6
13
|
after_create_commit :clear_resolved_errors, if: :should_clear_resolved_errors?
|
|
7
14
|
|
|
8
15
|
# The parsed exception backtrace. Lines in this backtrace that are from installed gems
|
|
@@ -12,7 +19,7 @@ module SolidErrors
|
|
|
12
19
|
def parsed_backtrace
|
|
13
20
|
return @parsed_backtrace if defined? @parsed_backtrace
|
|
14
21
|
|
|
15
|
-
@parsed_backtrace = parse_backtrace(backtrace
|
|
22
|
+
@parsed_backtrace = parse_backtrace(backtrace&.split("\n"))
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
private
|
|
@@ -21,6 +28,12 @@ module SolidErrors
|
|
|
21
28
|
Backtrace.parse(backtrace)
|
|
22
29
|
end
|
|
23
30
|
|
|
31
|
+
def publish_events
|
|
32
|
+
payload = {error: error, occurrence: self}
|
|
33
|
+
ActiveSupport::Notifications.instrument("error_#{opens_error}.solid_errors", payload) if opens_error
|
|
34
|
+
ActiveSupport::Notifications.instrument("occurrence_created.solid_errors", payload)
|
|
35
|
+
end
|
|
36
|
+
|
|
24
37
|
def send_email
|
|
25
38
|
ErrorMailer.error_occurred(self).deliver_later
|
|
26
39
|
end
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
<%= csrf_meta_tags %>
|
|
6
6
|
<%= csp_meta_tag %>
|
|
7
7
|
|
|
8
|
-
<%= render "layouts/solid_errors/style" %>
|
|
8
|
+
<%= render "layouts/solid_errors/style", nonce: content_security_policy_nonce %>
|
|
9
9
|
</head>
|
|
10
10
|
<body class="h-full flex flex-col pb-4">
|
|
11
11
|
<main class="grow container mx-auto mt-4">
|
|
12
|
+
<% if defined?(main_app.root_path) %>
|
|
13
|
+
<%= link_to "← Back to main app", main_app.root_path, class: "text-sm text-blue-500 hover:underline decoration-blue-500" %>
|
|
14
|
+
<% end %>
|
|
12
15
|
<%= content_for?(:content) ? yield(:content) : yield %>
|
|
13
16
|
</main>
|
|
14
17
|
|
|
@@ -51,6 +54,87 @@
|
|
|
51
54
|
document.querySelectorAll('[data-controller="fade"]').forEach(element => {
|
|
52
55
|
fadeOut(element);
|
|
53
56
|
});
|
|
57
|
+
|
|
58
|
+
// Backtrace filter toggle functionality
|
|
59
|
+
(function() {
|
|
60
|
+
const STORAGE_KEY = 'solid_errors_backtrace_filter';
|
|
61
|
+
|
|
62
|
+
function toggleBacktrace(button) {
|
|
63
|
+
const filter = button.getAttribute('data-backtrace-toggle');
|
|
64
|
+
const occurrenceId = button.getAttribute('data-occurrence-id');
|
|
65
|
+
|
|
66
|
+
// Toggle visibility
|
|
67
|
+
const appTrace = document.getElementById(`backtrace-application-${occurrenceId}`);
|
|
68
|
+
const fullTrace = document.getElementById(`backtrace-full-${occurrenceId}`);
|
|
69
|
+
|
|
70
|
+
if (filter === 'application') {
|
|
71
|
+
appTrace.removeAttribute('hidden');
|
|
72
|
+
fullTrace.setAttribute('hidden', '');
|
|
73
|
+
} else {
|
|
74
|
+
appTrace.setAttribute('hidden', '');
|
|
75
|
+
fullTrace.removeAttribute('hidden');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Update button states for this occurrence
|
|
79
|
+
const occurrenceButtons = document.querySelectorAll(`[data-occurrence-id="${occurrenceId}"][data-backtrace-toggle]`);
|
|
80
|
+
occurrenceButtons.forEach(btn => {
|
|
81
|
+
if (btn === button) {
|
|
82
|
+
btn.classList.add('border-blue-500', 'text-blue-500');
|
|
83
|
+
btn.classList.remove('border-transparent', 'text-gray-500');
|
|
84
|
+
} else {
|
|
85
|
+
btn.classList.remove('border-blue-500', 'text-blue-500');
|
|
86
|
+
btn.classList.add('border-transparent', 'text-gray-500');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Remember the choice; storage can be unavailable (private browsing)
|
|
91
|
+
try { sessionStorage.setItem(STORAGE_KEY, filter); } catch (e) {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Initialize on page load
|
|
95
|
+
let savedFilter = 'application';
|
|
96
|
+
try { savedFilter = sessionStorage.getItem(STORAGE_KEY) || savedFilter; } catch (e) {}
|
|
97
|
+
|
|
98
|
+
// Apply saved filter to all occurrences
|
|
99
|
+
if (savedFilter === 'full') {
|
|
100
|
+
document.querySelectorAll('[id^="backtrace-application-"]').forEach(el => el.setAttribute('hidden', ''));
|
|
101
|
+
document.querySelectorAll('[id^="backtrace-full-"]').forEach(el => el.removeAttribute('hidden'));
|
|
102
|
+
|
|
103
|
+
// Update button states
|
|
104
|
+
document.querySelectorAll('[data-backtrace-toggle]').forEach(btn => {
|
|
105
|
+
const filter = btn.getAttribute('data-backtrace-toggle');
|
|
106
|
+
if (filter === savedFilter) {
|
|
107
|
+
btn.classList.add('border-blue-500', 'text-blue-500');
|
|
108
|
+
btn.classList.remove('border-transparent', 'text-gray-500');
|
|
109
|
+
} else {
|
|
110
|
+
btn.classList.remove('border-blue-500', 'text-blue-500');
|
|
111
|
+
btn.classList.add('border-transparent', 'text-gray-500');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Attach event listeners
|
|
117
|
+
document.querySelectorAll('[data-backtrace-toggle]').forEach(button => {
|
|
118
|
+
button.addEventListener('click', function() {
|
|
119
|
+
toggleBacktrace(this);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
})();
|
|
123
|
+
|
|
124
|
+
var copyBtn = document.getElementById('copy-markdown');
|
|
125
|
+
if (copyBtn) {
|
|
126
|
+
copyBtn.addEventListener('click', function() {
|
|
127
|
+
var text = document.getElementById('error-markdown').content.textContent;
|
|
128
|
+
var span = copyBtn.querySelector('span');
|
|
129
|
+
var originalText = span.textContent;
|
|
130
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
131
|
+
span.textContent = 'Copied!';
|
|
132
|
+
setTimeout(function() { span.textContent = originalText; }, 2000);
|
|
133
|
+
}).catch(function(err) {
|
|
134
|
+
alert('Failed to copy: ' + err);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
54
138
|
</script>
|
|
55
139
|
</body>
|
|
56
140
|
</html>
|
|
@@ -2,16 +2,169 @@
|
|
|
2
2
|
<head>
|
|
3
3
|
<title>Solid Errors | <%= @error.severity_emoji %> <%= @error.exception_class %></title>
|
|
4
4
|
<%= render "layouts/solid_errors/style" %>
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
padding: 20px;
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
9
|
+
}
|
|
10
|
+
.email-header {
|
|
11
|
+
margin-bottom: 16px;
|
|
12
|
+
}
|
|
13
|
+
.email-header h1 {
|
|
14
|
+
font-size: 20px;
|
|
15
|
+
font-weight: 700;
|
|
16
|
+
margin: 0 0 8px 0;
|
|
17
|
+
}
|
|
18
|
+
.email-header h1 a {
|
|
19
|
+
color: #1f2937;
|
|
20
|
+
text-decoration: none;
|
|
21
|
+
}
|
|
22
|
+
.email-header h1 a:hover {
|
|
23
|
+
text-decoration: underline;
|
|
24
|
+
}
|
|
25
|
+
.email-link {
|
|
26
|
+
font-size: 14px;
|
|
27
|
+
color: #2563eb;
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
margin-bottom: 12px;
|
|
30
|
+
display: inline-block;
|
|
31
|
+
}
|
|
32
|
+
.error-message {
|
|
33
|
+
background: #f3f4f6;
|
|
34
|
+
padding: 12px;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
margin: 12px 0;
|
|
37
|
+
font-family: monospace;
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
white-space: pre-wrap;
|
|
40
|
+
}
|
|
41
|
+
.info-table {
|
|
42
|
+
width: auto;
|
|
43
|
+
border-collapse: collapse;
|
|
44
|
+
margin: 12px 0;
|
|
45
|
+
font-size: 13px;
|
|
46
|
+
}
|
|
47
|
+
.info-table td {
|
|
48
|
+
padding: 4px 8px;
|
|
49
|
+
border-bottom: 1px solid #e5e7eb;
|
|
50
|
+
}
|
|
51
|
+
.info-table td:first-child {
|
|
52
|
+
font-weight: 600;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
vertical-align: top;
|
|
55
|
+
}
|
|
56
|
+
.backtrace {
|
|
57
|
+
background: #1e293b;
|
|
58
|
+
color: #fff;
|
|
59
|
+
padding: 12px;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
margin: 12px 0;
|
|
62
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
line-height: 1.4;
|
|
65
|
+
overflow-x: auto;
|
|
66
|
+
}
|
|
67
|
+
.backtrace-line {
|
|
68
|
+
margin: 2px 0;
|
|
69
|
+
}
|
|
70
|
+
.backtrace-highlight {
|
|
71
|
+
background: #374151;
|
|
72
|
+
margin-left: -12px;
|
|
73
|
+
margin-right: -12px;
|
|
74
|
+
padding-left: 12px;
|
|
75
|
+
padding-right: 12px;
|
|
76
|
+
}
|
|
77
|
+
.context-table {
|
|
78
|
+
width: auto;
|
|
79
|
+
border-collapse: collapse;
|
|
80
|
+
margin: 12px 0;
|
|
81
|
+
font-size: 13px;
|
|
82
|
+
}
|
|
83
|
+
.context-table th {
|
|
84
|
+
background: #f3f4f6;
|
|
85
|
+
padding: 6px 8px;
|
|
86
|
+
text-align: left;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
border: 1px solid #e5e7eb;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
}
|
|
91
|
+
.context-table td {
|
|
92
|
+
padding: 6px 8px;
|
|
93
|
+
border: 1px solid #e5e7eb;
|
|
94
|
+
font-family: monospace;
|
|
95
|
+
font-size: 12px;
|
|
96
|
+
}
|
|
97
|
+
.context-table td:first-child {
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
}
|
|
100
|
+
.section-title {
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
font-size: 14px;
|
|
103
|
+
margin: 16px 0 8px 0;
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
5
106
|
</head>
|
|
6
107
|
<body>
|
|
7
|
-
<%= render "solid_errors/errors/error",
|
|
8
|
-
error: @error,
|
|
9
|
-
show_actions: false %>
|
|
10
108
|
|
|
11
|
-
<
|
|
12
|
-
|
|
109
|
+
<div class="email-header">
|
|
110
|
+
<h1>
|
|
111
|
+
<%= @error.severity_emoji %>
|
|
112
|
+
<%= @error.exception_class %>
|
|
113
|
+
from <em><%= @error.source %></em>
|
|
114
|
+
</h1>
|
|
115
|
+
<% if (url = occurrence_url) %>
|
|
116
|
+
<a href="<%= url %>" class="email-link">View in Solid Errors →</a>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
13
119
|
|
|
14
|
-
|
|
15
|
-
|
|
120
|
+
<div class="error-message"><%= @error.message %></div>
|
|
121
|
+
|
|
122
|
+
<table class="info-table">
|
|
123
|
+
<tr>
|
|
124
|
+
<td>Severity</td>
|
|
125
|
+
<td><%= @error.severity_emoji %> <%= @error.severity %></td>
|
|
126
|
+
</tr>
|
|
127
|
+
<tr>
|
|
128
|
+
<td>Status</td>
|
|
129
|
+
<td><%= @error.status_emoji %> <%= @error.status %></td>
|
|
130
|
+
</tr>
|
|
131
|
+
<tr>
|
|
132
|
+
<td>Occurred at</td>
|
|
133
|
+
<td><%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
|
|
134
|
+
</tr>
|
|
135
|
+
</table>
|
|
136
|
+
|
|
137
|
+
<% if @occurrence.context&.any? %>
|
|
138
|
+
<div class="section-title">Context</div>
|
|
139
|
+
<table class="context-table">
|
|
140
|
+
<thead>
|
|
141
|
+
<tr>
|
|
142
|
+
<th>Key</th>
|
|
143
|
+
<th>Value</th>
|
|
144
|
+
</tr>
|
|
145
|
+
</thead>
|
|
146
|
+
<tbody>
|
|
147
|
+
<% @occurrence.context.each do |key, value| %>
|
|
148
|
+
<tr>
|
|
149
|
+
<td><%= key %></td>
|
|
150
|
+
<td><%= value %></td>
|
|
151
|
+
</tr>
|
|
152
|
+
<% end %>
|
|
153
|
+
</tbody>
|
|
154
|
+
</table>
|
|
155
|
+
<% end %>
|
|
156
|
+
|
|
157
|
+
<div class="section-title">Backtrace</div>
|
|
158
|
+
<div class="backtrace">
|
|
159
|
+
<% @occurrence.parsed_backtrace.lines.each_with_index do |line, i| %>
|
|
160
|
+
<div class="backtrace-line <%= i.zero? ? 'backtrace-highlight' : '' %>">
|
|
161
|
+
<% if line.filtered_file %>
|
|
162
|
+
<span style="color: #9ca3af;"><%= File.dirname(line.filtered_file) %>/</span><span style="color: #60a5fa; font-weight: 500;"><%= File.basename(line.filtered_file) %></span>:<span style="color: #fff; font-weight: 500;"><%= line.filtered_number %></span> <span style="color: #9ca3af;">in</span> <span style="color: #34d399;"><%= line.filtered_method %></span>
|
|
163
|
+
<% else %>
|
|
164
|
+
<span style="color: #9ca3af;"><%= line.unparsed_line %></span>
|
|
165
|
+
<% end %>
|
|
166
|
+
</div>
|
|
167
|
+
<% end %>
|
|
168
|
+
</div>
|
|
16
169
|
</body>
|
|
17
170
|
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%= @error.severity_emoji %> <%= @error.exception_class %> from <%= @error.source %>
|
|
2
|
+
================================================================================
|
|
3
|
+
|
|
4
|
+
<%= @error.message %>
|
|
5
|
+
|
|
6
|
+
<% if (url = occurrence_url) %>
|
|
7
|
+
View in Solid Errors: <%= url %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
DETAILS
|
|
11
|
+
--------------------------------------------------------------------------------
|
|
12
|
+
Severity: <%= @error.severity_emoji %> <%= @error.severity %>
|
|
13
|
+
Status: <%= @error.status_emoji %> <%= @error.status %>
|
|
14
|
+
Occurred at: <%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %>
|
|
15
|
+
|
|
16
|
+
<% if @occurrence.context&.any? %>
|
|
17
|
+
CONTEXT
|
|
18
|
+
--------------------------------------------------------------------------------
|
|
19
|
+
<% @occurrence.context.each do |key, value| %>
|
|
20
|
+
<%= key.to_s.ljust(20) %> <%= value %>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<% end %>
|
|
24
|
+
BACKTRACE
|
|
25
|
+
--------------------------------------------------------------------------------
|
|
26
|
+
<% @occurrence.parsed_backtrace.lines.each_with_index do |line, i| %>
|
|
27
|
+
<% if line.filtered_file %>
|
|
28
|
+
<%= i.zero? ? '>>>' : ' ' %> <%= File.join(File.dirname(line.filtered_file), File.basename(line.filtered_file)) %>:<%= line.filtered_number %> in <%= line.filtered_method %>
|
|
29
|
+
<% else %>
|
|
30
|
+
<%= i.zero? ? '>>>' : ' ' %> <%= line.unparsed_line %>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% end %>
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
</svg>
|
|
6
6
|
<span>Back to errors</span>
|
|
7
7
|
<% end %>
|
|
8
|
+
<template id="error-markdown"><%= error.to_markdown %></template>
|
|
9
|
+
<button id="copy-markdown"
|
|
10
|
+
class="inline-flex items-center justify-center gap-2 font-medium cursor-pointer border rounded-lg py-3 px-5 bg-gray-100 text-initial border-gray-300 hover:ring-gray-200 hover:ring-8">
|
|
11
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
|
|
12
|
+
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
|
|
13
|
+
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
|
|
14
|
+
</svg>
|
|
15
|
+
<span>Copy Markdown</span>
|
|
16
|
+
</button>
|
|
8
17
|
<% if error.resolved? %>
|
|
9
18
|
<%= render 'solid_errors/errors/delete_button', error: error %>
|
|
10
19
|
<% else %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%= tag.details open: open do %>
|
|
2
|
+
<summary class="hover:bg-gray-50 px-2 py-1 rounded cursor-pointer">
|
|
3
|
+
<% if line.filtered_file %>
|
|
4
|
+
<span class="text-gray-500"><%= File.dirname(line.filtered_file) %>/</span><span class="text-blue-500 font-medium"><%= File.basename(line.filtered_file) %></span>:<span class="text-gray-900 font-medium"><%= line.filtered_number %></span>
|
|
5
|
+
<span class="text-gray-500">in</span>
|
|
6
|
+
<code class="text-green-500 font-medium"><%= line.filtered_method %></code>
|
|
7
|
+
<% else %>
|
|
8
|
+
<span class="text-gray-500"><%= line.unparsed_line %></span>
|
|
9
|
+
<% end %>
|
|
10
|
+
</summary>
|
|
11
|
+
<div><pre class="flex overflow-auto rounded-b-lg bg-slate-800 p-4 text-sm leading-normal text-white sm:rounded-t-lg"><code class="flex flex-1 flex-col min-h-full min-w-content px-0"><% line.source.each do |n, code| %>
|
|
12
|
+
<div class="line <%= n == line.filtered_number.to_i ? 'bg-gray-600/50' : '' %>"><span class="mr-2 text-right select-none text-gray-600"><%= n %></span><span><%= code %></span></div>
|
|
13
|
+
<% end %></code></pre></div>
|
|
14
|
+
<% end %>
|
|
@@ -20,25 +20,25 @@
|
|
|
20
20
|
|
|
21
21
|
<nav aria-label="Pages">
|
|
22
22
|
<% if (first_page = page.first) %>
|
|
23
|
-
<a href="<%= url_for(
|
|
23
|
+
<a href="<%= url_for(page: first_page, items: params[:items]) %>" aria-label="First" class="border border-black text-black bg-white py-1 px-2 cursor-pointer hover:bg-black hover:text-white">«</a>
|
|
24
24
|
<% else %>
|
|
25
25
|
<a role="link" aria-disabled="true" aria-label="First" class="border text-gray-500 bg-white py-1 px-2">«</a>
|
|
26
26
|
<% end %>
|
|
27
27
|
|
|
28
28
|
<% if (prev_page = page.prev) %>
|
|
29
|
-
<a href="<%= url_for(
|
|
29
|
+
<a href="<%= url_for(page: prev_page, items: params[:items]) %>" aria-label="Previous" class="border border-black text-black bg-white py-1 px-2 cursor-pointer hover:bg-black hover:text-white">‹</a>
|
|
30
30
|
<% else %>
|
|
31
31
|
<a role="link" aria-disabled="true" aria-label="Previous" class="border text-gray-500 bg-white py-1 px-2">‹</a>
|
|
32
32
|
<% end %>
|
|
33
33
|
|
|
34
34
|
<% if (next_page = page.next) %>
|
|
35
|
-
<a href="<%= url_for(
|
|
35
|
+
<a href="<%= url_for(page: next_page, items: params[:items]) %>" aria-label="Next" class="border border-black text-black bg-white py-1 px-2 cursor-pointer hover:bg-black hover:text-white">›</a>
|
|
36
36
|
<% else %>
|
|
37
37
|
<a role="link" aria-disabled="true" aria-label="Next" class="border text-gray-500 bg-white py-1 px-2">›</a>
|
|
38
38
|
<% end %>
|
|
39
39
|
|
|
40
40
|
<% if (last_page = page.last) %>
|
|
41
|
-
<a href="<%= url_for(
|
|
41
|
+
<a href="<%= url_for(page: last_page, items: params[:items]) %>" aria-label="Last" class="border border-black text-black bg-white py-1 px-2 cursor-pointer hover:bg-black hover:text-white">»</a>
|
|
42
42
|
<% else %>
|
|
43
43
|
<a role="link" aria-disabled="true" aria-label="Last" class="border text-gray-500 bg-white py-1 px-2">»</a>
|
|
44
44
|
<% end %>
|
|
@@ -25,22 +25,39 @@
|
|
|
25
25
|
<%= SolidErrors::Occurrence.human_attribute_name(:backtrace) %>
|
|
26
26
|
</dt>
|
|
27
27
|
<dd class="">
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
<div class="flex gap-2 mb-3">
|
|
29
|
+
<button type="button"
|
|
30
|
+
data-backtrace-toggle="application"
|
|
31
|
+
data-occurrence-id="<%= occurrence.id %>"
|
|
32
|
+
class="backtrace-toggle inline-flex items-center border-b-2 px-3 py-2 text-sm font-medium cursor-pointer border-blue-500 text-blue-500">
|
|
33
|
+
Application
|
|
34
|
+
</button>
|
|
35
|
+
<button type="button"
|
|
36
|
+
data-backtrace-toggle="full"
|
|
37
|
+
data-occurrence-id="<%= occurrence.id %>"
|
|
38
|
+
class="backtrace-toggle inline-flex items-center border-b-2 px-3 py-2 text-sm font-medium cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700">
|
|
39
|
+
Full
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div id="backtrace-application-<%= occurrence.id %>">
|
|
44
|
+
<% if backtrace.application_lines.empty? %>
|
|
45
|
+
<p class="text-gray-500 text-sm">
|
|
46
|
+
<em>No application code in backtrace.</em>
|
|
47
|
+
<button type="button" data-backtrace-toggle="full" data-occurrence-id="<%= occurrence.id %>" class="text-blue-500 hover:underline cursor-pointer">
|
|
48
|
+
View full trace
|
|
49
|
+
</button>
|
|
50
|
+
</p>
|
|
51
|
+
<% else %>
|
|
52
|
+
<%= render partial: "solid_errors/occurrences/backtrace_line", collection: backtrace.application_lines, as: :line, locals: {open: true} %>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div id="backtrace-full-<%= occurrence.id %>" hidden>
|
|
57
|
+
<% backtrace.lines.each_with_index do |line, i| %>
|
|
58
|
+
<%= render "solid_errors/occurrences/backtrace_line", line: line, open: line.application? || i.zero? %>
|
|
42
59
|
<% end %>
|
|
43
|
-
|
|
60
|
+
</div>
|
|
44
61
|
</dd>
|
|
45
62
|
</div>
|
|
46
63
|
</dl>
|
data/lib/solid_errors/engine.rb
CHANGED
|
@@ -32,15 +32,18 @@ module SolidErrors
|
|
|
32
32
|
}
|
|
33
33
|
fingerprint = Digest::SHA256.hexdigest(error_attributes.values.join)
|
|
34
34
|
if (record = SolidErrors::Error.find_by(fingerprint: fingerprint))
|
|
35
|
+
opens_error = :reopened if record.resolved?
|
|
35
36
|
record.update!(resolved_at: nil, updated_at: Time.now)
|
|
36
37
|
else
|
|
38
|
+
opens_error = :created
|
|
37
39
|
record = SolidErrors::Error.create!(error_attributes.merge(fingerprint: fingerprint))
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
SolidErrors::Occurrence.create(
|
|
41
43
|
error_id: record.id,
|
|
42
44
|
backtrace: error.backtrace&.join("\n"),
|
|
43
|
-
context: s(context)
|
|
45
|
+
context: s(context),
|
|
46
|
+
opens_error: opens_error
|
|
44
47
|
)
|
|
45
48
|
end
|
|
46
49
|
|
|
@@ -49,7 +52,7 @@ module SolidErrors
|
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def ignore_by_class?(error_class_name)
|
|
52
|
-
IGNORED_ERRORS.any? do |ignored_class|
|
|
55
|
+
[*IGNORED_ERRORS, *SolidErrors.ignored_errors].any? do |ignored_class|
|
|
53
56
|
ignored_class_name = ignored_class.respond_to?(:name) ? ignored_class.name : ignored_class
|
|
54
57
|
|
|
55
58
|
ignored_class_name == error_class_name
|
data/lib/solid_errors/version.rb
CHANGED
data/lib/solid_errors.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "solid_errors/subscriber"
|
|
|
6
6
|
require_relative "solid_errors/engine"
|
|
7
7
|
|
|
8
8
|
module SolidErrors
|
|
9
|
+
mattr_accessor :enabled, default: true
|
|
9
10
|
mattr_accessor :connects_to
|
|
10
11
|
mattr_accessor :base_controller_class, default: "::ActionController::Base"
|
|
11
12
|
mattr_writer :username
|
|
@@ -14,7 +15,10 @@ module SolidErrors
|
|
|
14
15
|
mattr_accessor :email_from, default: "solid_errors@noreply.com"
|
|
15
16
|
mattr_accessor :email_to
|
|
16
17
|
mattr_accessor :email_subject_prefix
|
|
18
|
+
mattr_accessor :email_every_occurrence, default: true
|
|
17
19
|
mattr_accessor :destroy_after
|
|
20
|
+
mattr_accessor :ignored_errors, default: []
|
|
21
|
+
mattr_writer :url_helper_name, :url_options
|
|
18
22
|
|
|
19
23
|
class << self
|
|
20
24
|
# use method instead of attr_accessor to ensure
|
|
@@ -30,7 +34,41 @@ module SolidErrors
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def send_emails?
|
|
33
|
-
send_emails && email_to.present?
|
|
37
|
+
send_emails && email_to.present? && !ActiveSupport::IsolatedExecutionState[:solid_errors_without_emails]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Errors reported inside the block are recorded but never emailed
|
|
41
|
+
def without_emails
|
|
42
|
+
previous = ActiveSupport::IsolatedExecutionState[:solid_errors_without_emails]
|
|
43
|
+
ActiveSupport::IsolatedExecutionState[:solid_errors_without_emails] = true
|
|
44
|
+
yield
|
|
45
|
+
ensure
|
|
46
|
+
ActiveSupport::IsolatedExecutionState[:solid_errors_without_emails] = previous
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def url_helper_name
|
|
50
|
+
@url_helper_name ||= detect_url_helper_name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def url_options
|
|
54
|
+
@url_options ||= detect_url_options
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def find_solid_errors_route
|
|
60
|
+
@solid_errors_route ||= Rails.application.routes.routes.find do |r|
|
|
61
|
+
r.name&.to_s&.end_with?("solid_errors")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def detect_url_helper_name
|
|
66
|
+
find_solid_errors_route&.name&.to_sym || :solid_errors
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def detect_url_options
|
|
70
|
+
subdomain = find_solid_errors_route&.constraints&.dig(:subdomain)
|
|
71
|
+
subdomain.is_a?(String) ? {subdomain: subdomain} : {}
|
|
34
72
|
end
|
|
35
73
|
end
|
|
36
74
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_errors
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Margheim
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: actionpack
|
|
@@ -15,70 +15,70 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '7.
|
|
18
|
+
version: '7.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '7.
|
|
25
|
+
version: '7.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: actionview
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '7.
|
|
32
|
+
version: '7.1'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '7.
|
|
39
|
+
version: '7.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: activerecord
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '7.
|
|
46
|
+
version: '7.1'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '7.
|
|
53
|
+
version: '7.1'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: activesupport
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '7.
|
|
60
|
+
version: '7.1'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '7.
|
|
67
|
+
version: '7.1'
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: railties
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '7.
|
|
74
|
+
version: '7.1'
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '7.
|
|
81
|
+
version: '7.1'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: sqlite3
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -109,7 +109,7 @@ files:
|
|
|
109
109
|
- app/models/solid_errors/error.rb
|
|
110
110
|
- app/models/solid_errors/occurrence.rb
|
|
111
111
|
- app/models/solid_errors/record.rb
|
|
112
|
-
- app/views/layouts/solid_errors/_style.html
|
|
112
|
+
- app/views/layouts/solid_errors/_style.html.erb
|
|
113
113
|
- app/views/layouts/solid_errors/application.html.erb
|
|
114
114
|
- app/views/solid_errors/error_mailer/error_occurred.html.erb
|
|
115
115
|
- app/views/solid_errors/error_mailer/error_occurred.text.erb
|
|
@@ -120,6 +120,7 @@ files:
|
|
|
120
120
|
- app/views/solid_errors/errors/_row.html.erb
|
|
121
121
|
- app/views/solid_errors/errors/index.html.erb
|
|
122
122
|
- app/views/solid_errors/errors/show.html.erb
|
|
123
|
+
- app/views/solid_errors/occurrences/_backtrace_line.html.erb
|
|
123
124
|
- app/views/solid_errors/occurrences/_collection.html.erb
|
|
124
125
|
- app/views/solid_errors/occurrences/_occurrence.html.erb
|
|
125
126
|
- config/locales/en.yml
|
|
@@ -145,14 +146,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
145
146
|
requirements:
|
|
146
147
|
- - ">="
|
|
147
148
|
- !ruby/object:Gem::Version
|
|
148
|
-
version: 2.
|
|
149
|
+
version: 3.2.0
|
|
149
150
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
150
151
|
requirements:
|
|
151
152
|
- - ">="
|
|
152
153
|
- !ruby/object:Gem::Version
|
|
153
154
|
version: '0'
|
|
154
155
|
requirements: []
|
|
155
|
-
rubygems_version:
|
|
156
|
+
rubygems_version: 4.0.16
|
|
156
157
|
specification_version: 4
|
|
157
158
|
summary: Database-backed Rails error subscriber
|
|
158
159
|
test_files: []
|