snitch-rails 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +54 -3
- data/app/assets/builds/snitch/application.css +2 -0
- data/app/assets/stylesheets/snitch/application.css +2 -0
- data/app/controllers/snitch/snitches_controller.rb +35 -0
- data/app/views/layouts/snitch/application.html.erb +51 -0
- data/app/views/snitch/snitches/index.html.erb +125 -0
- data/app/views/snitch/snitches/show.html.erb +99 -0
- data/config/routes.rb +4 -0
- data/lib/generators/snitch/install/install_generator.rb +4 -0
- data/lib/generators/snitch/install/templates/create_snitch_errors.rb.erb +2 -0
- data/lib/generators/snitch/update/templates/add_status_to_snitch_errors.rb.erb +6 -0
- data/lib/generators/snitch/update/update_generator.rb +17 -0
- data/lib/snitch/exception_handler.rb +4 -1
- data/lib/snitch/models/event.rb +6 -0
- data/lib/snitch/version.rb +1 -1
- data/lib/tasks/snitch_tailwind.rake +22 -0
- metadata +25 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c4a5dc4773dd228a4f9478cd08a0b1f3b6a4160500340dd692beca1395f0f49
|
|
4
|
+
data.tar.gz: 95b3c04d9b95d0e15c426c12746bf0fcefaefd47e35aeba898cd21fc26ba47b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1592e5587346cac3b8bd92b6f9536a6f890c7922df77ad3e47423b3139a0692a4367f69b80eee1702abc5eff63cd5bc99ebdce3c59db5e8a54ee91d282e8ed4
|
|
7
|
+
data.tar.gz: d0b95a4e8fa41d368af44a6b0efdfd1c70c1e9dfdf9c072faeb5316a62e19ef2a8c081a317a6e32a2be0fe70da969e8647e720c30a2e46dce88219a7ac35874b
|
data/README.md
CHANGED
|
@@ -4,6 +4,16 @@ Snitch catches unhandled exceptions in your Rails application, persists them to
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Automatic exception capture via Rack middleware
|
|
10
|
+
- Fingerprinting and deduplication (increments occurrence count for repeat exceptions)
|
|
11
|
+
- GitHub issue creation with full backtrace, request context, and @mentions
|
|
12
|
+
- **Dashboard** at `/snitches` with tabbed views: Open, Closed, and Ignored
|
|
13
|
+
- Ignore exceptions via config or directly from the dashboard
|
|
14
|
+
- Manual exception reporting for rescued exceptions
|
|
15
|
+
- Upgrade generator for existing installations
|
|
16
|
+
|
|
7
17
|
## Installation
|
|
8
18
|
|
|
9
19
|
Add Snitch to your Gemfile:
|
|
@@ -18,7 +28,7 @@ Run bundle install:
|
|
|
18
28
|
bundle install
|
|
19
29
|
```
|
|
20
30
|
|
|
21
|
-
Run the install generator to create the migration and
|
|
31
|
+
Run the install generator to create the migration, initializer, and mount the engine routes:
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
rails generate snitch:install
|
|
@@ -30,6 +40,29 @@ Run the migration:
|
|
|
30
40
|
rails db:migrate
|
|
31
41
|
```
|
|
32
42
|
|
|
43
|
+
The installer automatically adds the following route to your application:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
mount Snitch::Engine, at: "/snitches"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you need to mount it manually or at a different path, add the above line to your `config/routes.rb`.
|
|
50
|
+
|
|
51
|
+
### Upgrading from a previous version
|
|
52
|
+
|
|
53
|
+
If you're upgrading from a version prior to 0.3.0, run the update generator to add the `status` column to the `snitch_errors` table:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
rails generate snitch:update
|
|
57
|
+
rails db:migrate
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then mount the engine in your `config/routes.rb`:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
mount Snitch::Engine, at: "/snitches"
|
|
64
|
+
```
|
|
65
|
+
|
|
33
66
|
## Configuration
|
|
34
67
|
|
|
35
68
|
The generator creates an initializer at `config/initializers/snitch.rb`. Update it with your settings:
|
|
@@ -55,7 +88,26 @@ end
|
|
|
55
88
|
|
|
56
89
|
### GitHub Token
|
|
57
90
|
|
|
58
|
-
Create a [personal access token](https://github.com/settings/tokens) with the `repo` scope and set it as an environment variable
|
|
91
|
+
Create a [personal access token](https://github.com/settings/tokens) with the `repo` scope and set it as an environment variable.
|
|
92
|
+
|
|
93
|
+
## Dashboard
|
|
94
|
+
|
|
95
|
+
Visit `/snitches` in your browser to view the Snitch dashboard. The dashboard provides three tabs:
|
|
96
|
+
|
|
97
|
+
- **Open** — Exceptions that need attention. Shows the most recently occurred first.
|
|
98
|
+
- **Closed** — Exceptions that have been resolved. You can reopen them if they recur.
|
|
99
|
+
- **Ignored** — Exceptions you've chosen to suppress. Ignored exceptions will not create new GitHub issues even if they occur again.
|
|
100
|
+
|
|
101
|
+
From the dashboard you can change the status of any exception:
|
|
102
|
+
- Open exceptions can be **closed** or **ignored**
|
|
103
|
+
- Closed or ignored exceptions can be **reopened**
|
|
104
|
+
|
|
105
|
+
### Ignoring Exceptions
|
|
106
|
+
|
|
107
|
+
There are two ways to ignore exceptions:
|
|
108
|
+
|
|
109
|
+
1. **Via configuration** — Add exception classes to `config.ignored_exceptions` in the initializer. These are never captured.
|
|
110
|
+
2. **Via the dashboard** — Mark individual exceptions as "Ignored" from the Open tab. These exceptions are already captured but future occurrences won't create new GitHub issues.
|
|
59
111
|
|
|
60
112
|
## Manual Reporting
|
|
61
113
|
|
|
@@ -85,7 +137,6 @@ The same fingerprinting and deduplication rules apply. If the same exception is
|
|
|
85
137
|
## Roadmap
|
|
86
138
|
|
|
87
139
|
- [ ] multi-db support
|
|
88
|
-
- [ ] simple dashboard to view captured exceptions (linked to gh issue)
|
|
89
140
|
- [ ] webhook to resolve snitch record, when issues resolve
|
|
90
141
|
|
|
91
142
|
## Requirements
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-green-50:oklch(98.2% .018 155.826);--color-green-200:oklch(92.5% .084 155.995);--color-green-500:oklch(72.3% .219 149.579);--color-green-700:oklch(52.7% .154 150.069);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.end{inset-inline-end:var(--spacing)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-5{height:calc(var(--spacing) * 5)}.h-7{height:calc(var(--spacing) * 7)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-12{width:calc(var(--spacing) * 12)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[14rem\]{max-width:14rem}.max-w-xs{max-width:var(--container-xs)}.min-w-\[2rem\]{min-width:2rem}.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-x-1{column-gap:calc(var(--spacing) * 1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-amber-300{border-color:var(--color-amber-300)}.border-amber-500{border-color:var(--color-amber-500)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-500{border-color:var(--color-gray-500)}.border-green-200{border-color:var(--color-green-200)}.border-indigo-300{border-color:var(--color-indigo-300)}.border-indigo-500{border-color:var(--color-indigo-500)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-white{background-color:var(--color-white)}.p-4{padding:calc(var(--spacing) * 4)}.p-12{padding:calc(var(--spacing) * 12)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-8{padding-block:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-700{color:var(--color-green-700)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-indigo-600:hover{color:var(--color-indigo-600)}.hover\:text-indigo-800:hover{color:var(--color-indigo-800)}}@media (min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:64rem){.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}}}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Snitch
|
|
2
|
+
class SnitchesController < ActionController::Base
|
|
3
|
+
layout "snitch/application"
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@tab = params[:tab] || "open"
|
|
7
|
+
@events = case @tab
|
|
8
|
+
when "open" then Snitch::Event.open.order(last_occurred_at: :desc)
|
|
9
|
+
when "closed" then Snitch::Event.closed.order(updated_at: :desc)
|
|
10
|
+
when "ignored" then Snitch::Event.ignored.order(updated_at: :desc)
|
|
11
|
+
else Snitch::Event.open.order(last_occurred_at: :desc)
|
|
12
|
+
end
|
|
13
|
+
@counts = {
|
|
14
|
+
open: Snitch::Event.open.count,
|
|
15
|
+
closed: Snitch::Event.closed.count,
|
|
16
|
+
ignored: Snitch::Event.ignored.count
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def show
|
|
21
|
+
@event = Snitch::Event.find(params[:id])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update
|
|
25
|
+
@event = Snitch::Event.find(params[:id])
|
|
26
|
+
@event.update!(status: params[:status])
|
|
27
|
+
|
|
28
|
+
if params[:redirect_to] == "show"
|
|
29
|
+
redirect_to snitch_path(@event), notice: "Snitch updated."
|
|
30
|
+
else
|
|
31
|
+
redirect_to snitches_path(tab: params[:tab] || @event.status), notice: "Snitch updated."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Snitch</title>
|
|
7
|
+
<style>
|
|
8
|
+
<%= File.read(Snitch::Engine.root.join("app/assets/builds/snitch/application.css")).html_safe %>
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-gray-50 min-h-screen">
|
|
12
|
+
<header class="bg-white border-b border-gray-200">
|
|
13
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
14
|
+
<div class="flex items-center justify-between h-16">
|
|
15
|
+
<div class="flex items-center gap-3">
|
|
16
|
+
<svg class="w-7 h-7 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
17
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
18
|
+
</svg>
|
|
19
|
+
<%= link_to "Snitch", snitches_path, class: "text-xl font-bold text-gray-900 hover:text-indigo-600 transition-colors" %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
26
|
+
<% if notice %>
|
|
27
|
+
<div class="mb-6 rounded-lg bg-green-50 border border-green-200 p-4">
|
|
28
|
+
<div class="flex items-center">
|
|
29
|
+
<svg class="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
31
|
+
</svg>
|
|
32
|
+
<p class="text-sm text-green-700"><%= notice %></p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<% if alert %>
|
|
38
|
+
<div class="mb-6 rounded-lg bg-red-50 border border-red-200 p-4">
|
|
39
|
+
<div class="flex items-center">
|
|
40
|
+
<svg class="w-5 h-5 text-red-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
41
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
42
|
+
</svg>
|
|
43
|
+
<p class="text-sm text-red-700"><%= alert %></p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<%= yield %>
|
|
49
|
+
</main>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="border-b border-gray-200 mb-6">
|
|
3
|
+
<nav class="flex gap-x-1" aria-label="Tabs">
|
|
4
|
+
<% [
|
|
5
|
+
["open", "Open", @counts[:open], "indigo"],
|
|
6
|
+
["closed", "Closed", @counts[:closed], "gray"],
|
|
7
|
+
["ignored", "Ignored", @counts[:ignored], "amber"]
|
|
8
|
+
].each do |tab_key, tab_label, count, color| %>
|
|
9
|
+
<% active = @tab == tab_key %>
|
|
10
|
+
<%= link_to snitches_path(tab: tab_key),
|
|
11
|
+
class: "flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors #{
|
|
12
|
+
if active
|
|
13
|
+
case color
|
|
14
|
+
when 'indigo' then 'border-indigo-500 text-indigo-600'
|
|
15
|
+
when 'gray' then 'border-gray-500 text-gray-700'
|
|
16
|
+
when 'amber' then 'border-amber-500 text-amber-600'
|
|
17
|
+
end
|
|
18
|
+
else
|
|
19
|
+
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
20
|
+
end
|
|
21
|
+
}" do %>
|
|
22
|
+
<%= tab_label %>
|
|
23
|
+
<span class="rounded-full px-2.5 py-0.5 text-xs font-medium <%=
|
|
24
|
+
if active
|
|
25
|
+
case color
|
|
26
|
+
when 'indigo' then 'bg-indigo-100 text-indigo-700'
|
|
27
|
+
when 'gray' then 'bg-gray-100 text-gray-700'
|
|
28
|
+
when 'amber' then 'bg-amber-100 text-amber-700'
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
'bg-gray-100 text-gray-600'
|
|
32
|
+
end
|
|
33
|
+
%>"><%= count %></span>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% end %>
|
|
36
|
+
</nav>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<% if @events.any? %>
|
|
40
|
+
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-x-auto">
|
|
41
|
+
<table class="w-full divide-y divide-gray-200">
|
|
42
|
+
<thead class="bg-gray-50">
|
|
43
|
+
<tr>
|
|
44
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Exception</th>
|
|
45
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Message</th>
|
|
46
|
+
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">Count</th>
|
|
47
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Last Occurred</th>
|
|
48
|
+
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Issue</th>
|
|
49
|
+
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">Actions</th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody class="divide-y divide-gray-200">
|
|
53
|
+
<% @events.each do |event| %>
|
|
54
|
+
<tr class="hover:bg-gray-50 transition-colors">
|
|
55
|
+
<td class="px-6 py-4 max-w-[14rem]">
|
|
56
|
+
<%= link_to snitch_path(event), class: "font-mono text-sm font-semibold text-indigo-600 hover:text-indigo-800 truncate block" do %>
|
|
57
|
+
<%= truncate(event.exception_class, length: 30) %>
|
|
58
|
+
<% end %>
|
|
59
|
+
</td>
|
|
60
|
+
<td class="px-6 py-4 max-w-xs">
|
|
61
|
+
<p class="text-sm text-gray-600 truncate"><%= truncate(event.message, length: 80) %></p>
|
|
62
|
+
</td>
|
|
63
|
+
<td class="px-6 py-4 text-center">
|
|
64
|
+
<span class="inline-flex items-center justify-center min-w-[2rem] rounded-full px-2.5 py-0.5 text-xs font-semibold <%= event.occurrence_count > 10 ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700' %>">
|
|
65
|
+
<%= event.occurrence_count %>
|
|
66
|
+
</span>
|
|
67
|
+
</td>
|
|
68
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
69
|
+
<span class="text-sm text-gray-500"><%= event.last_occurred_at&.strftime("%b %d, %Y %l:%M %p") %></span>
|
|
70
|
+
</td>
|
|
71
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
72
|
+
<% if event.github_issue_url.present? %>
|
|
73
|
+
<a href="<%= event.github_issue_url %>" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1 text-sm text-indigo-600 hover:text-indigo-800 font-medium">
|
|
74
|
+
#<%= event.github_issue_number %>
|
|
75
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
76
|
+
</a>
|
|
77
|
+
<% else %>
|
|
78
|
+
<span class="text-sm text-gray-400">—</span>
|
|
79
|
+
<% end %>
|
|
80
|
+
</td>
|
|
81
|
+
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
82
|
+
<div class="flex items-center justify-end gap-2">
|
|
83
|
+
<% if @tab == "open" %>
|
|
84
|
+
<%= form_with url: snitch_path(event), method: :patch, local: true, class: "inline" do |f| %>
|
|
85
|
+
<%= f.hidden_field :status, value: "closed" %>
|
|
86
|
+
<%= f.hidden_field :tab, value: @tab %>
|
|
87
|
+
<%= f.submit "Close", class: "inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 transition-colors cursor-pointer" %>
|
|
88
|
+
<% end %>
|
|
89
|
+
<%= form_with url: snitch_path(event), method: :patch, local: true, class: "inline" do |f| %>
|
|
90
|
+
<%= f.hidden_field :status, value: "ignored" %>
|
|
91
|
+
<%= f.hidden_field :tab, value: @tab %>
|
|
92
|
+
<%= f.submit "Ignore", class: "inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg border border-amber-300 text-amber-700 bg-amber-50 hover:bg-amber-100 transition-colors cursor-pointer" %>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% else %>
|
|
95
|
+
<%= form_with url: snitch_path(event), method: :patch, local: true, class: "inline" do |f| %>
|
|
96
|
+
<%= f.hidden_field :status, value: "open" %>
|
|
97
|
+
<%= f.hidden_field :tab, value: @tab %>
|
|
98
|
+
<%= f.submit "Reopen", class: "inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-lg border border-indigo-300 text-indigo-700 bg-indigo-50 hover:bg-indigo-100 transition-colors cursor-pointer" %>
|
|
99
|
+
<% end %>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
</td>
|
|
103
|
+
</tr>
|
|
104
|
+
<% end %>
|
|
105
|
+
</tbody>
|
|
106
|
+
</table>
|
|
107
|
+
</div>
|
|
108
|
+
<% else %>
|
|
109
|
+
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
|
110
|
+
<svg class="mx-auto w-12 h-12 text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
111
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
112
|
+
</svg>
|
|
113
|
+
<h3 class="text-sm font-semibold text-gray-900 mb-1">No <%= @tab %> exceptions</h3>
|
|
114
|
+
<p class="text-sm text-gray-500">
|
|
115
|
+
<% if @tab == "open" %>
|
|
116
|
+
All clear! No unhandled exceptions to review.
|
|
117
|
+
<% elsif @tab == "closed" %>
|
|
118
|
+
No exceptions have been closed yet.
|
|
119
|
+
<% else %>
|
|
120
|
+
No exceptions are being ignored.
|
|
121
|
+
<% end %>
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<% end %>
|
|
125
|
+
</div>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="mb-6">
|
|
3
|
+
<%= link_to "← Back to list".html_safe, snitches_path(tab: @event.status), class: "text-sm text-gray-500 hover:text-gray-700 transition-colors" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
7
|
+
<div class="px-6 py-5 border-b border-gray-200 flex items-center justify-between">
|
|
8
|
+
<div>
|
|
9
|
+
<h1 class="font-mono text-lg font-bold text-gray-900"><%= @event.exception_class %></h1>
|
|
10
|
+
<p class="text-sm text-gray-500 mt-1"><%= @event.message %></p>
|
|
11
|
+
</div>
|
|
12
|
+
<span class="inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold <%=
|
|
13
|
+
case @event.status
|
|
14
|
+
when 'open' then 'bg-indigo-100 text-indigo-700'
|
|
15
|
+
when 'closed' then 'bg-gray-100 text-gray-700'
|
|
16
|
+
when 'ignored' then 'bg-amber-100 text-amber-700'
|
|
17
|
+
end
|
|
18
|
+
%>"><%= @event.status.capitalize %></span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="px-6 py-5 border-b border-gray-200">
|
|
22
|
+
<div class="grid grid-cols-2 gap-6 sm:grid-cols-4">
|
|
23
|
+
<div>
|
|
24
|
+
<dt class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Occurrences</dt>
|
|
25
|
+
<dd class="mt-1 text-sm font-semibold text-gray-900"><%= @event.occurrence_count %></dd>
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<dt class="text-xs font-semibold text-gray-500 uppercase tracking-wider">First seen</dt>
|
|
29
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @event.first_occurred_at&.strftime("%b %d, %Y %l:%M %p") %></dd>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<dt class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Last seen</dt>
|
|
33
|
+
<dd class="mt-1 text-sm text-gray-900"><%= @event.last_occurred_at&.strftime("%b %d, %Y %l:%M %p") %></dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<dt class="text-xs font-semibold text-gray-500 uppercase tracking-wider">GitHub issue</dt>
|
|
37
|
+
<dd class="mt-1 text-sm">
|
|
38
|
+
<% if @event.github_issue_url.present? %>
|
|
39
|
+
<a href="<%= @event.github_issue_url %>" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1 text-indigo-600 hover:text-indigo-800 font-medium">
|
|
40
|
+
#<%= @event.github_issue_number %>
|
|
41
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
|
42
|
+
</a>
|
|
43
|
+
<% else %>
|
|
44
|
+
<span class="text-gray-400">—</span>
|
|
45
|
+
<% end %>
|
|
46
|
+
</dd>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<% if @event.request_url.present? %>
|
|
52
|
+
<div class="px-6 py-5 border-b border-gray-200">
|
|
53
|
+
<h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Request</h2>
|
|
54
|
+
<div class="text-sm text-gray-900">
|
|
55
|
+
<span class="inline-flex items-center rounded px-2 py-0.5 text-xs font-semibold bg-gray-100 text-gray-700 mr-2"><%= @event.request_method %></span>
|
|
56
|
+
<span class="font-mono text-sm"><%= @event.request_url %></span>
|
|
57
|
+
</div>
|
|
58
|
+
<% if @event.request_params.present? %>
|
|
59
|
+
<pre class="mt-3 bg-gray-50 rounded-lg p-4 text-xs font-mono text-gray-700 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@event.request_params)) rescue @event.request_params %></pre>
|
|
60
|
+
<% end %>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<% if @event.backtrace.present? %>
|
|
65
|
+
<div class="px-6 py-5 border-b border-gray-200">
|
|
66
|
+
<h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Backtrace</h2>
|
|
67
|
+
<div class="bg-gray-50 rounded-lg p-4 overflow-x-auto">
|
|
68
|
+
<% @event.backtrace.each_with_index do |line, i| %>
|
|
69
|
+
<div class="flex gap-3 py-0.5 text-xs font-mono <%= line.include?('/app/') ? 'text-gray-900 font-medium' : 'text-gray-400' %>">
|
|
70
|
+
<span class="text-gray-300 select-none w-6 text-right shrink-0"><%= i + 1 %></span>
|
|
71
|
+
<span class="break-all"><%= line %></span>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
|
|
78
|
+
<div class="px-6 py-5 flex items-center gap-3">
|
|
79
|
+
<% if @event.status == "open" %>
|
|
80
|
+
<%= form_with url: snitch_path(@event), method: :patch, local: true, class: "inline" do |f| %>
|
|
81
|
+
<%= f.hidden_field :status, value: "closed" %>
|
|
82
|
+
<%= f.hidden_field :redirect_to, value: "show" %>
|
|
83
|
+
<%= f.submit "Close", class: "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 transition-colors cursor-pointer" %>
|
|
84
|
+
<% end %>
|
|
85
|
+
<%= form_with url: snitch_path(@event), method: :patch, local: true, class: "inline" do |f| %>
|
|
86
|
+
<%= f.hidden_field :status, value: "ignored" %>
|
|
87
|
+
<%= f.hidden_field :redirect_to, value: "show" %>
|
|
88
|
+
<%= f.submit "Ignore", class: "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg border border-amber-300 text-amber-700 bg-amber-50 hover:bg-amber-100 transition-colors cursor-pointer" %>
|
|
89
|
+
<% end %>
|
|
90
|
+
<% else %>
|
|
91
|
+
<%= form_with url: snitch_path(@event), method: :patch, local: true, class: "inline" do |f| %>
|
|
92
|
+
<%= f.hidden_field :status, value: "open" %>
|
|
93
|
+
<%= f.hidden_field :redirect_to, value: "show" %>
|
|
94
|
+
<%= f.submit "Reopen", class: "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg border border-indigo-300 text-indigo-700 bg-indigo-50 hover:bg-indigo-100 transition-colors cursor-pointer" %>
|
|
95
|
+
<% end %>
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -11,6 +11,7 @@ class CreateSnitchErrors < ActiveRecord::Migration[<%= ActiveRecord::Migration.c
|
|
|
11
11
|
t.integer :occurrence_count, default: 1
|
|
12
12
|
t.integer :github_issue_number
|
|
13
13
|
t.string :github_issue_url
|
|
14
|
+
t.string :status, default: "open", null: false
|
|
14
15
|
t.datetime :first_occurred_at
|
|
15
16
|
t.datetime :last_occurred_at
|
|
16
17
|
t.timestamps
|
|
@@ -18,5 +19,6 @@ class CreateSnitchErrors < ActiveRecord::Migration[<%= ActiveRecord::Migration.c
|
|
|
18
19
|
|
|
19
20
|
add_index :snitch_errors, :fingerprint, unique: true
|
|
20
21
|
add_index :snitch_errors, :exception_class
|
|
22
|
+
add_index :snitch_errors, :status
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Snitch
|
|
5
|
+
module Generators
|
|
6
|
+
class UpdateGenerator < Rails::Generators::Base
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
def create_migration_file
|
|
12
|
+
migration_template "add_status_to_snitch_errors.rb.erb",
|
|
13
|
+
"db/migrate/add_status_to_snitch_errors.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -16,10 +16,13 @@ module Snitch
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
18
|
def ignored?(exception)
|
|
19
|
-
Snitch.configuration.ignored_exceptions.any? do |ignored|
|
|
19
|
+
config_ignored = Snitch.configuration.ignored_exceptions.any? do |ignored|
|
|
20
20
|
ignored_class = ignored.is_a?(String) ? ignored.safe_constantize : ignored
|
|
21
21
|
ignored_class && exception.is_a?(ignored_class)
|
|
22
22
|
end
|
|
23
|
+
return true if config_ignored
|
|
24
|
+
|
|
25
|
+
Snitch::Event.ignored.where(exception_class: exception.class.name).exists?
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def extract_request_data(env)
|
data/lib/snitch/models/event.rb
CHANGED
|
@@ -2,12 +2,18 @@ module Snitch
|
|
|
2
2
|
class Event < ActiveRecord::Base
|
|
3
3
|
self.table_name = "snitch_errors"
|
|
4
4
|
|
|
5
|
+
STATUSES = %w[open closed ignored].freeze
|
|
6
|
+
|
|
5
7
|
serialize :backtrace, coder: JSON
|
|
6
8
|
serialize :request_params, coder: JSON
|
|
7
9
|
|
|
8
10
|
validates :exception_class, presence: true
|
|
9
11
|
validates :fingerprint, presence: true
|
|
12
|
+
validates :status, inclusion: { in: STATUSES }
|
|
10
13
|
|
|
11
14
|
scope :by_fingerprint, ->(fp) { where(fingerprint: fp) }
|
|
15
|
+
scope :open, -> { where(status: "open") }
|
|
16
|
+
scope :closed, -> { where(status: "closed") }
|
|
17
|
+
scope :ignored, -> { where(status: "ignored") }
|
|
12
18
|
end
|
|
13
19
|
end
|
data/lib/snitch/version.rb
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
namespace :snitch do
|
|
2
|
+
namespace :tailwind do
|
|
3
|
+
desc "Build Snitch Tailwind CSS"
|
|
4
|
+
task :build do
|
|
5
|
+
require "tailwindcss/ruby"
|
|
6
|
+
|
|
7
|
+
gem_root = File.expand_path("../..", __dir__)
|
|
8
|
+
input = File.join(gem_root, "app/assets/stylesheets/snitch/application.css")
|
|
9
|
+
output = File.join(gem_root, "app/assets/builds/snitch/application.css")
|
|
10
|
+
|
|
11
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
12
|
+
|
|
13
|
+
system(Tailwindcss::Ruby.executable,
|
|
14
|
+
"-i", input,
|
|
15
|
+
"-o", output,
|
|
16
|
+
"--minify",
|
|
17
|
+
exception: true)
|
|
18
|
+
|
|
19
|
+
puts "Snitch Tailwind CSS compiled successfully"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: snitch-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- RiseKit
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '9.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: tailwindcss-ruby
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '4.0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: rspec-rails
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -115,9 +129,18 @@ extra_rdoc_files: []
|
|
|
115
129
|
files:
|
|
116
130
|
- LICENSE.txt
|
|
117
131
|
- README.md
|
|
132
|
+
- app/assets/builds/snitch/application.css
|
|
133
|
+
- app/assets/stylesheets/snitch/application.css
|
|
134
|
+
- app/controllers/snitch/snitches_controller.rb
|
|
135
|
+
- app/views/layouts/snitch/application.html.erb
|
|
136
|
+
- app/views/snitch/snitches/index.html.erb
|
|
137
|
+
- app/views/snitch/snitches/show.html.erb
|
|
138
|
+
- config/routes.rb
|
|
118
139
|
- lib/generators/snitch/install/install_generator.rb
|
|
119
140
|
- lib/generators/snitch/install/templates/create_snitch_errors.rb.erb
|
|
120
141
|
- lib/generators/snitch/install/templates/snitch.rb
|
|
142
|
+
- lib/generators/snitch/update/templates/add_status_to_snitch_errors.rb.erb
|
|
143
|
+
- lib/generators/snitch/update/update_generator.rb
|
|
121
144
|
- lib/snitch.rb
|
|
122
145
|
- lib/snitch/configuration.rb
|
|
123
146
|
- lib/snitch/engine.rb
|
|
@@ -128,6 +151,7 @@ files:
|
|
|
128
151
|
- lib/snitch/middleware.rb
|
|
129
152
|
- lib/snitch/models/event.rb
|
|
130
153
|
- lib/snitch/version.rb
|
|
154
|
+
- lib/tasks/snitch_tailwind.rake
|
|
131
155
|
licenses:
|
|
132
156
|
- MIT
|
|
133
157
|
metadata: {}
|