├── .github
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .standard.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── UPGRADE.md
├── app
├── controllers
│ └── solid_errors
│ │ ├── application_controller.rb
│ │ └── errors_controller.rb
├── mailers
│ └── solid_errors
│ │ └── error_mailer.rb
├── models
│ └── solid_errors
│ │ ├── backtrace.rb
│ │ ├── backtrace_line.rb
│ │ ├── error.rb
│ │ ├── occurrence.rb
│ │ └── record.rb
└── views
│ ├── layouts
│ └── solid_errors
│ │ ├── _style.html
│ │ └── application.html.erb
│ └── solid_errors
│ ├── error_mailer
│ ├── error_occurred.html.erb
│ └── error_occurred.text.erb
│ ├── errors
│ ├── _actions.html.erb
│ ├── _delete_button.html.erb
│ ├── _error.html.erb
│ ├── _resolve_button.html.erb
│ ├── _row.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ └── occurrences
│ ├── _collection.html.erb
│ └── _occurrence.html.erb
├── bin
├── console
└── setup
├── config
├── locales
│ └── en.yml
└── routes.rb
├── images
├── index-screenshot.png
└── show-screenshot.png
├── lib
├── generators
│ └── solid_errors
│ │ └── install
│ │ ├── USAGE
│ │ ├── install_generator.rb
│ │ └── templates
│ │ └── db
│ │ └── errors_schema.rb
├── solid_errors.rb
└── solid_errors
│ ├── engine.rb
│ ├── sanitizer.rb
│ ├── subscriber.rb
│ └── version.rb
├── sig
└── solid_errors.rbs
├── solid_errors.gemspec
└── test
├── dummy
├── app
│ ├── controllers
│ │ └── application_controller.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ ├── models
│ │ └── application_record.rb
│ └── views
│ │ └── layouts
│ │ └── application.html.erb
├── bin
│ ├── rails
│ ├── rake
│ └── setup
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ └── test.rb
│ ├── locales
│ │ └── en.yml
│ ├── puma.rb
│ └── routes.rb
├── db
│ ├── errors.sqlite3
│ ├── queue.sqlite3
│ └── test.sqlite3
├── log
│ └── .keep
├── storage
│ ├── .keep
│ └── test.sqlite3
└── tmp
│ ├── .keep
│ ├── local_secret.txt
│ ├── pids
│ └── .keep
│ └── storage
│ └── .keep
├── test_helper.rb
└── test_solid_errors.rb
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Ruby
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | name: Ruby ${{ matrix.ruby }}
14 | strategy:
15 | matrix:
16 | ruby:
17 | - '3.1.4'
18 | - '3.2.3'
19 | - '3.3.0'
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Set up Ruby
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: ${{ matrix.ruby }}
26 | bundler-cache: true
27 | - name: Run the default task
28 | run: bundle exec rake
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | .DS_Store
10 | test/dummy/log/*.log
11 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | # For available configuration options, see:
2 | # https://github.com/testdouble/standard
3 | ruby_version: 3.0
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [Unreleased]
2 |
3 | ## [0.6.1] - 2024-09-19
4 |
5 | - Fix the install generator by putting the schema in the db/ directory ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/62))
6 |
7 | ## [0.6.0] - 2024-09-09
8 |
9 | - Slim down installer and have it use a final schema instead of migrations ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/60))
10 | - Fix rails 8 warning: "Drawing a route with a hash key name is deprecated" ([@dorianmariecom](https://github.com/fractaledmind/solid_errors/pull/59))
11 |
12 | ## [0.5.0] - 2024-08-22
13 |
14 | - introduce ability to view resolved errors and delete them ([@acoffman](https://github.com/fractaledmind/solid_errors/pull/56))
15 | - loosen Rails dependency constraints to allow for Rails 8 ([@dorianmariecom](https://github.com/fractaledmind/solid_errors/pull/58))
16 | - update README.md with rails error reporting example ([@stillhart](https://github.com/fractaledmind/solid_errors/pull/57))
17 |
18 | ## [0.4.3] - 2024-06-21
19 |
20 | - fix `SolidErrors.send_emails?` that always returns true ([@defkode](https://github.com/fractaledmind/solid_errors/pull/46))
21 | - Highlight source line ([@Bhacaz](https://github.com/fractaledmind/solid_errors/pull/47))
22 | - Latest occurrences first ([@Bhacaz](https://github.com/fractaledmind/solid_errors/pull/48))
23 | - Fix partial not being found with different inflector [@emilioeduardob](https://github.com/fractaledmind/solid_errors/pull/50)
24 |
25 | ## [0.4.2] - 2024-04-10
26 |
27 | - Fix bug in error page when using Postgres ([@mintuhouse](https://github.com/fractaledmind/solid_errors/pull/43))
28 |
29 | ## [0.4.1] - 2024-04-09
30 |
31 | - Ensure only first occurrence is open on error page ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/42))
32 |
33 | ## [0.4.0] - 2024-04-09
34 |
35 | - Add email notifications ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/3))
36 | - Add `fingerprint` column to `SolidErrors::Error` model ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/10))
37 | - read more about the upgrade process in the [UPGRADE.md](./UPGRADE.md) file
38 | - Paginate error occurrences ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/39))
39 | - Ensure footer sticks to bottom of page ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/40))
40 | - Fix `errors/index` view ([@dorianmariecom](https://github.com/fractaledmind/solid_errors/pull/25))
41 | - Fix `occurrences/_occurrence` partial ([@fractaledmind](https://github.com/fractaledmind/solid_errors/pull/28))
42 | - Add documentation on ejecting views ([@dorianmariecom](https://github.com/fractaledmind/solid_errors/pull/30))
43 | - Only declare necessary Rails sub-systems as dependencies ([@everton](https://github.com/fractaledmind/solid_errors/pull/35))
44 | - Force :en locale inside ErrorsController to avoid missing translations ([@everton](https://github.com/fractaledmind/solid_errors/pull/36))
45 | - Avoid triggering N+1 on index page ([luizkowalski](https://github.com/fractaledmind/solid_errors/pull/38))
46 |
47 | ## [0.3.5] - 2024-02-06
48 |
49 | - Fix issue with `gsub!` on a frozen string ([@joelmoss](https://github.com/fractaledmind/solid_errors/pull/9))
50 |
51 | ## [0.3.4] - 2024-01-29
52 |
53 | - Ensure that setting username/password with Ruby triggers the basic authentication
54 |
55 | ## [0.3.3] - 2024-01-28
56 |
57 | - Properly fix the setup issues
58 | - Add the needed locale file
59 |
60 | ## [0.3.2] - 2024-01-28
61 |
62 | - Fix belongs_to reference in Occurrence model
63 |
64 | ## [0.3.1] - 2024-01-28
65 |
66 | - Fix incorrect table reference name and long index name in the migration template
67 |
68 | ## [0.3.0] - 2024-01-28
69 |
70 | - Proper release with full coherent functionality and documentation
71 |
72 | ## [0.2.0] - 2024-01-14
73 |
74 | - Initial release
75 |
76 | ## [0.1.0] - 2024-01-14
77 |
78 | - Reserve gem name
79 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | * Demonstrating empathy and kindness toward other people
14 | * Being respectful of differing opinions, viewpoints, and experiences
15 | * Giving and gracefully accepting constructive feedback
16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | * Focusing on what is best not just for us as individuals, but for the overall community
18 |
19 | Examples of unacceptable behavior include:
20 |
21 | * The use of sexualized language or imagery, and sexual attention or
22 | advances of any kind
23 | * Trolling, insulting or derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or email
26 | address, without their explicit permission
27 | * Other conduct which could reasonably be considered inappropriate in a
28 | professional setting
29 |
30 | ## Enforcement Responsibilities
31 |
32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33 |
34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35 |
36 | ## Scope
37 |
38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39 |
40 | ## Enforcement
41 |
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at stephen_margheim@epam.com. All complaints will be reviewed and investigated promptly and fairly.
43 |
44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45 |
46 | ## Enforcement Guidelines
47 |
48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49 |
50 | ### 1. Correction
51 |
52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 |
54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55 |
56 | ### 2. Warning
57 |
58 | **Community Impact**: A violation through a single incident or series of actions.
59 |
60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61 |
62 | ### 3. Temporary Ban
63 |
64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65 |
66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67 |
68 | ### 4. Permanent Ban
69 |
70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71 |
72 | **Consequence**: A permanent ban from any sort of public interaction within the community.
73 |
74 | ## Attribution
75 |
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78 |
79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80 |
81 | [homepage]: https://www.contributor-covenant.org
82 |
83 | For answers to common questions about this code of conduct, see the FAQ at
84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in solid_errors.gemspec
6 | gemspec
7 |
8 | gem "rake", "~> 13.0"
9 |
10 | gem "minitest", "~> 5.0"
11 |
12 | gem "standard", "~> 1.3"
13 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | solid_errors (0.6.1)
5 | actionmailer (>= 7.0)
6 | actionpack (>= 7.0)
7 | actionview (>= 7.0)
8 | activerecord (>= 7.0)
9 | activesupport (>= 7.0)
10 | railties (>= 7.0)
11 |
12 | GEM
13 | remote: https://rubygems.org/
14 | specs:
15 | actionmailer (7.2.0)
16 | actionpack (= 7.2.0)
17 | actionview (= 7.2.0)
18 | activejob (= 7.2.0)
19 | activesupport (= 7.2.0)
20 | mail (>= 2.8.0)
21 | rails-dom-testing (~> 2.2)
22 | actionpack (7.2.0)
23 | actionview (= 7.2.0)
24 | activesupport (= 7.2.0)
25 | nokogiri (>= 1.8.5)
26 | racc
27 | rack (>= 2.2.4, < 3.2)
28 | rack-session (>= 1.0.1)
29 | rack-test (>= 0.6.3)
30 | rails-dom-testing (~> 2.2)
31 | rails-html-sanitizer (~> 1.6)
32 | useragent (~> 0.16)
33 | actionview (7.2.0)
34 | activesupport (= 7.2.0)
35 | builder (~> 3.1)
36 | erubi (~> 1.11)
37 | rails-dom-testing (~> 2.2)
38 | rails-html-sanitizer (~> 1.6)
39 | activejob (7.2.0)
40 | activesupport (= 7.2.0)
41 | globalid (>= 0.3.6)
42 | activemodel (7.2.0)
43 | activesupport (= 7.2.0)
44 | activerecord (7.2.0)
45 | activemodel (= 7.2.0)
46 | activesupport (= 7.2.0)
47 | timeout (>= 0.4.0)
48 | activesupport (7.2.0)
49 | base64
50 | bigdecimal
51 | concurrent-ruby (~> 1.0, >= 1.3.1)
52 | connection_pool (>= 2.2.5)
53 | drb
54 | i18n (>= 1.6, < 2)
55 | logger (>= 1.4.2)
56 | minitest (>= 5.1)
57 | securerandom (>= 0.3)
58 | tzinfo (~> 2.0, >= 2.0.5)
59 | ast (2.4.2)
60 | base64 (0.2.0)
61 | bigdecimal (3.1.8)
62 | builder (3.3.0)
63 | concurrent-ruby (1.3.4)
64 | connection_pool (2.4.1)
65 | crass (1.0.6)
66 | date (3.3.4)
67 | drb (2.2.1)
68 | erubi (1.13.0)
69 | globalid (1.2.1)
70 | activesupport (>= 6.1)
71 | i18n (1.14.5)
72 | concurrent-ruby (~> 1.0)
73 | io-console (0.7.2)
74 | irb (1.14.0)
75 | rdoc (>= 4.0.0)
76 | reline (>= 0.4.2)
77 | json (2.7.2)
78 | language_server-protocol (3.17.0.3)
79 | lint_roller (1.1.0)
80 | logger (1.6.0)
81 | loofah (2.22.0)
82 | crass (~> 1.0.2)
83 | nokogiri (>= 1.12.0)
84 | mail (2.8.1)
85 | mini_mime (>= 0.1.1)
86 | net-imap
87 | net-pop
88 | net-smtp
89 | mini_mime (1.1.5)
90 | minitest (5.25.1)
91 | net-imap (0.4.14)
92 | date
93 | net-protocol
94 | net-pop (0.1.2)
95 | net-protocol
96 | net-protocol (0.2.2)
97 | timeout
98 | net-smtp (0.5.0)
99 | net-protocol
100 | nokogiri (1.16.7-arm64-darwin)
101 | racc (~> 1.4)
102 | nokogiri (1.16.7-x86_64-linux)
103 | racc (~> 1.4)
104 | parallel (1.26.3)
105 | parser (3.3.4.2)
106 | ast (~> 2.4.1)
107 | racc
108 | psych (5.1.2)
109 | stringio
110 | racc (1.8.1)
111 | rack (3.1.7)
112 | rack-session (2.0.0)
113 | rack (>= 3.0.0)
114 | rack-test (2.1.0)
115 | rack (>= 1.3)
116 | rackup (2.1.0)
117 | rack (>= 3)
118 | webrick (~> 1.8)
119 | rails-dom-testing (2.2.0)
120 | activesupport (>= 5.0.0)
121 | minitest
122 | nokogiri (>= 1.6)
123 | rails-html-sanitizer (1.6.0)
124 | loofah (~> 2.21)
125 | nokogiri (~> 1.14)
126 | railties (7.2.0)
127 | actionpack (= 7.2.0)
128 | activesupport (= 7.2.0)
129 | irb (~> 1.13)
130 | rackup (>= 1.0.0)
131 | rake (>= 12.2)
132 | thor (~> 1.0, >= 1.2.2)
133 | zeitwerk (~> 2.6)
134 | rainbow (3.1.1)
135 | rake (13.2.1)
136 | rdoc (6.7.0)
137 | psych (>= 4.0.0)
138 | regexp_parser (2.9.2)
139 | reline (0.5.9)
140 | io-console (~> 0.5)
141 | rexml (3.3.6)
142 | strscan
143 | rubocop (1.65.1)
144 | json (~> 2.3)
145 | language_server-protocol (>= 3.17.0)
146 | parallel (~> 1.10)
147 | parser (>= 3.3.0.2)
148 | rainbow (>= 2.2.2, < 4.0)
149 | regexp_parser (>= 2.4, < 3.0)
150 | rexml (>= 3.2.5, < 4.0)
151 | rubocop-ast (>= 1.31.1, < 2.0)
152 | ruby-progressbar (~> 1.7)
153 | unicode-display_width (>= 2.4.0, < 3.0)
154 | rubocop-ast (1.32.1)
155 | parser (>= 3.3.1.0)
156 | rubocop-performance (1.21.1)
157 | rubocop (>= 1.48.1, < 2.0)
158 | rubocop-ast (>= 1.31.1, < 2.0)
159 | ruby-progressbar (1.13.0)
160 | securerandom (0.3.1)
161 | sqlite3 (2.0.4-arm64-darwin)
162 | sqlite3 (2.0.4-x86_64-linux-gnu)
163 | standard (1.40.0)
164 | language_server-protocol (~> 3.17.0.2)
165 | lint_roller (~> 1.0)
166 | rubocop (~> 1.65.0)
167 | standard-custom (~> 1.0.0)
168 | standard-performance (~> 1.4)
169 | standard-custom (1.0.2)
170 | lint_roller (~> 1.0)
171 | rubocop (~> 1.50)
172 | standard-performance (1.4.0)
173 | lint_roller (~> 1.1)
174 | rubocop-performance (~> 1.21.0)
175 | stringio (3.1.1)
176 | strscan (3.1.0)
177 | thor (1.3.1)
178 | timeout (0.4.1)
179 | tzinfo (2.0.6)
180 | concurrent-ruby (~> 1.0)
181 | unicode-display_width (2.5.0)
182 | useragent (0.16.10)
183 | webrick (1.8.1)
184 | zeitwerk (2.6.17)
185 |
186 | PLATFORMS
187 | arm64-darwin
188 | x86_64-linux
189 |
190 | DEPENDENCIES
191 | minitest (~> 5.0)
192 | rake (~> 13.0)
193 | solid_errors!
194 | sqlite3
195 | standard (~> 1.3)
196 |
197 | BUNDLED WITH
198 | 2.5.17
199 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Stephen Margheim
4 | Copyright (c) 2024 Honeybadger Industries LLC
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Solid Errors
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 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 |
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 |
35 | ## Installation
36 |
37 | Install the gem and add to the application's Gemfile by executing:
38 | ```bash
39 | $ bundle add solid_errors
40 | ```
41 |
42 | If bundler is not being used to manage dependencies, install the gem by executing:
43 | ```bash
44 | $ gem install solid_errors
45 | ```
46 |
47 | After installing the gem, run the installer:
48 | ```bash
49 | $ rails generate solid_errors:install
50 | ```
51 |
52 | This will create the `db/errors_schema.rb` file.
53 |
54 | You will then have to add the configuration for the errors database in `config/database.yml`. If you're using sqlite, it'll look something like this:
55 |
56 | ```yaml
57 | production:
58 | primary:
59 | <<: *default
60 | database: storage/production.sqlite3
61 | errors:
62 | <<: *default
63 | database: storage/production_errors.sqlite3
64 | migrations_paths: db/errors_migrate
65 | ```
66 |
67 | ...or if you're using MySQL/PostgreSQL/Trilogy:
68 |
69 | ```yaml
70 | production:
71 | primary: &primary_production
72 | <<: *default
73 | database: app_production
74 | username: app
75 | password: <%= ENV["APP_DATABASE_PASSWORD"] %>
76 | errors:
77 | <<: *primary_production
78 | database: app_production_errors
79 | migrations_paths: db/errors_migrate
80 | ```
81 |
82 | > [!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`.
84 |
85 | Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
86 |
87 | Then mount the engine in your `config/routes.rb` file:
88 | ```ruby
89 | authenticate :user, -> (user) { user.admin? } do
90 | mount SolidErrors::Engine, at: "/solid_errors"
91 | end
92 | ```
93 |
94 | > [!NOTE]
95 | > Be sure to [secure the dashboard](#authentication) in production.
96 |
97 | ## Usage
98 |
99 | All exceptions are recorded automatically. No additional code required.
100 |
101 | Please consult the [official guides](https://guides.rubyonrails.org/error_reporting.html) for an introduction to the error reporting API.
102 |
103 | There are intentionally few features; you can view and resolve errors. That’s it. The goal is to provide a simple, lightweight, and performant solution for tracking exceptions in your Rails application. If you need more features, you should probably use a 3rd party service like [Honeybadger](https://www.honeybadger.io/), whose MIT-licensed [Ruby agent gem](https://github.com/honeybadger-io/honeybadger-ruby) provided a couple of critical pieces of code for this project.
104 |
105 | ### Manually reporting an Error
106 |
107 | Errors can be added to Solid Errors via the [Rails error reporter](https://guides.rubyonrails.org/error_reporting.html).
108 |
109 | There are [three ways](https://guides.rubyonrails.org/error_reporting.html#using-the-error-reporter) you can use the error reporter:
110 |
111 | `Rails.error.handle` will report any error raised within the block. It will then swallow the error, and the rest of your code outside the block will continue as normal.
112 |
113 | ```ruby
114 | result = Rails.error.handle do
115 | 1 + '1' # raises TypeError
116 | end
117 | result # => nil
118 | 1 + 1 # This will be executed
119 | ```
120 |
121 | `Rails.error.record` will report errors to all registered subscribers and then re-raise the error, meaning that the rest of your code won't execute.
122 |
123 | ```ruby
124 | Rails.error.record do
125 | 1 + '1' # raises TypeError
126 | end
127 | 1 + 1 # This won't be executed
128 | ```
129 |
130 | You can also manually report errors by calling `Rails.error.report`:
131 |
132 | ```ruby
133 | begin
134 | # code
135 | rescue StandardError => e
136 | Rails.error.report(e)
137 | end
138 | ```
139 |
140 | All 3 reporting APIs (`#handle`, `#record`, and `#report`) support the following options, which are then passed along to all registered subscribers:
141 |
142 | * `handled`: a Boolean to indicate if the error was handled. This is set to `true` by default. `#record` sets this to `false`.
143 | * `severity`: a Symbol describing the severity of the error. Expected values are: `:error`, `:warning`, and `:info`. `#handle` sets this to `:warning`, while `#record` sets it to `:error`.
144 | * `context`: a Hash to provide more context about the error, like request or user details
145 | * `source`: a String about the source of the error. The default source is `"application"`. Errors reported by internal libraries may set other sources; the Redis cache library may use "redis_cache_store.active_support", for instance. Your subscriber can use the source to ignore errors you aren't interested in.
146 |
147 | ```ruby
148 | Rails.error.handle(context: { user_id: user.id }, severity: :info) do
149 | # ...
150 | end
151 | ```
152 |
153 | ### Configuration
154 |
155 | You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently, 6 configuration options are available:
156 |
157 | * `connects_to` - The database configuration to use for the Solid Errors database. See [Database Configuration](#database-configuration) for more information.
158 | * `username` - The username to use for HTTP authentication. See [Authentication](#authentication) for more information.
159 | * `password` - The password to use for HTTP authentication. See [Authentication](#authentication) for more information.
160 | * `sends_email` - Whether or not to send emails when an error occurs. See [Email notifications](#email-notifications) for more information.
161 | * `email_from` - The email address to send a notification from. See [Email notifications](#email-notifications) for more information.
162 | * `email_to` - The email address(es) to send a notification to. See [Email notifications](#email-notifications) for more information.
163 | * `email_subject_prefix` - Prefix added to the subject line for email notifications. See [Email notifications](#email-notifications) for more information.
164 |
165 | ### Database Configuration
166 |
167 | `config.solid_errors.connects_to` takes a custom database configuration hash that will be used in the abstract `SolidErrors::Record` Active Record model. This is required to use a different database than the main app ([but the primary database can also be used](#single-database-configuration)). For example:
168 |
169 | ```ruby
170 | # Use a single separate DB for Solid Errors
171 | config.solid_errors.connects_to = { database: { writing: :solid_errors, reading: :solid_errors } }
172 | ```
173 |
174 | or
175 |
176 | ```ruby
177 | # Use a separate primary/replica pair for Solid Errors
178 | config.solid_errors.connects_to = { database: { writing: :solid_errors_primary, reading: :solid_errors_replica } }
179 | ```
180 |
181 | #### Single Database Configuration
182 |
183 | Running Solid Errors in a separate database is recommended, but it's also possible to use one single database for both the app and the errors. Just follow these steps to add errors to the primary database:
184 |
185 | 1. Copy the contents of `db/errors_schema.rb` into a normal migration and delete `db/errors_schema.rb`
186 | 2. Remove `config.solid_errors.connects_to` from your configuration files.
187 | 3. Migrate your database.
188 |
189 | You won't have multiple databases, so `database.yml` doesn't need to have the errors database configuration.
190 |
191 | #### Authentication
192 |
193 | 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.
194 |
195 | There are two ways to setup a username and password. First, you can use the `SOLIDERRORS_USERNAME` and `SOLIDERRORS_PASSWORD` environment variables:
196 |
197 | ```ruby
198 | ENV["SOLIDERRORS_USERNAME"] = "frodo"
199 | ENV["SOLIDERRORS_PASSWORD"] = "ikeptmysecrets"
200 | ```
201 |
202 | Second, you can set the `SolidErrors.username` and `SolidErrors.password` variables in an initializer:
203 |
204 | ```ruby
205 | # Set authentication credentials for Solid Errors
206 | config.solid_errors.username = Rails.application.credentials.solid_errors.username
207 | config.solid_errors.password = Rails.application.credentials.solid_errors.password
208 | ```
209 |
210 | Either way, if you have set a username and password, Solid Errors will use basic HTTP authentication. If you have not set a username and password, Solid Errors will not require any authentication to view the dashboard.
211 |
212 | If you use Devise for authentication in your app, you can also restrict access to the dashboard by using their `authenticate` constraint in your routes file:
213 |
214 | ```ruby
215 | authenticate :user, -> (user) { user.admin? } do
216 | mount SolidErrors::Engine, at: "/solid_errors"
217 | end
218 | ```
219 |
220 | #### Email notifications
221 |
222 | Solid Errors _can_ send email notifications whenever an error occurs, if your application has ActionMailer already properly setup to send emails. However, in order to activate this feature you must define the email address(es) to send the notifications to. Optionally, you can also define the email address to send the notifications from (useful if your email provider only allows emails to be sent from a predefined list of addresses) or simply turn off this feature altogether. You can also define a subject prefix for the email notifications to quickly identify the source of the error.
223 |
224 | There are two ways to configure email notifications. First, you can use environment variables:
225 |
226 | ```ruby
227 | ENV["SOLIDERRORS_SEND_EMAILS"] = true # defaults to false
228 | ENV["SOLIDERRORS_EMAIL_FROM"] = "errors@myapp.com" # defaults to "solid_errors@noreply.com"
229 | ENV["SOLIDERRORS_EMAIL_TO"] = "devs@myapp.com" # no default, must be set
230 | ENV["SOLIDERRORS_EMAIL_SUBJECT_PREFIX"] = "[Application name][Environment]" # no default, optional
231 | ```
232 |
233 | Second, you can set the values via the configuration object:
234 |
235 | ```ruby
236 | # Set authentication credentials and optional subject prefix for Solid Errors
237 | config.solid_errors.send_emails = true
238 | config.solid_errors.email_from = "errors@myapp.com"
239 | config.solid_errors.email_to = "devs@myapp.com"
240 | config.solid_errors.email_subject_prefix = "[#{Rails.application.name}][#{Rails.env}]"
241 | ```
242 |
243 | 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.
244 |
245 | ### Examples
246 |
247 | There are only two screens in the dashboard.
248 |
249 | * the index view of all unresolved errors:
250 |
251 | 
252 |
253 | * and the show view of a particular error:
254 |
255 | 
256 |
257 | ### Usage with API-only Applications
258 |
259 | If your Rails application is an API-only application (generated with the `rails new --api` command), you will need to add the following middleware to your `config/application.rb` file in order to use the dashboard UI provided by Solid Errors:
260 |
261 | ```ruby
262 | # /config/application.rb
263 | config.middleware.use ActionDispatch::Cookies
264 | config.middleware.use ActionDispatch::Session::CookieStore
265 | config.middleware.use ActionDispatch::Flash
266 | ```
267 |
268 | ### Overwriting the views
269 |
270 | You can find the views in [`app/views`](https://github.com/fractaledmind/solid_errors/tree/main/app/views).
271 |
272 | ```bash
273 | app/views/
274 | ├── layouts
275 | │ └── solid_errors
276 | │ ├── _style.html
277 | │ └── application.html.erb
278 | └── solid_errors
279 | ├── error_mailer
280 | │ ├── error_occurred.html.erb
281 | │ └── error_occurred.text.erb
282 | ├── errors
283 | │ ├── _actions.html.erb
284 | │ ├── _error.html.erb
285 | │ ├── _row.html.erb
286 | │ ├── index.html.erb
287 | │ └── show.html.erb
288 | └── occurrences
289 | ├── _collection.html.erb
290 | └── _occurrence.html.erb
291 | ```
292 |
293 | You can always take control of the views by creating your own views and/or partials at these paths in your application. For example, if you wanted to overwrite the application layout, you could create a file at `app/views/layouts/solid_errors/application.html.erb`. If you wanted to remove the footer and the automatically disappearing flash messages, as one concrete example, you could define that file as:
294 |
295 | ```erb
296 |
297 |
298 |
299 | Solid Errors
300 | <%= csrf_meta_tags %>
301 | <%= csp_meta_tag %>
302 |
303 | <%= render "layouts/solid_errors/style" %>
304 |
305 |
306 |
307 | <%= content_for?(:content) ? yield(:content) : yield %>
308 |
309 |
310 |
311 | <% if notice.present? %>
312 |
313 | <%= notice %>
314 |
315 | <% end %>
316 |
317 | <% if alert.present? %>
318 |
319 | <%= alert %>
320 |
321 | <% end %>
322 |
323 |
324 |
325 | ```
326 |
327 | ## Sponsors
328 |
329 |
330 | Proudly sponsored by
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 | ## Development
339 |
340 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
341 |
342 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
343 |
344 | ## Contributing
345 |
346 | Bug reports and pull requests are welcome on GitHub at https://github.com/fractaledmind/solid_errors. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/fractaledmind/solid_errors/blob/main/CODE_OF_CONDUCT.md).
347 |
348 | ## License
349 |
350 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
351 |
352 | ## Code of Conduct
353 |
354 | 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).
355 |
356 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rake/testtask"
5 |
6 | Rake::TestTask.new(:test) do |t|
7 | t.libs << "test"
8 | t.libs << "lib"
9 | t.test_files = FileList["test/**/test_*.rb"]
10 | end
11 |
12 | require "standard/rake"
13 |
14 | task default: %i[test standard]
15 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # Solid Errors Upgrade Guide
2 |
3 | Follow this guide to upgrade your Solid Errors implementation to the next version
4 |
5 | ## Solid Errors 0.4.0
6 |
7 | We've added a `fingerprint` column to the `solid_errors` table and changed the `exception_class`, `message`, `severity`, and `source` columns to be limitless `text` type columns. This allows the unique index on the table to be on the single `fingerprint` column, which is a SHA256 hash of the `exception_class`, `message`, `severity`, and `source` columns. This change resolves problems with the unique index being too large as well as problems with one of the data columns being truncated. But, it requires a migration to update the `solid_errors` table.
8 |
9 | Create a migration for whatever database stores your errors:
10 | ```bash
11 | rails generate migration UpgradeSolidErrors --database {name_of_errors_database}
12 | ```
13 |
14 | Then, update the migration file with the following code:
15 | ```ruby
16 | class UpgradeSolidErrors < ActiveRecord::Migration[7.1]
17 | def up
18 | change_column :solid_errors, :exception_class, :text, null: false, limit: nil
19 | change_column :solid_errors, :message, :text, null: false, limit: nil
20 | change_column :solid_errors, :severity, :text, null: false, limit: nil
21 | change_column :solid_errors, :source, :text, null: true, limit: nil
22 | add_column :solid_errors, :fingerprint, :string, limit: 64
23 | add_index :solid_errors, :fingerprint, unique: true
24 | remove_index :solid_errors, [:exception_class, :message, :severity, :source], unique: true
25 | end
26 |
27 | def down
28 | change_column :solid_errors, :exception_class, :string, null: false, limit: 200
29 | change_column :solid_errors, :message, :string, null: false, limit: nil
30 | change_column :solid_errors, :severity, :string, null: false, limit: 25
31 | change_column :solid_errors, :source, :string, null: true, limit: nil
32 | remove_index :solid_errors, [:fingerprint], unique: true
33 | remove_column :solid_errors, :fingerprint, :string, limit: 64
34 | add_index :solid_errors, [:exception_class, :message, :severity, :source], unique: true
35 | end
36 | end
37 | ```
38 |
39 | Then, run this migration:
40 | ```bash
41 | rails db:migrate:{name_of_errors_database}
42 | ```
43 |
44 | Once the migration is complete, you will next need to fingerprint any existing errors in your database. This can be done using the following Ruby script, which can be put in a Rake task, a data migration, or simply done in the console:
45 | ```ruby
46 | SolidErrors::Error.where(fingerprint: nil).find_each do |error|
47 | error_attributes = error.attributes.slice('exception_class', 'message', 'severity', 'source')
48 | fingerprint = Digest::SHA256.hexdigest(error_attributes.values.join)
49 | error.update_attribute(:fingerprint, fingerprint)
50 | end
51 | ```
52 |
53 | You will need to run this script _as soon as the schema migration_ is complete so that you can fingerprint all existing errors before new errors with pre-generated fingerprints are recorded.
54 |
55 | Once you have migrated all existing errors to include a fingerprint, the final step is to run one more schema migration to mark the `fingerprint` column as non-nullable. You can generate a migration for this with the following command:
56 | ```bash
57 | rails generate migration SolidErrorFingerprintNonNullable --database {name_of_errors_database}
58 | ```
59 |
60 | Then, update the migration file with the following code:
61 | ```ruby
62 | class SolidErrorFingerprintNonNullable < ActiveRecord::Migration[7.1]
63 | def change
64 | change_column_null :solid_errors, :fingerprint, false
65 | end
66 | end
67 | ```
68 |
69 | Once this migration has been successfully run in production, your upgrade to Solid Errors 0.4.0 is complete!
70 |
--------------------------------------------------------------------------------
/app/controllers/solid_errors/application_controller.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | class ApplicationController < ActionController::Base
3 | protect_from_forgery with: :exception
4 |
5 | http_basic_authenticate_with name: SolidErrors.username, password: SolidErrors.password if SolidErrors.password
6 |
7 | # adapted from: https://github.com/ddnexus/pagy/blob/master/gem/lib/pagy.rb
8 | OverflowError = Class.new(StandardError)
9 | class Page
10 | attr_reader :count, :items, :pages, :first, :last, :prev, :next, :offset, :from, :to
11 |
12 | def initialize(collection, params)
13 | @count = collection.count
14 | @items = (params[:items] || 20).to_i
15 | @pages = [(@count.to_f / @items).ceil, 1].max
16 | @page = ((page = (params[:page] || 1).to_i) > @pages) ? @pages : page
17 | @first = (1 unless @page == 1)
18 | @last = (@pages unless @page == @pages)
19 | @prev = (@page - 1 unless @page == 1)
20 | @next = (@page == @pages) ? nil : @page + 1
21 | @offset = (@items * (@page - 1))
22 | @from = [@offset + 1, @count].min
23 | @to = [@offset + @items, @count].min
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/controllers/solid_errors/errors_controller.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | class ErrorsController < ApplicationController
3 | around_action :force_english_locale!
4 |
5 | before_action :set_error, only: %i[show update destroy]
6 |
7 | helper_method :error_scope
8 |
9 | # GET /errors
10 | def index
11 | errors_table = Error.arel_table
12 | occurrences_table = Occurrence.arel_table
13 | query_scope = error_scope.resolved? ? Error.resolved : Error.unresolved
14 | @errors = query_scope
15 | .joins(:occurrences)
16 | .select(errors_table[Arel.star],
17 | occurrences_table[:created_at].maximum.as("recent_occurrence"),
18 | occurrences_table[:id].count.as("occurrences_count"))
19 | .group(errors_table[:id])
20 | .order(recent_occurrence: :desc)
21 | end
22 |
23 | # GET /errors/1
24 | def show
25 | @page = Page.new(@error.occurrences, params)
26 | @occurrences = @error.occurrences.offset(@page.offset).limit(@page.items).order(created_at: :desc)
27 | end
28 |
29 | # PATCH/PUT /errors/1
30 | def update
31 | @error.update!(error_params)
32 | redirect_to errors_path, notice: "Error marked as resolved."
33 | end
34 |
35 | # DELETE /errors/1
36 | def destroy
37 | if @error.resolved?
38 | @error.destroy
39 | redirect_to errors_path(scope: :resolved), notice: "Error deleted."
40 | else
41 | redirect_to error_path(@error), alert: "You must resolve the error before deleting it."
42 | end
43 | end
44 |
45 | private
46 |
47 | # Only allow a list of trusted parameters through.
48 | def error_params
49 | params.require(:error).permit(:resolved_at)
50 | end
51 |
52 | def set_error
53 | @error = Error.find(params[:id])
54 | end
55 |
56 | def force_english_locale!(&action)
57 | I18n.with_locale(:en, &action)
58 | end
59 |
60 | def error_scope
61 | ActiveSupport::StringInquirer.new(params[:scope] || "unresolved")
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/app/mailers/solid_errors/error_mailer.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | # adapted from: https://github.com/codergeek121/email_error_reporter/blob/main/lib/email_error_reporter/error_mailer.rb
3 | class ErrorMailer < ActionMailer::Base
4 | def error_occurred(occurrence)
5 | @occurrence = occurrence
6 | @error = occurrence.error
7 | subject = "#{@error.severity_emoji} #{@error.exception_class}"
8 | if SolidErrors.email_subject_prefix.present?
9 | subject = [SolidErrors.email_subject_prefix, subject].join(" ").squish!
10 | end
11 | mail(
12 | subject: subject,
13 | from: SolidErrors.email_from,
14 | to: SolidErrors.email_to
15 | )
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/models/solid_errors/backtrace.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | # adapted from: https://github.com/honeybadger-io/honeybadger-ruby/blob/master/lib/honeybadger/backtrace.rb
3 | class Backtrace
4 | # Holder for an Array of Backtrace::Line instances.
5 | attr_reader :lines, :application_lines
6 |
7 | def self.parse(ruby_backtrace, opts = {})
8 | ruby_lines = ruby_backtrace.to_a
9 |
10 | lines = ruby_lines.collect do |unparsed_line|
11 | BacktraceLine.parse(unparsed_line.to_s, opts)
12 | end.compact
13 |
14 | new(lines)
15 | end
16 |
17 | def initialize(lines)
18 | self.lines = lines
19 | self.application_lines = lines.select(&:application?)
20 | end
21 |
22 | # Convert Backtrace to array.
23 | #
24 | # Returns array containing backtrace lines.
25 | def to_ary
26 | lines.take(1000).map { |l| {number: l.filtered_number, file: l.filtered_file, method: l.filtered_method, source: l.source} }
27 | end
28 | alias_method :to_a, :to_ary
29 |
30 | # JSON support.
31 | #
32 | # Returns JSON representation of backtrace.
33 | def as_json(options = {})
34 | to_ary
35 | end
36 |
37 | def to_s
38 | lines.map(&:to_s).join("\n")
39 | end
40 |
41 | def inspect
42 | ""
43 | end
44 |
45 | private
46 |
47 | attr_writer :lines, :application_lines
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/models/solid_errors/backtrace_line.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | # adapted from: https://github.com/honeybadger-io/honeybadger-ruby/blob/master/lib/honeybadger/backtrace.rb
3 | class BacktraceLine
4 | # Backtrace line regexp (optionally allowing leading X: for windows support).
5 | INPUT_FORMAT = %r{^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$}
6 | STRING_EMPTY = "".freeze
7 | GEM_ROOT = "[GEM_ROOT]".freeze
8 | PROJECT_ROOT = "[PROJECT_ROOT]".freeze
9 | PROJECT_ROOT_CACHE = {}
10 | GEM_ROOT_CACHE = {}
11 | RELATIVE_ROOT = Regexp.new('^\.\/').freeze
12 | RAILS_ROOT = ::Rails.root.to_s.dup.freeze
13 | ROOT_REGEXP = Regexp.new("^#{Regexp.escape(RAILS_ROOT)}").freeze
14 | BACKTRACE_FILTERS = [
15 | lambda { |line|
16 | return line unless defined?(Gem)
17 | GEM_ROOT_CACHE[line] ||= Gem.path.reduce(line) do |line, path|
18 | line.sub(path, GEM_ROOT)
19 | end
20 | },
21 | lambda { |line|
22 | c = (PROJECT_ROOT_CACHE[RAILS_ROOT] ||= {})
23 | return c[line] if c.has_key?(line)
24 | c[line] ||= line.sub(ROOT_REGEXP, PROJECT_ROOT)
25 | },
26 | lambda { |line| line.sub(RELATIVE_ROOT, STRING_EMPTY) }
27 | ].freeze
28 |
29 | attr_reader :file
30 | attr_reader :number
31 | attr_reader :method
32 | attr_reader :filtered_file, :filtered_number, :filtered_method, :unparsed_line
33 |
34 | # Parses a single line of a given backtrace
35 | #
36 | # @param [String] unparsed_line The raw line from +caller+ or some backtrace.
37 | #
38 | # @return The parsed backtrace line.
39 | def self.parse(unparsed_line, opts = {})
40 | filtered_line = BACKTRACE_FILTERS.reduce(unparsed_line) do |line, proc|
41 | proc.call(line)
42 | end
43 |
44 | if filtered_line
45 | match = unparsed_line.match(INPUT_FORMAT) || [].freeze
46 | fmatch = filtered_line.match(INPUT_FORMAT) || [].freeze
47 |
48 | file, number, method = match[1], match[2], match[3]
49 | filtered_args = [fmatch[1], fmatch[2], fmatch[3]]
50 | new(unparsed_line, file, number, method, *filtered_args, opts.fetch(:source_radius, 2))
51 | end
52 | end
53 |
54 | def initialize(unparsed_line, file, number, method, filtered_file = file,
55 | filtered_number = number, filtered_method = method,
56 | source_radius = 2)
57 | self.unparsed_line = unparsed_line
58 | self.filtered_file = filtered_file
59 | self.filtered_number = filtered_number
60 | self.filtered_method = filtered_method
61 | self.file = file
62 | self.number = number
63 | self.method = method
64 | self.source_radius = source_radius
65 | end
66 |
67 | # Reconstructs the line in a readable fashion.
68 | def to_s
69 | "#{filtered_file}:#{filtered_number}:in `#{filtered_method}'"
70 | end
71 |
72 | def ==(other)
73 | to_s == other.to_s
74 | end
75 |
76 | def inspect
77 | ""
78 | end
79 |
80 | # Determines if this line is part of the application trace or not.
81 | def application?
82 | (filtered_file =~ /^\[PROJECT_ROOT\]/i) && !(filtered_file =~ /^\[PROJECT_ROOT\]\/vendor/i)
83 | end
84 |
85 | def source
86 | @source ||= get_source(file, number, source_radius)
87 | end
88 |
89 | private
90 |
91 | attr_writer :file, :number, :method, :filtered_file, :filtered_number, :filtered_method, :unparsed_line
92 |
93 | attr_accessor :source_radius
94 |
95 | # Open source file and read line(s).
96 | #
97 | # Returns an array of line(s) from source file.
98 | def get_source(file, number, radius = 2)
99 | return {} unless file && File.exist?(file)
100 |
101 | before = after = radius
102 | start = (number.to_i - 1) - before
103 | start = 0 and before = 1 if start <= 0
104 | duration = before + 1 + after
105 |
106 | l = 0
107 | File.open(file) do |f|
108 | start.times {
109 | f.gets
110 | l += 1
111 | }
112 | return duration.times.map { (line = f.gets) ? [(l += 1), line] : nil }.compact.to_h
113 | end
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/app/models/solid_errors/error.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | class Error < Record
3 | self.table_name = "solid_errors"
4 |
5 | SEVERITY_TO_EMOJI = {
6 | error: "🔥",
7 | warning: "⚠️",
8 | info: "ℹ️"
9 | }
10 | SEVERITY_TO_BADGE_CLASSES = {
11 | error: "bg-red-100 text-red-800",
12 | warning: "bg-yellow-100 text-yellow-800",
13 | info: "bg-blue-100 text-blue-800"
14 | }
15 | STATUS_TO_EMOJI = {
16 | resolved: "✅",
17 | unresolved: "⏳"
18 | }
19 | STATUS_TO_BADGE_CLASSES = {
20 | resolved: "bg-green-100 text-green-800",
21 | unresolved: "bg-violet-100 text-violet-800"
22 | }
23 |
24 | has_many :occurrences, class_name: "SolidErrors::Occurrence", dependent: :destroy
25 |
26 | validates :exception_class, presence: true
27 | validates :message, presence: true
28 | validates :severity, presence: true
29 |
30 | scope :resolved, -> { where.not(resolved_at: nil) }
31 | scope :unresolved, -> { where(resolved_at: nil) }
32 |
33 | def severity_emoji
34 | SEVERITY_TO_EMOJI[severity.to_sym]
35 | end
36 |
37 | def severity_badge_classes
38 | "px-2 inline-flex text-sm font-semibold rounded-md #{SEVERITY_TO_BADGE_CLASSES[severity.to_sym]}"
39 | end
40 |
41 | def status
42 | resolved? ? :resolved : :unresolved
43 | end
44 |
45 | def status_emoji
46 | STATUS_TO_EMOJI[status]
47 | end
48 |
49 | def status_badge_classes
50 | "px-2 inline-flex text-sm font-semibold rounded-md #{STATUS_TO_BADGE_CLASSES[status]}"
51 | end
52 |
53 | def resolved?
54 | resolved_at.present?
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/models/solid_errors/occurrence.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | class Occurrence < Record
3 | belongs_to :error, class_name: "SolidErrors::Error"
4 |
5 | after_create_commit :send_email, if: -> { SolidErrors.send_emails? && SolidErrors.email_to.present? }
6 |
7 | # The parsed exception backtrace. Lines in this backtrace that are from installed gems
8 | # have the base path for gem installs replaced by "[GEM_ROOT]", while those in the project
9 | # have "[PROJECT_ROOT]".
10 | # @return [Array<{:number, :file, :method => String}>]
11 | def parsed_backtrace
12 | return @parsed_backtrace if defined? @parsed_backtrace
13 |
14 | @parsed_backtrace = parse_backtrace(backtrace.split("\n"))
15 | end
16 |
17 | private
18 |
19 | def parse_backtrace(backtrace)
20 | Backtrace.parse(backtrace)
21 | end
22 |
23 | def send_email
24 | ErrorMailer.error_occurred(self).deliver_later
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/models/solid_errors/record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidErrors
4 | class Record < ActiveRecord::Base
5 | self.abstract_class = true
6 |
7 | connects_to(**SolidErrors.connects_to) if SolidErrors.connects_to
8 | end
9 | end
10 |
11 | ActiveSupport.run_load_hooks :solid_errors_record, SolidErrors::Record
12 |
--------------------------------------------------------------------------------
/app/views/layouts/solid_errors/_style.html:
--------------------------------------------------------------------------------
1 |
1194 |
--------------------------------------------------------------------------------
/app/views/layouts/solid_errors/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Solid Errors
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= render "layouts/solid_errors/style" %>
9 |
10 |
11 |
12 | <%= content_for?(:content) ? yield(:content) : yield %>
13 |
14 |
15 |
16 |
17 | Solid Errors
|
18 | Made by @fractaledmind and friends ! Want to help? It's open source !
19 |
20 |
21 |
22 |
23 | <% if notice.present? %>
24 |
27 | <%= notice %>
28 |
29 | <% end %>
30 |
31 | <% if alert.present? %>
32 |
35 | <%= alert %>
36 |
37 | <% end %>
38 |
39 |
40 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/views/solid_errors/error_mailer/error_occurred.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Solid Errors | <%= @error.severity_emoji %> <%= @error.exception_class %>
4 | <%= render "layouts/solid_errors/style" %>
5 |
6 |
7 | <%= render "solid_errors/errors/error",
8 | error: @error,
9 | show_actions: false %>
10 |
11 |
12 |
13 |
14 | <%= render "solid_errors/occurrences/occurrence",
15 | occurrence: @occurrence %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/views/solid_errors/error_mailer/error_occurred.text.erb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/app/views/solid_errors/error_mailer/error_occurred.text.erb
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/_actions.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to errors_path(scope: error.resolved? ? :resolved : :unresolved), 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" do %>
3 |
4 |
5 |
6 |
Back to errors
7 | <% end %>
8 | <% if error.resolved? %>
9 | <%= render 'solid_errors/errors/delete_button', error: error %>
10 | <% else %>
11 | <%= render 'solid_errors/errors/resolve_button', error: error %>
12 | <% end %>
13 |
14 |
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/_delete_button.html.erb:
--------------------------------------------------------------------------------
1 | <%# locals: (error:) -%>
2 | <%= button_to error_path(error), method: :delete, class: "inline-flex items-center justify-center gap-2 font-medium cursor-pointer border rounded-lg py-3 px-5 bg-transparent text-red-500 border-red-500 hover:ring-red-200 hover:ring-8" do %>
3 |
4 |
5 |
6 | Delete , Error #<%= error.id %>
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/_error.html.erb:
--------------------------------------------------------------------------------
1 | <%# locals: (error:, show_actions: true) -%>
2 |
3 | <%= tag.section id: dom_id(error), class: "space-y-6" do %>
4 |
5 | <%= tag.h1 class: "font-bold flex items-center text-2xl gap-2" do %>
6 | <%= error.severity_emoji %>
7 | <%= error.exception_class %>
8 | from
9 | <%= error.source %>
10 | <% end %>
11 |
12 |
13 | #<%= error.id %>
14 |
15 |
16 |
17 | <%= error.message %>
18 |
19 |
20 |
21 |
22 | <%= SolidErrors::Error.human_attribute_name(:severity) %>
23 |
24 |
25 | <%= error.severity_emoji %>
26 |
27 | <%= error.severity %>
28 |
29 |
30 |
31 |
32 |
33 | <%= SolidErrors::Error.human_attribute_name(:status) %>
34 |
35 |
36 | <%= error.status_emoji %>
37 |
38 | <%= error.status %>
39 |
40 |
41 |
42 |
43 |
44 | <%= SolidErrors::Error.human_attribute_name(:first_seen) %>
45 |
46 |
47 | <% first_seen_at = error.occurrences.minimum(:created_at) %>
48 |
49 |
50 | <%= time_tag first_seen_at, "#{time_ago_in_words(first_seen_at)} ago" %>
51 |
52 |
53 |
54 |
55 |
56 | <%= SolidErrors::Error.human_attribute_name(:last_seen) %>
57 |
58 |
59 | <% last_seen_at = error.occurrences.maximum(:created_at) %>
60 |
61 |
62 | <%= time_tag last_seen_at, "#{time_ago_in_words(last_seen_at)} ago" %>
63 |
64 |
65 |
66 |
67 |
68 | <%= SolidErrors::Error.human_attribute_name(:exception_class) %>
69 |
70 |
71 | <%= error.exception_class %>
72 |
73 |
74 |
75 |
76 | <%= SolidErrors::Error.human_attribute_name(:source) %>
77 |
78 |
79 | <%= error.source %>
80 |
81 |
82 |
83 |
84 | <%= SolidErrors::Error.human_attribute_name(:project_root) %>
85 |
86 |
87 | <%= SolidErrors::BacktraceLine::RAILS_ROOT %>
88 |
89 |
90 |
91 |
92 | <%= SolidErrors::Error.human_attribute_name(:gem_root) %>
93 |
94 |
95 |
96 | <% Gem.path.each do |path| %>
97 | <%= path %>
98 | <% end %>
99 |
100 |
101 |
102 |
103 |
104 | <%= render "solid_errors/errors/actions", error: error if show_actions %>
105 | <% end %>
106 |
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/_resolve_button.html.erb:
--------------------------------------------------------------------------------
1 | <%# locals: (error:) -%>
2 | <%= button_to error_path(error), method: :patch, class: "inline-flex items-center justify-center gap-2 font-medium cursor-pointer border rounded-lg py-3 px-5 bg-transparent text-blue-500 border-blue-500 hover:ring-blue-200 hover:ring-8", params: { error: { resolved_at: Time.now } } do %>
3 |
4 |
5 |
6 |
7 | Resolve , Error #<%= error.id %>
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/_row.html.erb:
--------------------------------------------------------------------------------
1 | <%# locals: (error:) -%>
2 |
3 |
4 |
5 | <%= error.severity_emoji %>
6 | <%= link_to error_path(error), class: "text-blue-400 underline inline-flex items-baseline gap-1" do %>
7 | <%= error.exception_class %>
8 | <% end %>
9 | from
10 | <%= error.source %>
11 |
12 | <%= error.message %>
13 |
14 |
15 | <%= error.occurrences_count %>
16 |
17 |
18 | <% last_seen_at = error.recent_occurrence.is_a?(String) ? DateTime.strptime(error.recent_occurrence, "%Y-%m-%d %H:%M:%S.%N") : error.recent_occurrence %>
19 |
20 | <%= time_tag last_seen_at, time_ago_in_words(last_seen_at, scope: 'datetime.distance_in_words.short') %>
21 |
22 |
23 |
24 | <% if error.resolved? %>
25 | <%= render 'solid_errors/errors/delete_button', error: error %>
26 | <% else %>
27 | <%= render 'solid_errors/errors/resolve_button', error: error %>
28 | <% end %>
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Errors
4 |
5 |
6 | <%= link_to errors_path(scope: :unresolved), class: class_names(
7 | "inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium",
8 | "border-blue-500 text-blue-500" => !error_scope.resolved?,
9 | "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" => error_scope.resolved?) do %>
10 | <%= SolidErrors::Error::STATUS_TO_EMOJI[:unresolved] %>
11 | Unresolved
12 | <% end %>
13 | <%= link_to errors_path(scope: :resolved), class: class_names(
14 | "inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium",
15 | "border-blue-500 text-blue-500" => error_scope.resolved?,
16 | "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" => !error_scope.resolved?) do %>
17 | <%= SolidErrors::Error::STATUS_TO_EMOJI[:resolved] %>
18 | Resolved
19 | <% end %>
20 |
21 |
22 |
23 |
24 |
25 |
26 | Error
27 | Count
28 | Last
29 |
30 | Resolve
31 |
32 |
33 |
34 |
35 |
36 | <%= render partial: "solid_errors/errors/row", collection: @errors, as: :error %>
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/views/solid_errors/errors/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "solid_errors/errors/error",
2 | error: @error,
3 | show_actions: true %>
4 |
5 |
6 |
7 |
8 | <%= render "solid_errors/occurrences/collection",
9 | occurrences: @occurrences,
10 | page: @page,
11 | titled: true %>
12 |
--------------------------------------------------------------------------------
/app/views/solid_errors/occurrences/_collection.html.erb:
--------------------------------------------------------------------------------
1 | <% titled ||= false %>
2 |
3 |
4 | <% if titled %>
5 |
6 |
7 | Occurrences
8 |
9 |
10 |
11 | <% page_count = page.count %>
12 | <% if page_count.zero? %>
13 | No posts found
14 | <% elsif page.pages == 1 %>
15 | <%= page_count %> total
16 | <% else %>
17 | <%= page.from %>-<%= page.to %> of <%= page_count %>
18 | <% end %>
19 |
20 |
21 |
22 | <% if (first_page = page.first) %>
23 | «
24 | <% else %>
25 | «
26 | <% end %>
27 |
28 | <% if (prev_page = page.prev) %>
29 | ‹
30 | <% else %>
31 | ‹
32 | <% end %>
33 |
34 | <% if (next_page = page.next) %>
35 | ›
36 | <% else %>
37 | ›
38 | <% end %>
39 |
40 | <% if (last_page = page.last) %>
41 | »
42 | <% else %>
43 | »
44 | <% end %>
45 |
46 |
47 |
48 | <% end %>
49 |
50 |
51 | <% if occurrences.any? %>
52 | <%= render partial: "solid_errors/occurrences/occurrence",
53 | collection: occurrences, as: :occurrence %>
54 | <% else %>
55 |
56 | <%= bootstrap_svg "list-ul" %>
57 | No occurrences yet…
58 |
59 | <% end %>
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/views/solid_errors/occurrences/_occurrence.html.erb:
--------------------------------------------------------------------------------
1 | <% seen_at = occurrence.created_at %>
2 | <% backtrace = occurrence.parsed_backtrace %>
3 | <% open ||= defined?(occurrence_counter) ? occurrence_counter.zero? : true %>
4 |
5 | <%= tag.section id: dom_id(occurrence), class: "" do %>
6 | <%= tag.details open: open do %>
7 |
8 | <%= time_tag seen_at, seen_at %>
9 | (<%= time_ago_in_words(seen_at) %> ago )
10 |
11 |
12 |
13 | <% occurrence.context&.each do |key, value| %>
14 |
15 |
16 | <%= SolidErrors::Occurrence.human_attribute_name(key) %>
17 |
18 |
19 | <%= value %>
20 |
21 |
22 | <% end %>
23 |
24 |
25 | <%= SolidErrors::Occurrence.human_attribute_name(:backtrace) %>
26 |
27 |
28 | <% backtrace.lines.each_with_index do |line, i| %>
29 | <%= tag.details open: line.application? || i.zero? do %>
30 |
31 | <% if line.filtered_file %>
32 | <%= File.dirname(line.filtered_file) %>/ <%= File.basename(line.filtered_file) %> :<%= line.filtered_number %>
33 | in
34 | <%= line.filtered_method %>
35 | <% else %>
36 | <%= line.unparsed_line %>
37 | <% end %>
38 |
39 | <% line.source.each do |n, code| %>
40 | <%= n %> <%= code %>
41 | <% end %>
42 | <% end %>
43 | <% end %>
44 |
45 |
46 |
47 |
48 | <% end %>
49 | <% end %>
50 |
51 |
52 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "solid_errors"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require "irb"
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | # en.stripe.individual.card_issuing.user_terms_acceptance.ip
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 | # activerecord.errors.models.card.attributes.stripe_issuing_account.mismatched_accounts
32 |
33 | en:
34 | datetime:
35 | distance_in_words:
36 | short:
37 | about_x_hours:
38 | one: ~%{count}h
39 | other: ~%{count}h
40 | about_x_months:
41 | one: ~%{count}mo
42 | other: ~%{count}mo
43 | about_x_years:
44 | one: ~%{count}y
45 | other: ~%{count}y
46 | almost_x_years:
47 | one: ~%{count}y
48 | other: ~%{count}y
49 | half_a_minute: .5m
50 | less_than_x_seconds:
51 | one: <%{count}s
52 | other: <%{count}s
53 | less_than_x_minutes:
54 | one: <1m
55 | other: <%{count}m
56 | over_x_years:
57 | one: ">%{count}y"
58 | other: ">%{count}y"
59 | x_seconds:
60 | one: "%{count}s"
61 | other: "%{count}s"
62 | x_minutes:
63 | one: "%{count}m"
64 | other: "%{count}m"
65 | x_days:
66 | one: "%{count}d"
67 | other: "%{count}d"
68 | x_months:
69 | one: "%{count}mo"
70 | other: "%{count}mo"
71 | x_years:
72 | one: "%{count}y"
73 | other: "%{count}y"
74 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | SolidErrors::Engine.routes.draw do
2 | get "/", to: "errors#index", as: :root
3 |
4 | resources :errors, only: [:index, :show, :update, :destroy], path: ""
5 | end
6 |
--------------------------------------------------------------------------------
/images/index-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/images/index-screenshot.png
--------------------------------------------------------------------------------
/images/show-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/images/show-screenshot.png
--------------------------------------------------------------------------------
/lib/generators/solid_errors/install/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Installs solid_errors as a Rails error subscriber
3 |
4 | Example:
5 | bin/rails generate solid_errors:install
6 |
7 | This will perform the following:
8 | Adds solid_errors db schema
9 |
--------------------------------------------------------------------------------
/lib/generators/solid_errors/install/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidErrors
4 | #
5 | # Rails generator used for setting up SolidErrors in a Rails application.
6 | # Run it with +bin/rails g solid_errors:install+ in your console.
7 | #
8 | class InstallGenerator < Rails::Generators::Base
9 | source_root File.expand_path("templates", __dir__)
10 |
11 | def add_solid_errors_db_schema
12 | template "db/errors_schema.rb"
13 | end
14 |
15 | def configure_solid_errors
16 | insert_into_file Pathname(destination_root).join("config/environments/production.rb"), after: /^([ \t]*).*?(?=\nend)$/ do
17 | [
18 | "",
19 | '\1# Configure Solid Errors',
20 | '\1config.solid_errors.connects_to = { database: { writing: :errors } }',
21 | '\1config.solid_errors.send_emails = true',
22 | '\1config.solid_errors.email_from = ""',
23 | '\1config.solid_errors.email_to = ""',
24 | '\1config.solid_errors.username = Rails.application.credentials.dig(:solid_errors, :username)',
25 | '\1config.solid_errors.password = Rails.application.credentials.dig(:solid_errors, :password)'
26 | ].join("\n")
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/generators/solid_errors/install/templates/db/errors_schema.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ActiveRecord::Schema[7.1].define(version: 1) do
4 | create_table "solid_errors", force: :cascade do |t|
5 | t.text "exception_class", null: false
6 | t.text "message", null: false
7 | t.text "severity", null: false
8 | t.text "source"
9 | t.datetime "resolved_at"
10 | t.string "fingerprint", limit: 64, null: false
11 | t.datetime "created_at", null: false
12 | t.datetime "updated_at", null: false
13 | t.index ["fingerprint"], name: "index_solid_errors_on_fingerprint", unique: true
14 | t.index ["resolved_at"], name: "index_solid_errors_on_resolved_at"
15 | end
16 |
17 | create_table "solid_errors_occurrences", force: :cascade do |t|
18 | t.integer "error_id", null: false
19 | t.text "backtrace"
20 | t.json "context"
21 | t.datetime "created_at", null: false
22 | t.datetime "updated_at", null: false
23 | t.index ["error_id"], name: "index_solid_errors_occurrences_on_error_id"
24 | end
25 |
26 | add_foreign_key "solid_errors_occurrences", "solid_errors", column: "error_id"
27 | end
28 |
--------------------------------------------------------------------------------
/lib/solid_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "solid_errors/version"
4 | require_relative "solid_errors/sanitizer"
5 | require_relative "solid_errors/subscriber"
6 | require_relative "solid_errors/engine"
7 |
8 | module SolidErrors
9 | mattr_accessor :connects_to
10 | mattr_writer :username
11 | mattr_writer :password
12 | mattr_writer :send_emails
13 | mattr_writer :email_from
14 | mattr_writer :email_to
15 | mattr_writer :email_subject_prefix
16 |
17 | class << self
18 | # use method instead of attr_accessor to ensure
19 | # this works if variable set after SolidErrors is loaded
20 | def username
21 | @username ||= ENV["SOLIDERRORS_USERNAME"] || @@username
22 | end
23 |
24 | # use method instead of attr_accessor to ensure
25 | # this works if variable set after SolidErrors is loaded
26 | def password
27 | @password ||= ENV["SOLIDERRORS_PASSWORD"] || @@password
28 | end
29 |
30 | def send_emails?
31 | @send_emails ||= ENV["SOLIDERRORS_SEND_EMAILS"] || @@send_emails || false
32 | end
33 |
34 | def email_from
35 | @email_from ||= ENV["SOLIDERRORS_EMAIL_FROM"] || @@email_from || "solid_errors@noreply.com"
36 | end
37 |
38 | def email_to
39 | @email_to ||= ENV["SOLIDERRORS_EMAIL_TO"] || @@email_to
40 | end
41 |
42 | def email_subject_prefix
43 | @email_subject_prefix ||= ENV["SOLIDERRORS_EMAIL_SUBJECT_PREFIX"] || @@email_subject_prefix
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/solid_errors/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidErrors
4 | class Engine < ::Rails::Engine
5 | isolate_namespace SolidErrors
6 |
7 | config.solid_errors = ActiveSupport::OrderedOptions.new
8 |
9 | initializer "solid_errors.config" do
10 | config.solid_errors.each do |name, value|
11 | SolidErrors.public_send(:"#{name}=", value)
12 | end
13 | end
14 |
15 | initializer "solid_errors.active_record.error_subscriber" do
16 | Rails.error.subscribe(SolidErrors::Subscriber.new)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/solid_errors/sanitizer.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | # adapted from: https://github.com/honeybadger-io/honeybadger-ruby/blob/master/lib/honeybadger/util/sanitizer.rb
3 | class Sanitizer
4 | BASIC_OBJECT = "#".freeze
5 | DEPTH = "[DEPTH]".freeze
6 | RAISED = "[RAISED]".freeze
7 | RECURSION = "[RECURSION]".freeze
8 | TRUNCATED = "[TRUNCATED]".freeze
9 | MAX_STRING_SIZE = 65536
10 |
11 | def self.sanitize(data)
12 | @sanitizer ||= new
13 | @sanitizer.sanitize(data)
14 | end
15 |
16 | def initialize(max_depth: 20)
17 | @max_depth = max_depth
18 | end
19 |
20 | def sanitize(data, depth = 0, stack = nil)
21 | return BASIC_OBJECT if basic_object?(data)
22 |
23 | if recursive?(data)
24 | return RECURSION if stack&.include?(data.object_id)
25 |
26 | stack = stack ? stack.dup : Set.new
27 | stack << data.object_id
28 | end
29 |
30 | case data
31 | when Hash
32 | return DEPTH if depth >= max_depth
33 |
34 | new_hash = {}
35 | data.each do |key, value|
36 | key = key.is_a?(Symbol) ? key : sanitize(key, depth + 1, stack)
37 | value = sanitize(value, depth + 1, stack)
38 | new_hash[key] = value
39 | end
40 | new_hash
41 | when Array, Set
42 | return DEPTH if depth >= max_depth
43 |
44 | data.to_a.map do |value|
45 | sanitize(value, depth + 1, stack)
46 | end
47 | when Numeric, TrueClass, FalseClass, NilClass
48 | data
49 | when String
50 | sanitize_string(data)
51 | else # all other objects
52 | klass = data.class
53 |
54 | begin
55 | data = String(data)
56 | rescue
57 | return RAISED
58 | end
59 |
60 | return "#<#{klass.name}>" if inspected?(data)
61 |
62 | sanitize_string(data)
63 | end
64 | end
65 |
66 | private
67 |
68 | attr_reader :max_depth
69 |
70 | def basic_object?(object)
71 | object.respond_to?(:to_s)
72 | false
73 | rescue
74 | # BasicObject doesn't respond to `#respond_to?`.
75 | true
76 | end
77 |
78 | def recursive?(data)
79 | data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Set)
80 | end
81 |
82 | def sanitize_string(string)
83 | string = string.gsub(/#<(.*?):0x.*?>/, '#<\1>') # remove object_id
84 | return string unless string.respond_to?(:size) && string.size > MAX_STRING_SIZE
85 | string[0...MAX_STRING_SIZE] + TRUNCATED
86 | end
87 |
88 | def inspected?(string)
89 | String(string) =~ /#<.*>/
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/solid_errors/subscriber.rb:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | class Subscriber
3 | IGNORED_ERRORS = ["ActionController::RoutingError",
4 | "AbstractController::ActionNotFound",
5 | "ActionController::MethodNotAllowed",
6 | "ActionController::UnknownHttpMethod",
7 | "ActionController::NotImplemented",
8 | "ActionController::UnknownFormat",
9 | "ActionController::InvalidAuthenticityToken",
10 | "ActionController::InvalidCrossOriginRequest",
11 | "ActionDispatch::Http::Parameters::ParseError",
12 | "ActionController::BadRequest",
13 | "ActionController::ParameterMissing",
14 | "ActiveRecord::RecordNotFound",
15 | "ActionController::UnknownAction",
16 | "ActionDispatch::Http::MimeNegotiation::InvalidType",
17 | "Rack::QueryParser::ParameterTypeError",
18 | "Rack::QueryParser::InvalidParameterError",
19 | "CGI::Session::CookieStore::TamperedWithCookie",
20 | "Mongoid::Errors::DocumentNotFound",
21 | "Sinatra::NotFound",
22 | "Sidekiq::JobRetry::Skip"].map(&:freeze).freeze
23 |
24 | def report(error, handled:, severity:, context:, source: nil)
25 | return if ignore_by_class?(error.class.name)
26 |
27 | error_attributes = {
28 | exception_class: error.class.name,
29 | message: s(error.message),
30 | severity: severity,
31 | source: source
32 | }
33 | fingerprint = Digest::SHA256.hexdigest(error_attributes.values.join)
34 | if (record = SolidErrors::Error.find_by(fingerprint: fingerprint))
35 | record.update!(resolved_at: nil, updated_at: Time.now)
36 | else
37 | record = SolidErrors::Error.create!(error_attributes.merge(fingerprint: fingerprint))
38 | end
39 |
40 | SolidErrors::Occurrence.create(
41 | error_id: record.id,
42 | backtrace: error.backtrace.join("\n"),
43 | context: s(context)
44 | )
45 | end
46 |
47 | def s(data)
48 | Sanitizer.sanitize(data)
49 | end
50 |
51 | def ignore_by_class?(error_class_name)
52 | IGNORED_ERRORS.any? do |ignored_class|
53 | ignored_class_name = ignored_class.respond_to?(:name) ? ignored_class.name : ignored_class
54 |
55 | ignored_class_name == error_class_name
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/solid_errors/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidErrors
4 | VERSION = "0.6.1"
5 | end
6 |
--------------------------------------------------------------------------------
/sig/solid_errors.rbs:
--------------------------------------------------------------------------------
1 | module SolidErrors
2 | VERSION: String
3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4 | end
5 |
--------------------------------------------------------------------------------
/solid_errors.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/solid_errors/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "solid_errors"
7 | spec.version = SolidErrors::VERSION
8 | spec.authors = ["Stephen Margheim"]
9 | spec.email = ["stephen.margheim@gmail.com"]
10 |
11 | spec.summary = "Database-backed Rails error subscriber"
12 | spec.homepage = "https://github.com/fractaledmind/solid_errors"
13 | spec.license = "MIT"
14 | spec.required_ruby_version = ">= 2.6.0"
15 |
16 | spec.metadata["homepage_uri"] = spec.homepage
17 | spec.metadata["source_code_uri"] = "https://github.com/fractaledmind/solid_errors"
18 |
19 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
20 | Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
21 | end
22 |
23 | ">= 7.0".tap do |rails_version|
24 | spec.add_dependency "actionmailer", rails_version
25 | spec.add_dependency "actionpack", rails_version
26 | spec.add_dependency "actionview", rails_version
27 | spec.add_dependency "activerecord", rails_version
28 | spec.add_dependency "activesupport", rails_version
29 | spec.add_dependency "railties", rails_version
30 | end
31 |
32 | spec.add_development_dependency "sqlite3"
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args, exception: true)
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | # require "active_job/railtie"
7 | require "active_record/railtie"
8 | # require "active_storage/engine"
9 | require "action_controller/railtie"
10 | require "action_mailer/railtie"
11 | # require "action_mailbox/engine"
12 | # require "action_text/engine"
13 | require "action_view/railtie"
14 | # require "action_cable/engine"
15 | require "rails/test_unit/railtie"
16 |
17 | # Require the gems listed in Gemfile, including any gems
18 | # you've limited to :test, :development, or :production.
19 | Bundler.require(*Rails.groups)
20 |
21 | module Dummy
22 | class Application < Rails::Application
23 | config.load_defaults Rails::VERSION::STRING.to_f
24 |
25 | # For compatibility with applications that use this config
26 | config.action_controller.include_all_helpers = false
27 |
28 | # Please, add to the `ignore` list any other `lib` subdirectories that do
29 | # not contain `.rb` files, or that should not be reloaded or eager loaded.
30 | # Common ones are `templates`, `generators`, or `middleware`, for example.
31 | config.autoload_lib(ignore: %w[assets tasks])
32 |
33 | # Configuration for the application, engines, and railties goes here.
34 | #
35 | # These settings can be overridden in specific environments using the files
36 | # in config/environments, which are processed later.
37 | #
38 | # config.time_zone = "Central Time (US & Canada)"
39 | # config.eager_load_paths << Rails.root.join("extras")
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | primary: &primary
13 | <<: *default
14 | database: storage/<%= ENV.fetch("RAILS_ENV", "development") %>.sqlite3
15 |
16 | queue: &queue
17 | <<: *default
18 | migrations_paths: db/queue_migrate
19 | database: storage/queue.sqlite3
20 |
21 | errors: &errors
22 | <<: *default
23 | migrations_paths: db/errors_migrate
24 | database: storage/errors.sqlite3
25 |
26 | development:
27 | primary:
28 | <<: *primary
29 | database: storage/<%= `git branch --show-current`.chomp || 'development' %>.sqlite3
30 | queue: *queue
31 | errors: *errors
32 |
33 | # Warning: The database defined as "test" will be erased and
34 | # re-generated from your development database when you run "rake".
35 | # Do not set this db to the same as development or production.
36 | test:
37 | primary:
38 | <<: *primary
39 | database: db/test.sqlite3
40 | queue:
41 | <<: *queue
42 | database: db/queue.sqlite3
43 | errors:
44 | <<: *errors
45 | database: db/errors.sqlite3
46 |
47 | production:
48 | primary: *primary
49 | queue: *queue
50 | errors: *errors
51 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # While tests run files are not watched, reloading is not necessary.
12 | config.enable_reloading = false
13 |
14 | # Eager loading loads your entire application. When running a single test locally,
15 | # this is usually not necessary, and can slow down your test suite. However, it's
16 | # recommended that you enable it in continuous integration systems to ensure eager
17 | # loading is working properly before deploying your code.
18 | config.eager_load = ENV["CI"].present?
19 |
20 | # Configure public file server for tests with Cache-Control for performance.
21 | config.public_file_server.enabled = true
22 | config.public_file_server.headers = {
23 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
24 | }
25 |
26 | # Show full error reports and disable caching.
27 | config.consider_all_requests_local = true
28 | config.action_controller.perform_caching = false
29 | config.cache_store = :null_store
30 |
31 | # Render exception templates for rescuable exceptions and raise for other exceptions.
32 | config.action_dispatch.show_exceptions = :rescuable
33 |
34 | # Disable request forgery protection in test environment.
35 | config.action_controller.allow_forgery_protection = false
36 |
37 | # Store uploaded files on the local file system in a temporary directory.
38 | # config.active_storage.service = :test
39 |
40 | # config.action_mailer.perform_caching = false
41 |
42 | # Tell Action Mailer not to deliver emails to the real world.
43 | # The :test delivery method accumulates sent emails in the
44 | # ActionMailer::Base.deliveries array.
45 | # config.action_mailer.delivery_method = :test
46 |
47 | # Print deprecation notices to the stderr.
48 | config.active_support.deprecation = :stderr
49 |
50 | # Raise exceptions for disallowed deprecations.
51 | config.active_support.disallowed_deprecation = :raise
52 |
53 | # Tell Active Support which deprecation messages to disallow.
54 | config.active_support.disallowed_deprecation_warnings = []
55 |
56 | # Raises error for missing translations.
57 | # config.i18n.raise_on_missing_translations = true
58 |
59 | # Annotate rendered view with file names.
60 | # config.action_view.annotate_rendered_view_with_filenames = true
61 |
62 | # Raise error when a before_action's only/except options reference missing actions
63 | config.action_controller.raise_on_missing_callback_actions = true
64 | end
65 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization and
2 | # are automatically loaded by Rails. If you want to use locales other than
3 | # English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more about the API, please read the Rails Internationalization guide
20 | # at https://guides.rubyonrails.org/i18n.html.
21 | #
22 | # Be aware that YAML interprets the following case-insensitive strings as
23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24 | # must be quoted to be interpreted as strings. For example:
25 | #
26 | # en:
27 | # "yes": yup
28 | # enabled: "ON"
29 |
30 | en:
31 | hello: "Hello world"
32 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # This configuration file will be evaluated by Puma. The top-level methods that
2 | # are invoked here are part of Puma's configuration DSL. For more information
3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
4 |
5 | # Puma can serve each request in a thread from an internal thread pool.
6 | # The `threads` method setting takes two numbers: a minimum and maximum.
7 | # Any libraries that use thread pools should be configured to match
8 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
9 | # and maximum; this matches the default thread size of Active Record.
10 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
11 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
12 | threads min_threads_count, max_threads_count
13 |
14 | # Specifies that the worker count should equal the number of processors in production.
15 | if ENV["RAILS_ENV"] == "production"
16 | require "concurrent-ruby"
17 | worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
18 | workers worker_count if worker_count > 1
19 | end
20 |
21 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
22 | # terminating a worker in development environments.
23 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
24 |
25 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
26 | port ENV.fetch("PORT") { 3000 }
27 |
28 | # Specifies the `environment` that Puma will run in.
29 | environment ENV.fetch("RAILS_ENV") { "development" }
30 |
31 | # Specifies the `pidfile` that Puma will use.
32 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
33 |
34 | # Allow puma to be restarted by `bin/rails restart` command.
35 | plugin :tmp_restart
36 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
3 |
4 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
5 | # Can be used by load balancers and uptime monitors to verify that the app is live.
6 | get "up" => "rails/health#show", :as => :rails_health_check
7 |
8 | # Defines the root path route ("/")
9 | # root "posts#index"
10 | end
11 |
--------------------------------------------------------------------------------
/test/dummy/db/errors.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/db/errors.sqlite3
--------------------------------------------------------------------------------
/test/dummy/db/queue.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/db/queue.sqlite3
--------------------------------------------------------------------------------
/test/dummy/db/test.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/db/test.sqlite3
--------------------------------------------------------------------------------
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/log/.keep
--------------------------------------------------------------------------------
/test/dummy/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/storage/.keep
--------------------------------------------------------------------------------
/test/dummy/storage/test.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/storage/test.sqlite3
--------------------------------------------------------------------------------
/test/dummy/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/tmp/.keep
--------------------------------------------------------------------------------
/test/dummy/tmp/local_secret.txt:
--------------------------------------------------------------------------------
1 | 2e674226836fd5b8d265fbc2088757c026b86086afb1ea213271ec09b432cce56122da2e7ba0ab5e70ab06f5b1b07fdf386b0cea6b28efce5afaa11d5aa1e76f
--------------------------------------------------------------------------------
/test/dummy/tmp/pids/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/tmp/pids/.keep
--------------------------------------------------------------------------------
/test/dummy/tmp/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fractaledmind/solid_errors/b75251b35c28e53f4339cc01f2ed519367c7a6f0/test/dummy/tmp/storage/.keep
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV["RAILS_ENV"] = "test"
4 |
5 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6 |
7 | require_relative "../test/dummy/config/environment"
8 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
9 | require "rails/test_help"
10 | require "solid_errors"
11 |
12 | require "minitest/autorun"
13 |
--------------------------------------------------------------------------------
/test/test_solid_errors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class TestSolidErrors < Minitest::Test
6 | def test_that_it_has_a_version_number
7 | refute_nil ::SolidErrors::VERSION
8 | end
9 | end
10 |
--------------------------------------------------------------------------------