solid_errors 0.2.16 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +111 -14
- data/app/controllers/solid_errors/application_controller.rb +3 -0
- data/app/controllers/solid_errors/errors_controller.rb +13 -15
- data/app/models/solid_errors/backtrace.rb +3 -3
- data/app/models/solid_errors/backtrace_line.rb +17 -16
- data/app/models/solid_errors/error.rb +1 -1
- data/app/models/solid_errors/occurrence.rb +3 -3
- data/app/models/solid_errors/record.rb +1 -1
- data/app/views/layouts/solid_errors/application.html.erb +136 -120
- data/app/views/solid_errors/errors/_error.html.erb +5 -2
- data/app/views/solid_errors/errors/show.html.erb +11 -6
- data/app/views/solid_errors/occurrences/_occurrence.html.erb +4 -4
- data/config/routes.rb +2 -2
- data/lib/generators/solid_errors/install/install_generator.rb +4 -4
- data/lib/solid_errors/sanitizer.rb +30 -29
- data/lib/solid_errors/subscriber.rb +20 -21
- data/lib/solid_errors/version.rb +1 -1
- data/lib/solid_errors.rb +16 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dabfc29afbf8683a9f79db87fc193679fa3074415e3a68b2d0d0088bfd0d99d5
|
4
|
+
data.tar.gz: 6124021da4ef6ea9823138cc264d2bcdd07d2496c2638ea3ff7ceee852975b2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7859b1b2ced0cb2e44f2351d9645bb6ffa3cc88451d5bccd6d6ec74eee10c698dfd39eeccc1e5efeed5505b173a85417d3008977dad0c12afccb5a8ba3039e04
|
7
|
+
data.tar.gz: 87100e0396f8b6630a41889516e32da08f7cbd94ee7da6b45e17c67a0726bd49aa4f77a59df05fbbbcc5c916105cb4911f99c2400d34c4e89f5c9e76a7db2f93
|
data/README.md
CHANGED
@@ -1,28 +1,125 @@
|
|
1
|
-
#
|
1
|
+
# Solid Errors
|
2
|
+
|
3
|
+
<p>
|
4
|
+
<a href="https://rubygems.org/gems/solid_errors">
|
5
|
+
<img alt="GEM Version" src="https://img.shields.io/gem/v/solid_errors?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
|
6
|
+
</a>
|
7
|
+
<a href="https://rubygems.org/gems/solid_errors">
|
8
|
+
<img alt="GEM Downloads" src="https://img.shields.io/gem/dt/solid_errors?color=168AFE&logo=ruby&logoColor=FE1616">
|
9
|
+
</a>
|
10
|
+
<a href="https://github.com/testdouble/standard">
|
11
|
+
<img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
|
12
|
+
</a>
|
13
|
+
<a href="https://github.com/fractaledmind/solid_errors/actions/workflows/main.yml">
|
14
|
+
<img alt="Tests" src="https://github.com/fractaledmind/solid_errors/actions/workflows/main.yml/badge.svg" />
|
15
|
+
</a>
|
16
|
+
<a href="https://github.com/sponsors/fractaledmind">
|
17
|
+
<img alt="Sponsors" src="https://img.shields.io/github/sponsors/fractaledmind?color=eb4aaa&logo=GitHub%20Sponsors" />
|
18
|
+
</a>
|
19
|
+
<a href="https://ruby.social/@fractaledmind">
|
20
|
+
<img alt="Ruby.Social Follow" src="https://img.shields.io/mastodon/follow/109291299520066427?domain=https%3A%2F%2Fruby.social&label=%40fractaledmind&style=social">
|
21
|
+
</a>
|
22
|
+
<a href="https://twitter.com/fractaledmind">
|
23
|
+
<img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40fractaledmind&style=social&url=https%3A%2F%2Ftwitter.com%2Ffractaledmind">
|
24
|
+
</a>
|
25
|
+
</p>
|
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.
|
2
29
|
|
3
|
-
|
30
|
+
## Installation
|
4
31
|
|
5
|
-
|
32
|
+
Install the gem and add to the application's Gemfile by executing:
|
6
33
|
|
7
|
-
|
34
|
+
$ bundle add solid_errors
|
35
|
+
|
36
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
37
|
+
|
38
|
+
$ gem install solid_errors
|
39
|
+
|
40
|
+
After installing the gem, run the installer:
|
41
|
+
|
42
|
+
$ rails generate solid_errors:install
|
43
|
+
|
44
|
+
This will copy the required migration over to your app.
|
45
|
+
|
46
|
+
Then mount the engine in your config/routes.rb file
|
47
|
+
|
48
|
+
mount SolidErrors::Engine, at: "/solid_errors"
|
49
|
+
|
50
|
+
> [!NOTE]
|
51
|
+
> Be sure to [secure the dashboard](#authentication) in production.
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
All exceptions are recorded automatically. No additional code required.
|
56
|
+
|
57
|
+
Please consult the [official guides](https://guides.rubyonrails.org/error_reporting.html) for an introduction to the error reporting API.
|
58
|
+
|
59
|
+
### Configuration
|
60
|
+
|
61
|
+
You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently, only 3 configuration options are available:
|
62
|
+
|
63
|
+
* `connects_to` - The database configuration to use for the Solid Errors database. See [Database Configuration](#database-configuration) for more information.
|
64
|
+
* `username` - The username to use for HTTP authentication. See [Authentication](#authentication) for more information.
|
65
|
+
* `password` - The password to use for HTTP authentication. See [Authentication](#authentication) for more information.
|
66
|
+
|
67
|
+
#### Database Configuration
|
68
|
+
|
69
|
+
`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. For example:
|
8
70
|
|
9
|
-
|
71
|
+
```ruby
|
72
|
+
# Use a single separate DB for Solid Errors
|
73
|
+
config.solid_errors.connects_to = { database: { writing: :solid_errors, reading: :solid_errors } }
|
74
|
+
```
|
75
|
+
|
76
|
+
or
|
10
77
|
|
11
78
|
```ruby
|
12
|
-
|
79
|
+
# Use a separate primary/replica pair for Solid Errors
|
80
|
+
config.solid_errors.connects_to = { database: { writing: :solid_errors_primary, reading: :solid_errors_replica } }
|
13
81
|
```
|
14
82
|
|
15
|
-
|
83
|
+
#### Authentication
|
16
84
|
|
17
|
-
|
85
|
+
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.
|
18
86
|
|
19
|
-
|
87
|
+
There are two ways to setup a username and password. First, you can use the `SOLIDERRORS_USERNAME` and `SOLIDERRORS_PASSWORD` environment variables:
|
20
88
|
|
21
|
-
|
89
|
+
```ruby
|
90
|
+
ENV["SOLIDERRORS_USERNAME"] = "frodo"
|
91
|
+
ENV["SOLIDERRORS_PASSWORD"] = "ikeptmysecrets"
|
92
|
+
```
|
22
93
|
|
23
|
-
|
94
|
+
Second, you can set the `SolidErrors.username` and `SolidErrors.password` variables in an initializer:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# Set authentication credentials for Solid Errors
|
98
|
+
config.solid_errors.username = Rails.application.credentials.solid_errors.username
|
99
|
+
config.solid_errors.password = Rails.application.credentials.solid_errors.password
|
100
|
+
```
|
101
|
+
|
102
|
+
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.
|
103
|
+
|
104
|
+
If you use Devise for authenctication in your app, you can also restrict access to the dashboard by using their `authenticate` contraint in your routes file:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
authenticate :user, -> (user) { user.admin? } do
|
108
|
+
mount SolidErrors::Engine, at: "/solid_errors"
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
### Examples
|
113
|
+
|
114
|
+
There are only two screens in the dashboard.
|
115
|
+
|
116
|
+
* the index view of all unresolved errors:
|
117
|
+
|
118
|
+
![image description](images/index-screenshot.png)
|
119
|
+
|
120
|
+
* and the show view of a particular error:
|
24
121
|
|
25
|
-
|
122
|
+
![image description](images/show-screenshot.png)
|
26
123
|
|
27
124
|
## Development
|
28
125
|
|
@@ -32,7 +129,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
129
|
|
33
130
|
## Contributing
|
34
131
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
132
|
+
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).
|
36
133
|
|
37
134
|
## License
|
38
135
|
|
@@ -40,4 +137,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
40
137
|
|
41
138
|
## Code of Conduct
|
42
139
|
|
43
|
-
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/
|
140
|
+
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).
|
@@ -1,20 +1,17 @@
|
|
1
1
|
module SolidErrors
|
2
2
|
class ErrorsController < ApplicationController
|
3
|
-
before_action :set_error, only: %i[
|
3
|
+
before_action :set_error, only: %i[show update]
|
4
4
|
|
5
5
|
# GET /errors
|
6
6
|
def index
|
7
7
|
errors_table = Error.arel_table
|
8
8
|
occurrences_table = Occurrence.arel_table
|
9
|
-
recent_occurrence = occurrences_table
|
10
|
-
.project(occurrences_table[:created_at].maximum)
|
11
|
-
.as('recent_occurrence')
|
12
9
|
|
13
10
|
@errors = Error.unresolved
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
.joins(:occurrences)
|
12
|
+
.select(errors_table[Arel.star], occurrences_table[:created_at].maximum.as("recent_occurrence"))
|
13
|
+
.group(errors_table[:id])
|
14
|
+
.order(recent_occurrence: :desc)
|
18
15
|
end
|
19
16
|
|
20
17
|
# GET /errors/1
|
@@ -28,13 +25,14 @@ module SolidErrors
|
|
28
25
|
end
|
29
26
|
|
30
27
|
private
|
31
|
-
# Only allow a list of trusted parameters through.
|
32
|
-
def error_params
|
33
|
-
params.require(:error).permit(:resolved_at)
|
34
|
-
end
|
35
28
|
|
36
|
-
|
37
|
-
|
38
|
-
|
29
|
+
# Only allow a list of trusted parameters through.
|
30
|
+
def error_params
|
31
|
+
params.require(:error).permit(:resolved_at)
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_error
|
35
|
+
@error = Error.find(params[:id])
|
36
|
+
end
|
39
37
|
end
|
40
38
|
end
|
@@ -10,7 +10,7 @@ module SolidErrors
|
|
10
10
|
BacktraceLine.parse(unparsed_line.to_s, opts)
|
11
11
|
end.compact
|
12
12
|
|
13
|
-
|
13
|
+
new(lines)
|
14
14
|
end
|
15
15
|
|
16
16
|
def initialize(lines)
|
@@ -22,9 +22,9 @@ module SolidErrors
|
|
22
22
|
#
|
23
23
|
# Returns array containing backtrace lines.
|
24
24
|
def to_ary
|
25
|
-
lines.take(1000).map { |l| {
|
25
|
+
lines.take(1000).map { |l| {number: l.filtered_number, file: l.filtered_file, method: l.filtered_method, source: l.source} }
|
26
26
|
end
|
27
|
-
|
27
|
+
alias_method :to_a, :to_ary
|
28
28
|
|
29
29
|
# JSON support.
|
30
30
|
#
|
@@ -2,14 +2,14 @@ module SolidErrors
|
|
2
2
|
class BacktraceLine
|
3
3
|
# Backtrace line regexp (optionally allowing leading X: for windows support).
|
4
4
|
INPUT_FORMAT = %r{^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$}.freeze
|
5
|
-
STRING_EMPTY =
|
6
|
-
GEM_ROOT =
|
7
|
-
PROJECT_ROOT =
|
5
|
+
STRING_EMPTY = "".freeze
|
6
|
+
GEM_ROOT = "[GEM_ROOT]".freeze
|
7
|
+
PROJECT_ROOT = "[PROJECT_ROOT]".freeze
|
8
8
|
PROJECT_ROOT_CACHE = {}
|
9
9
|
GEM_ROOT_CACHE = {}
|
10
10
|
RELATIVE_ROOT = Regexp.new('^\.\/').freeze
|
11
11
|
RAILS_ROOT = ::Rails.root.to_s.dup.freeze
|
12
|
-
ROOT_REGEXP = Regexp.new("^#{
|
12
|
+
ROOT_REGEXP = Regexp.new("^#{Regexp.escape(RAILS_ROOT)}").freeze
|
13
13
|
BACKTRACE_FILTERS = [
|
14
14
|
lambda { |line|
|
15
15
|
return line unless defined?(Gem)
|
@@ -47,21 +47,19 @@ module SolidErrors
|
|
47
47
|
file, number, method = match[1], match[2], match[3]
|
48
48
|
filtered_args = [fmatch[1], fmatch[2], fmatch[3]]
|
49
49
|
new(file, number, method, *filtered_args, opts.fetch(:source_radius, 2))
|
50
|
-
else
|
51
|
-
nil
|
52
50
|
end
|
53
51
|
end
|
54
52
|
|
55
53
|
def initialize(file, number, method, filtered_file = file,
|
56
|
-
|
57
|
-
|
58
|
-
self.filtered_file
|
54
|
+
filtered_number = number, filtered_method = method,
|
55
|
+
source_radius = 2)
|
56
|
+
self.filtered_file = filtered_file
|
59
57
|
self.filtered_number = filtered_number
|
60
58
|
self.filtered_method = filtered_method
|
61
|
-
self.file
|
62
|
-
self.number
|
63
|
-
self.method
|
64
|
-
self.source_radius
|
59
|
+
self.file = file
|
60
|
+
self.number = number
|
61
|
+
self.method = method
|
62
|
+
self.source_radius = source_radius
|
65
63
|
end
|
66
64
|
|
67
65
|
# Reconstructs the line in a readable fashion.
|
@@ -74,7 +72,7 @@ module SolidErrors
|
|
74
72
|
end
|
75
73
|
|
76
74
|
def inspect
|
77
|
-
"<Line:#{
|
75
|
+
"<Line:#{self}>"
|
78
76
|
end
|
79
77
|
|
80
78
|
# Determines if this line is part of the application trace or not.
|
@@ -105,8 +103,11 @@ module SolidErrors
|
|
105
103
|
|
106
104
|
l = 0
|
107
105
|
File.open(file) do |f|
|
108
|
-
start.times {
|
109
|
-
|
106
|
+
start.times {
|
107
|
+
f.gets
|
108
|
+
l += 1
|
109
|
+
}
|
110
|
+
return duration.times.map { (line = f.gets) ? [(l += 1), line] : nil }.compact.to_h
|
110
111
|
end
|
111
112
|
end
|
112
113
|
end
|
@@ -27,7 +27,7 @@ module SolidErrors
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def badge_classes
|
30
|
-
"px-2 inline-flex text-
|
30
|
+
"px-2 inline-flex text-sm font-semibold rounded-md #{SEVERITY_TO_BADGE_CLASSES[severity.to_sym]}"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -95,7 +95,8 @@
|
|
95
95
|
*/
|
96
96
|
|
97
97
|
abbr:where([title]) {
|
98
|
-
text-decoration: underline
|
98
|
+
text-decoration-line: underline;
|
99
|
+
text-decoration-style: dotted;
|
99
100
|
}
|
100
101
|
|
101
102
|
/*
|
@@ -589,14 +590,26 @@
|
|
589
590
|
border-width: 0
|
590
591
|
}
|
591
592
|
|
592
|
-
.
|
593
|
-
position:
|
593
|
+
.fixed{
|
594
|
+
position: fixed
|
594
595
|
}
|
595
596
|
|
596
597
|
.relative{
|
597
598
|
position: relative
|
598
599
|
}
|
599
600
|
|
601
|
+
.left-0{
|
602
|
+
left: 0px
|
603
|
+
}
|
604
|
+
|
605
|
+
.right-0{
|
606
|
+
right: 0px
|
607
|
+
}
|
608
|
+
|
609
|
+
.top-0{
|
610
|
+
top: 0px
|
611
|
+
}
|
612
|
+
|
600
613
|
.-mx-2{
|
601
614
|
margin-left: -0.5rem;
|
602
615
|
margin-right: -0.5rem
|
@@ -611,10 +624,6 @@
|
|
611
624
|
margin-bottom: 0.75rem
|
612
625
|
}
|
613
626
|
|
614
|
-
.mb-36{
|
615
|
-
margin-bottom: 9rem
|
616
|
-
}
|
617
|
-
|
618
627
|
.ml-6{
|
619
628
|
margin-left: 1.5rem
|
620
629
|
}
|
@@ -627,37 +636,8 @@
|
|
627
636
|
margin-top: 1rem
|
628
637
|
}
|
629
638
|
|
630
|
-
.
|
631
|
-
|
632
|
-
}
|
633
|
-
|
634
|
-
.select-none{
|
635
|
-
user-select: none
|
636
|
-
}
|
637
|
-
|
638
|
-
.overflow-auto{
|
639
|
-
overflow: auto
|
640
|
-
}
|
641
|
-
|
642
|
-
.rounded-b-lg{
|
643
|
-
border-bottom-right-radius: 0.5rem;
|
644
|
-
border-bottom-left-radius: 0.5rem
|
645
|
-
}
|
646
|
-
|
647
|
-
.leading-normal{
|
648
|
-
line-height: 1.5
|
649
|
-
}
|
650
|
-
|
651
|
-
.mt-4{
|
652
|
-
margin-top: 1rem
|
653
|
-
}
|
654
|
-
|
655
|
-
.\!block{
|
656
|
-
display: block !important
|
657
|
-
}
|
658
|
-
|
659
|
-
.block{
|
660
|
-
display: block
|
639
|
+
.inline-block{
|
640
|
+
display: inline-block
|
661
641
|
}
|
662
642
|
|
663
643
|
.flex{
|
@@ -668,10 +648,6 @@
|
|
668
648
|
display: inline-flex
|
669
649
|
}
|
670
650
|
|
671
|
-
.flex-col{
|
672
|
-
flex-direction: column
|
673
|
-
}
|
674
|
-
|
675
651
|
.table{
|
676
652
|
display: table
|
677
653
|
}
|
@@ -680,12 +656,8 @@
|
|
680
656
|
display: grid
|
681
657
|
}
|
682
658
|
|
683
|
-
.
|
684
|
-
|
685
|
-
}
|
686
|
-
|
687
|
-
.w-full{
|
688
|
-
width: 100%
|
659
|
+
.min-h-full{
|
660
|
+
min-height: 100%
|
689
661
|
}
|
690
662
|
|
691
663
|
.min-w-full{
|
@@ -712,10 +684,18 @@
|
|
712
684
|
grid-template-columns: repeat(2, minmax(0, 1fr))
|
713
685
|
}
|
714
686
|
|
687
|
+
.flex-col{
|
688
|
+
flex-direction: column
|
689
|
+
}
|
690
|
+
|
715
691
|
.flex-wrap{
|
716
692
|
flex-wrap: wrap
|
717
693
|
}
|
718
694
|
|
695
|
+
.items-start{
|
696
|
+
align-items: flex-start
|
697
|
+
}
|
698
|
+
|
719
699
|
.items-center{
|
720
700
|
align-items: center
|
721
701
|
}
|
@@ -773,10 +753,18 @@
|
|
773
753
|
border-color: rgb(209 213 219 / var(--tw-divide-opacity))
|
774
754
|
}
|
775
755
|
|
756
|
+
.overflow-auto{
|
757
|
+
overflow: auto
|
758
|
+
}
|
759
|
+
|
776
760
|
.whitespace-nowrap{
|
777
761
|
white-space: nowrap
|
778
762
|
}
|
779
763
|
|
764
|
+
.whitespace-pre-wrap{
|
765
|
+
white-space: pre-wrap
|
766
|
+
}
|
767
|
+
|
780
768
|
.rounded{
|
781
769
|
border-radius: 0.25rem
|
782
770
|
}
|
@@ -785,8 +773,9 @@
|
|
785
773
|
border-radius: 0.5rem
|
786
774
|
}
|
787
775
|
|
788
|
-
.rounded-
|
789
|
-
border-radius: 0.
|
776
|
+
.rounded-b-lg{
|
777
|
+
border-bottom-right-radius: 0.5rem;
|
778
|
+
border-bottom-left-radius: 0.5rem
|
790
779
|
}
|
791
780
|
|
792
781
|
.border{
|
@@ -797,14 +786,14 @@
|
|
797
786
|
border-bottom-width: 1px
|
798
787
|
}
|
799
788
|
|
800
|
-
.border-
|
789
|
+
.border-blue-500{
|
801
790
|
--tw-border-opacity: 1;
|
802
|
-
border-color: rgb(
|
791
|
+
border-color: rgb(59 130 246 / var(--tw-border-opacity))
|
803
792
|
}
|
804
793
|
|
805
|
-
.border-
|
794
|
+
.border-gray-300{
|
806
795
|
--tw-border-opacity: 1;
|
807
|
-
border-color: rgb(
|
796
|
+
border-color: rgb(209 213 219 / var(--tw-border-opacity))
|
808
797
|
}
|
809
798
|
|
810
799
|
.bg-gray-100{
|
@@ -812,18 +801,14 @@
|
|
812
801
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity))
|
813
802
|
}
|
814
803
|
|
815
|
-
.bg-
|
804
|
+
.bg-green-50{
|
816
805
|
--tw-bg-opacity: 1;
|
817
|
-
background-color: rgb(
|
806
|
+
background-color: rgb(240 253 244 / var(--tw-bg-opacity))
|
818
807
|
}
|
819
808
|
|
820
|
-
.bg-
|
821
|
-
background-color: transparent
|
822
|
-
}
|
823
|
-
|
824
|
-
.bg-white{
|
809
|
+
.bg-red-50{
|
825
810
|
--tw-bg-opacity: 1;
|
826
|
-
background-color: rgb(
|
811
|
+
background-color: rgb(254 242 242 / var(--tw-bg-opacity))
|
827
812
|
}
|
828
813
|
|
829
814
|
.bg-slate-800{
|
@@ -831,34 +816,17 @@
|
|
831
816
|
background-color: rgb(30 41 59 / var(--tw-bg-opacity))
|
832
817
|
}
|
833
818
|
|
834
|
-
.bg-
|
835
|
-
|
836
|
-
background-color: rgb(219 234 254 / var(--tw-bg-opacity))
|
837
|
-
}
|
838
|
-
|
839
|
-
.bg-red-100{
|
840
|
-
--tw-bg-opacity: 1;
|
841
|
-
background-color: rgb(254 226 226 / var(--tw-bg-opacity))
|
819
|
+
.bg-transparent{
|
820
|
+
background-color: transparent
|
842
821
|
}
|
843
822
|
|
844
|
-
.bg-
|
823
|
+
.bg-white{
|
845
824
|
--tw-bg-opacity: 1;
|
846
|
-
background-color: rgb(
|
847
|
-
}
|
848
|
-
|
849
|
-
.text-blue-800{
|
850
|
-
--tw-text-opacity: 1;
|
851
|
-
color: rgb(30 64 175 / var(--tw-text-opacity))
|
852
|
-
}
|
853
|
-
|
854
|
-
.text-red-800{
|
855
|
-
--tw-text-opacity: 1;
|
856
|
-
color: rgb(153 27 27 / var(--tw-text-opacity))
|
825
|
+
background-color: rgb(255 255 255 / var(--tw-bg-opacity))
|
857
826
|
}
|
858
827
|
|
859
|
-
.
|
860
|
-
|
861
|
-
color: rgb(133 77 14 / var(--tw-text-opacity))
|
828
|
+
.p-2{
|
829
|
+
padding: 0.5rem
|
862
830
|
}
|
863
831
|
|
864
832
|
.p-4{
|
@@ -870,10 +838,6 @@
|
|
870
838
|
padding-right: 0px
|
871
839
|
}
|
872
840
|
|
873
|
-
.p-2{
|
874
|
-
padding: 0.5rem
|
875
|
-
}
|
876
|
-
|
877
841
|
.px-2{
|
878
842
|
padding-left: 0.5rem;
|
879
843
|
padding-right: 0.5rem
|
@@ -894,6 +858,11 @@
|
|
894
858
|
padding-bottom: 0.25rem
|
895
859
|
}
|
896
860
|
|
861
|
+
.py-2{
|
862
|
+
padding-top: 0.5rem;
|
863
|
+
padding-bottom: 0.5rem
|
864
|
+
}
|
865
|
+
|
897
866
|
.py-3{
|
898
867
|
padding-top: 0.75rem;
|
899
868
|
padding-bottom: 0.75rem
|
@@ -933,10 +902,18 @@
|
|
933
902
|
padding-top: 1.75rem
|
934
903
|
}
|
935
904
|
|
905
|
+
.rounded-md{
|
906
|
+
border-radius: 0.375rem
|
907
|
+
}
|
908
|
+
|
936
909
|
.text-left{
|
937
910
|
text-align: left
|
938
911
|
}
|
939
912
|
|
913
|
+
.text-center{
|
914
|
+
text-align: center
|
915
|
+
}
|
916
|
+
|
940
917
|
.text-right{
|
941
918
|
text-align: right
|
942
919
|
}
|
@@ -950,10 +927,6 @@
|
|
950
927
|
line-height: 2rem
|
951
928
|
}
|
952
929
|
|
953
|
-
.text-\[\.75em\]{
|
954
|
-
font-size: .75em
|
955
|
-
}
|
956
|
-
|
957
930
|
.text-base{
|
958
931
|
font-size: 1rem;
|
959
932
|
line-height: 1.5rem
|
@@ -976,9 +949,23 @@
|
|
976
949
|
font-weight: 600
|
977
950
|
}
|
978
951
|
|
979
|
-
.
|
980
|
-
|
981
|
-
|
952
|
+
.leading-normal{
|
953
|
+
line-height: 1.5
|
954
|
+
}
|
955
|
+
|
956
|
+
.bg-blue-100{
|
957
|
+
--tw-bg-opacity: 1;
|
958
|
+
background-color: rgb(219 234 254 / var(--tw-bg-opacity))
|
959
|
+
}
|
960
|
+
|
961
|
+
.bg-red-100{
|
962
|
+
--tw-bg-opacity: 1;
|
963
|
+
background-color: rgb(254 226 226 / var(--tw-bg-opacity))
|
964
|
+
}
|
965
|
+
|
966
|
+
.bg-yellow-100{
|
967
|
+
--tw-bg-opacity: 1;
|
968
|
+
background-color: rgb(254 249 195 / var(--tw-bg-opacity))
|
982
969
|
}
|
983
970
|
|
984
971
|
.text-blue-400{
|
@@ -1001,11 +988,6 @@
|
|
1001
988
|
color: rgb(75 85 99 / var(--tw-text-opacity))
|
1002
989
|
}
|
1003
990
|
|
1004
|
-
.text-gray-800{
|
1005
|
-
--tw-text-opacity: 1;
|
1006
|
-
color: rgb(31 41 55 / var(--tw-text-opacity))
|
1007
|
-
}
|
1008
|
-
|
1009
991
|
.text-gray-900{
|
1010
992
|
--tw-text-opacity: 1;
|
1011
993
|
color: rgb(17 24 39 / var(--tw-text-opacity))
|
@@ -1016,10 +998,45 @@
|
|
1016
998
|
color: rgb(34 197 94 / var(--tw-text-opacity))
|
1017
999
|
}
|
1018
1000
|
|
1001
|
+
.text-red-500{
|
1002
|
+
--tw-text-opacity: 1;
|
1003
|
+
color: rgb(239 68 68 / var(--tw-text-opacity))
|
1004
|
+
}
|
1005
|
+
|
1006
|
+
.text-blue-800{
|
1007
|
+
--tw-text-opacity: 1;
|
1008
|
+
color: rgb(30 64 175 / var(--tw-text-opacity))
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
.text-red-800{
|
1012
|
+
--tw-text-opacity: 1;
|
1013
|
+
color: rgb(153 27 27 / var(--tw-text-opacity))
|
1014
|
+
}
|
1015
|
+
|
1016
|
+
.text-yellow-800{
|
1017
|
+
--tw-text-opacity: 1;
|
1018
|
+
color: rgb(133 77 14 / var(--tw-text-opacity))
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
.text-white{
|
1022
|
+
--tw-text-opacity: 1;
|
1023
|
+
color: rgb(255 255 255 / var(--tw-text-opacity))
|
1024
|
+
}
|
1025
|
+
|
1019
1026
|
.underline{
|
1020
1027
|
text-decoration-line: underline
|
1021
1028
|
}
|
1022
1029
|
|
1030
|
+
.transition-opacity{
|
1031
|
+
transition-property: opacity;
|
1032
|
+
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
1033
|
+
transition-duration: 150ms
|
1034
|
+
}
|
1035
|
+
|
1036
|
+
.opacity-0{
|
1037
|
+
opacity: 0
|
1038
|
+
}
|
1039
|
+
|
1023
1040
|
.even\:bg-gray-50:nth-child(even){
|
1024
1041
|
--tw-bg-opacity: 1;
|
1025
1042
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity))
|
@@ -1036,14 +1053,14 @@
|
|
1036
1053
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)
|
1037
1054
|
}
|
1038
1055
|
|
1039
|
-
.hover\:ring-
|
1056
|
+
.hover\:ring-blue-200:hover{
|
1040
1057
|
--tw-ring-opacity: 1;
|
1041
|
-
--tw-ring-color: rgb(
|
1058
|
+
--tw-ring-color: rgb(191 219 254 / var(--tw-ring-opacity))
|
1042
1059
|
}
|
1043
1060
|
|
1044
|
-
.hover\:ring-
|
1061
|
+
.hover\:ring-gray-200:hover{
|
1045
1062
|
--tw-ring-opacity: 1;
|
1046
|
-
--tw-ring-color: rgb(
|
1063
|
+
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity))
|
1047
1064
|
}
|
1048
1065
|
|
1049
1066
|
@media (min-width: 640px){
|
@@ -1094,21 +1111,20 @@
|
|
1094
1111
|
<% end %>
|
1095
1112
|
</div>
|
1096
1113
|
|
1097
|
-
<script>
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
}
|
1111
|
-
fadeOut(element);
|
1114
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
1115
|
+
function fadeOut(element) {
|
1116
|
+
element.classList.add('transition-opacity')
|
1117
|
+
setTimeout(
|
1118
|
+
() => {
|
1119
|
+
element.classList.add('opacity-0')
|
1120
|
+
element.remove()
|
1121
|
+
},
|
1122
|
+
2000
|
1123
|
+
)
|
1124
|
+
}
|
1125
|
+
document.querySelectorAll('[data-controller="fade"]').forEach(element => {
|
1126
|
+
fadeOut(element);
|
1127
|
+
});
|
1112
1128
|
</script>
|
1113
1129
|
</body>
|
1114
1130
|
</html>
|
@@ -8,13 +8,16 @@
|
|
8
8
|
from
|
9
9
|
<em><code><%= error.source %></code></em>
|
10
10
|
</div>
|
11
|
-
<pre class="ml-6 mt-4"><%= error.message %></pre>
|
11
|
+
<pre class="whitespace-pre-wrap ml-6 mt-4"><%= error.message %></pre>
|
12
12
|
</td>
|
13
13
|
<td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-gray-500 text-right">
|
14
14
|
<%= error.occurrences.size %>
|
15
15
|
</td>
|
16
16
|
<td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-gray-500 text-right">
|
17
|
-
|
17
|
+
<% last_seen_at = error.occurrences.maximum(:created_at) %>
|
18
|
+
<abbr title="<%= last_seen_at.iso8601 %>" class="cursor-help">
|
19
|
+
<%= time_tag last_seen_at, time_ago_in_words(last_seen_at, scope: 'datetime.distance_in_words.short') %>
|
20
|
+
</abbr>
|
18
21
|
</td>
|
19
22
|
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-3">
|
20
23
|
<%= 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 %>
|
@@ -74,20 +74,24 @@
|
|
74
74
|
<em><%= @error.source %></em>
|
75
75
|
</dd>
|
76
76
|
</div>
|
77
|
-
<div class="flex items-
|
77
|
+
<div class="flex items-start justify-between flex-wrap gap-x-2">
|
78
78
|
<dt class="font-bold">
|
79
79
|
<%= SolidErrors::Error.human_attribute_name(:project_root) %>
|
80
80
|
</dt>
|
81
|
-
<dd class="
|
81
|
+
<dd class="">
|
82
82
|
<span><%= SolidErrors::BacktraceLine::RAILS_ROOT %></span>
|
83
83
|
</dd>
|
84
84
|
</div>
|
85
|
-
<div class="flex items-
|
85
|
+
<div class="flex items-start justify-between flex-wrap gap-x-2">
|
86
86
|
<dt class="font-bold">
|
87
87
|
<%= SolidErrors::Error.human_attribute_name(:gem_root) %>
|
88
88
|
</dt>
|
89
|
-
<dd class="
|
90
|
-
<
|
89
|
+
<dd class="">
|
90
|
+
<ul>
|
91
|
+
<% Gem.path.each do |path| %>
|
92
|
+
<li><%= path %></li>
|
93
|
+
<% end %>
|
94
|
+
</ul>
|
91
95
|
</dd>
|
92
96
|
</div>
|
93
97
|
</dl>
|
@@ -95,7 +99,8 @@
|
|
95
99
|
<%= render "solid_errors/errors/actions", error: @error %>
|
96
100
|
<% end %>
|
97
101
|
|
98
|
-
<
|
102
|
+
<br>
|
103
|
+
<br>
|
99
104
|
|
100
105
|
<%= render "solid_errors/occurrences/collection",
|
101
106
|
occurrences: @error.occurrences,
|
@@ -10,12 +10,12 @@
|
|
10
10
|
<div class="">
|
11
11
|
<dl class="ml-6">
|
12
12
|
<% occurrence.context&.each do |key, value| %>
|
13
|
-
<div class="flex items-center justify-
|
13
|
+
<div class="flex items-center justify-start flex-wrap gap-x-2">
|
14
14
|
<dt class="font-bold">
|
15
15
|
<%= SolidErrors::Occurrence.human_attribute_name(key) %>
|
16
16
|
</dt>
|
17
17
|
<dd class="">
|
18
|
-
|
18
|
+
<code><%= value %></code>
|
19
19
|
</dd>
|
20
20
|
</div>
|
21
21
|
<% end %>
|
@@ -27,9 +27,9 @@
|
|
27
27
|
<% backtrace.lines.each_with_index do |line, i| %>
|
28
28
|
<%= tag.details open: line.application? || i.zero? do %>
|
29
29
|
<summary class="hover:bg-gray-50 px-2 py-1 rounded cursor-pointer">
|
30
|
-
<span class="text-gray-500"><%= File.dirname(line.filtered_file) %>/</span><span class="text-blue-500"><%= File.basename(line.filtered_file) %></span>:<span class="text-gray-900"><%= line.filtered_number %></span>
|
30
|
+
<span class="text-gray-500"><%= File.dirname(line.filtered_file) %>/</span><span class="text-blue-500 font-medium"><%= File.basename(line.filtered_file) %></span>:<span class="text-gray-900 font-medium"><%= line.filtered_number %></span>
|
31
31
|
<span class="text-gray-500">in</span>
|
32
|
-
<code class="text-green-500"><%= line.filtered_method %></code>
|
32
|
+
<code class="text-green-500 font-medium"><%= line.filtered_method %></code>
|
33
33
|
</summary>
|
34
34
|
<div><pre class="flex overflow-auto rounded-b-lg bg-slate-800 p-4 text-sm leading-normal text-white sm:rounded-t-lg"><code class="flex flex-col min-h-full min-w-content px-0"><% line.source.each do |n, code| %>
|
35
35
|
<div class="line"><span class="mr-2 text-right select-none text-gray-600"><%= n %></span><span><%= code %></span></div>
|
data/config/routes.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record"
|
5
5
|
|
6
6
|
module SolidErrors
|
7
7
|
#
|
@@ -13,14 +13,14 @@ module SolidErrors
|
|
13
13
|
|
14
14
|
source_root File.expand_path("templates", __dir__)
|
15
15
|
|
16
|
-
class_option :database, type: :string, aliases: %i
|
16
|
+
class_option :database, type: :string, aliases: %i[--db], desc: "The database for your migration. By default, the current environment's primary database is used."
|
17
17
|
class_option :skip_migrations, type: :boolean, default: nil, desc: "Skip migrations"
|
18
18
|
|
19
19
|
# Generates monolithic migration file that contains all database changes.
|
20
20
|
def create_migration_file
|
21
21
|
return if options[:skip_migrations]
|
22
22
|
|
23
|
-
migration_template
|
23
|
+
migration_template "create_solid_errors_tables.rb.erb", File.join(db_migrate_path, "create_solid_errors_tables.rb")
|
24
24
|
end
|
25
25
|
|
26
26
|
private
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module SolidErrors
|
2
2
|
# adapted from: https://github.com/honeybadger-io/honeybadger-ruby/blob/master/lib/honeybadger/util/sanitizer.rb
|
3
3
|
class Sanitizer
|
4
|
-
BASIC_OBJECT =
|
5
|
-
DEPTH =
|
6
|
-
RAISED =
|
7
|
-
RECURSION =
|
8
|
-
TRUNCATED =
|
4
|
+
BASIC_OBJECT = "#<BasicObject>".freeze
|
5
|
+
DEPTH = "[DEPTH]".freeze
|
6
|
+
RAISED = "[RAISED]".freeze
|
7
|
+
RECURSION = "[RECURSION]".freeze
|
8
|
+
TRUNCATED = "[TRUNCATED]".freeze
|
9
9
|
MAX_STRING_SIZE = 65536
|
10
10
|
|
11
11
|
def self.sanitize(data)
|
@@ -21,7 +21,7 @@ module SolidErrors
|
|
21
21
|
return BASIC_OBJECT if basic_object?(data)
|
22
22
|
|
23
23
|
if recursive?(data)
|
24
|
-
return RECURSION if stack
|
24
|
+
return RECURSION if stack&.include?(data.object_id)
|
25
25
|
|
26
26
|
stack = stack ? stack.dup : Set.new
|
27
27
|
stack << data.object_id
|
@@ -33,8 +33,8 @@ module SolidErrors
|
|
33
33
|
|
34
34
|
new_hash = {}
|
35
35
|
data.each do |key, value|
|
36
|
-
key = key.
|
37
|
-
value = sanitize(value, depth+1, stack)
|
36
|
+
key = key.is_a?(Symbol) ? key : sanitize(key, depth + 1, stack)
|
37
|
+
value = sanitize(value, depth + 1, stack)
|
38
38
|
new_hash[key] = value
|
39
39
|
end
|
40
40
|
new_hash
|
@@ -42,7 +42,7 @@ module SolidErrors
|
|
42
42
|
return DEPTH if depth >= max_depth
|
43
43
|
|
44
44
|
data.to_a.map do |value|
|
45
|
-
sanitize(value, depth+1, stack)
|
45
|
+
sanitize(value, depth + 1, stack)
|
46
46
|
end
|
47
47
|
when Numeric, TrueClass, FalseClass, NilClass
|
48
48
|
data
|
@@ -64,28 +64,29 @@ module SolidErrors
|
|
64
64
|
end
|
65
65
|
|
66
66
|
private
|
67
|
-
attr_reader :max_depth
|
68
|
-
|
69
|
-
def basic_object?(object)
|
70
|
-
object.respond_to?(:to_s)
|
71
|
-
false
|
72
|
-
rescue
|
73
|
-
# BasicObject doesn't respond to `#respond_to?`.
|
74
|
-
true
|
75
|
-
end
|
76
67
|
|
77
|
-
|
78
|
-
data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Set)
|
79
|
-
end
|
68
|
+
attr_reader :max_depth
|
80
69
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
86
77
|
|
87
|
-
|
88
|
-
|
89
|
-
|
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.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
|
90
91
|
end
|
91
92
|
end
|
@@ -1,26 +1,25 @@
|
|
1
1
|
module SolidErrors
|
2
2
|
class Subscriber
|
3
|
-
IGNORED_ERRORS = [
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
24
23
|
|
25
24
|
def report(error, handled:, severity:, context:, source: nil)
|
26
25
|
return if ignore_by_class?(error.class.name)
|
data/lib/solid_errors/version.rb
CHANGED
data/lib/solid_errors.rb
CHANGED
@@ -7,4 +7,20 @@ require_relative "solid_errors/engine"
|
|
7
7
|
|
8
8
|
module SolidErrors
|
9
9
|
mattr_accessor :connects_to
|
10
|
+
mattr_accessor :username
|
11
|
+
mattr_accessor :password
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# use method instead of attr_accessor to ensure
|
15
|
+
# this works if variable set after SolidErrors is loaded
|
16
|
+
def username
|
17
|
+
@username ||= ENV["SOLIDERRORS_USERNAME"]
|
18
|
+
end
|
19
|
+
|
20
|
+
# use method instead of attr_accessor to ensure
|
21
|
+
# this works if variable set after SolidErrors is loaded
|
22
|
+
def password
|
23
|
+
@password ||= ENV["SOLIDERRORS_PASSWORD"]
|
24
|
+
end
|
25
|
+
end
|
10
26
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_errors
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Margheim
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-01-
|
11
|
+
date: 2024-01-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- stephen.margheim@gmail.com
|