snitch-rails 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c4a5dc4773dd228a4f9478cd08a0b1f3b6a4160500340dd692beca1395f0f49
4
- data.tar.gz: 95b3c04d9b95d0e15c426c12746bf0fcefaefd47e35aeba898cd21fc26ba47b9
3
+ metadata.gz: a33dc497c1b82d6d45ebc526e83f27eb51cb683fcf7eb3634d0b94aa020be386
4
+ data.tar.gz: 1302300c77ed110f702cb2f6b5817ce71e87ad1cfe35845a1935776a2f6d830c
5
5
  SHA512:
6
- metadata.gz: f1592e5587346cac3b8bd92b6f9536a6f890c7922df77ad3e47423b3139a0692a4367f69b80eee1702abc5eff63cd5bc99ebdce3c59db5e8a54ee91d282e8ed4
7
- data.tar.gz: d0b95a4e8fa41d368af44a6b0efdfd1c70c1e9dfdf9c072faeb5316a62e19ef2a8c081a317a6e32a2be0fe70da969e8647e720c30a2e46dce88219a7ac35874b
6
+ metadata.gz: d7bbd6b45dc3f1ed501940526360912793e60b7b18eae32c10b32b48f5e90ebb0497cb450e8581fa99e6d14144eabce3c5a03d61069ab37fc3968c5024e573a2
7
+ data.tar.gz: e7996ecc883c3a112211c31a174a3281f1a03898bb8af2c994aef9df21648619dac8e442099becc6223398dcae402fd1e69a07c83552b506cb61562c106f5b7a
data/README.md CHANGED
@@ -12,7 +12,6 @@ Snitch catches unhandled exceptions in your Rails application, persists them to
12
12
  - **Dashboard** at `/snitches` with tabbed views: Open, Closed, and Ignored
13
13
  - Ignore exceptions via config or directly from the dashboard
14
14
  - Manual exception reporting for rescued exceptions
15
- - Upgrade generator for existing installations
16
15
 
17
16
  ## Installation
18
17
 
@@ -28,26 +27,19 @@ Run bundle install:
28
27
  bundle install
29
28
  ```
30
29
 
31
- Run the install generator to create the migration, initializer, and mount the engine routes:
30
+ Run the install generator:
32
31
 
33
32
  ```bash
34
33
  rails generate snitch:install
35
- ```
36
-
37
- Run the migration:
38
-
39
- ```bash
40
34
  rails db:migrate
41
35
  ```
42
36
 
43
- The installer automatically adds the following route to your application:
37
+ The installer mounts the dashboard to `config/routes.rb`:
44
38
 
45
39
  ```ruby
46
40
  mount Snitch::Engine, at: "/snitches"
47
41
  ```
48
42
 
49
- If you need to mount it manually or at a different path, add the above line to your `config/routes.rb`.
50
-
51
43
  ### Upgrading from a previous version
52
44
 
53
45
  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:
@@ -83,6 +75,9 @@ Snitch.configure do |config|
83
75
 
84
76
  # Exceptions to ignore (default: ActiveRecord::RecordNotFound, ActionController::RoutingError)
85
77
  config.ignored_exceptions += [YourCustomError]
78
+
79
+ # GitHub webhook secret for auto-closing events when issues are closed (optional)
80
+ config.github_webhook_secret = ENV["SNITCH_GITHUB_WEBHOOK_SECRET"]
86
81
  end
87
82
  ```
88
83
 
@@ -92,7 +87,11 @@ Create a [personal access token](https://github.com/settings/tokens) with the `r
92
87
 
93
88
  ## Dashboard
94
89
 
95
- Visit `/snitches` in your browser to view the Snitch dashboard. The dashboard provides three tabs:
90
+ Visit `/snitches` in your browser to view the Snitch dashboard.
91
+
92
+ ![Snitch Dashboard](dashboard.png)
93
+
94
+ The dashboard provides three tabs:
96
95
 
97
96
  - **Open** — Exceptions that need attention. Shows the most recently occurred first.
98
97
  - **Closed** — Exceptions that have been resolved. You can reopen them if they recur.
@@ -104,10 +103,12 @@ From the dashboard you can change the status of any exception:
104
103
 
105
104
  ### Ignoring Exceptions
106
105
 
107
- There are two ways to ignore exceptions:
106
+ There are two ways to ignore exceptions, and Snitch honors both:
107
+
108
+ 1. **Via the initializer** — Add exception classes to `config.ignored_exceptions`. These are filtered at the middleware level and never captured or persisted.
109
+ 2. **Via the dashboard** — Mark individual exceptions as "Ignored" from the Open tab. Future occurrences of that exception class will be silently skipped — no new records, no GitHub issues.
108
110
 
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.
111
+ Both sources are checked every time an exception is caught. If an exception class appears in either the initializer config or has a record with status "ignored" in the database, it will be ignored.
111
112
 
112
113
  ## Manual Reporting
113
114
 
@@ -126,6 +127,42 @@ This is useful for exceptions you want to recover from gracefully but still want
126
127
 
127
128
  The same fingerprinting and deduplication rules apply. If the same exception is reported multiple times, Snitch will increment the occurrence count and comment on the existing GitHub issue rather than creating a new one.
128
129
 
130
+ ## GitHub Webhook (Auto-Close)
131
+
132
+ When a developer fixes a bug and closes the GitHub issue, Snitch can automatically close the matching event record in your database — no manual dashboard cleanup required.
133
+
134
+ ### Setup
135
+
136
+ 1. Generate a webhook secret:
137
+
138
+ ```bash
139
+ ruby -rsecurerandom -e "puts SecureRandom.hex(32)"
140
+ ```
141
+
142
+ 2. Add the secret to your environment and Snitch config:
143
+
144
+ ```bash
145
+ export SNITCH_GITHUB_WEBHOOK_SECRET="your-generated-secret"
146
+ ```
147
+
148
+ ```ruby
149
+ # config/initializers/snitch.rb
150
+ config.github_webhook_secret = ENV["SNITCH_GITHUB_WEBHOOK_SECRET"]
151
+ ```
152
+
153
+ 3. Add the same secret to your GitHub repository. Go to **Settings > Webhooks > Add webhook** and configure:
154
+
155
+ | Field | Value |
156
+ |----------------|------------------------------------------------|
157
+ | Payload URL | `https://yourapp.com/snitches/webhooks/github` |
158
+ | Content type | `application/json` |
159
+ | Secret | The secret you generated in step 1 |
160
+ | Events | Select **Issues** only |
161
+
162
+ The secret must match on both sides — your app uses it to verify that incoming webhooks are genuinely from GitHub.
163
+
164
+ Once configured, closing a GitHub issue will automatically close the corresponding Snitch event.
165
+
129
166
  ## How It Works
130
167
 
131
168
  1. Rack middleware catches any unhandled exception (and re-raises it so normal error handling still applies)
@@ -136,8 +173,9 @@ The same fingerprinting and deduplication rules apply. If the same exception is
136
173
 
137
174
  ## Roadmap
138
175
 
139
- - [ ] multi-db support
140
- - [ ] webhook to resolve snitch record, when issues resolve
176
+ - [x] Dashboard to view and manage captured exceptions
177
+ - [ ] Multi-db support
178
+ - [x] Webhook to resolve snitch records when GitHub issues close
141
179
 
142
180
  ## Requirements
143
181
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snitch
4
+ class WebhooksController < ActionController::Base
5
+ skip_forgery_protection
6
+
7
+ def github
8
+ secret = Snitch.configuration.github_webhook_secret
9
+ return head :unauthorized unless secret.present?
10
+
11
+ body = request.body.read
12
+ signature = request.headers["X-Hub-Signature-256"]
13
+ expected = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", secret, body)}"
14
+ return head :unauthorized unless signature.present? && Rack::Utils.secure_compare(expected, signature)
15
+
16
+ return head :ok unless request.headers["X-GitHub-Event"] == "issues"
17
+
18
+ payload = JSON.parse(body)
19
+ return head :ok unless payload["action"] == "closed"
20
+
21
+ issue_number = payload.dig("issue", "number")
22
+ ResolveEventJob.perform_later(issue_number)
23
+
24
+ head :ok
25
+ end
26
+ end
27
+ end
data/config/routes.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  Snitch::Engine.routes.draw do
2
2
  resources :snitches, only: [:index, :show, :update]
3
+ post "webhooks/github", to: "webhooks#github"
3
4
  root to: "snitches#index"
4
5
  end
@@ -13,4 +13,7 @@ Snitch.configure do |config|
13
13
 
14
14
  # Exceptions to ignore (default: RecordNotFound, RoutingError)
15
15
  # config.ignored_exceptions += [CustomError]
16
+
17
+ # GitHub webhook secret for auto-closing events when issues are closed
18
+ # config.github_webhook_secret = ENV["SNITCH_GITHUB_WEBHOOK_SECRET"]
16
19
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Snitch
4
4
  class Configuration
5
- attr_accessor :github_token, :github_repo, :mention, :enabled, :ignored_exceptions
5
+ attr_accessor :github_token, :github_repo, :github_webhook_secret, :mention, :enabled, :ignored_exceptions
6
6
 
7
7
  def initialize
8
8
  @github_token = nil
9
9
  @github_repo = nil
10
+ @github_webhook_secret = nil
10
11
  @mention = "@claude"
11
12
  @enabled = true
12
13
  @ignored_exceptions = [
@@ -45,12 +45,14 @@ module Snitch
45
45
  existing = Event.find_by(fingerprint: fingerprint)
46
46
 
47
47
  if existing
48
- existing.update!(
48
+ attrs = {
49
49
  occurrence_count: existing.occurrence_count + 1,
50
50
  last_occurred_at: Time.current,
51
51
  message: exception.message,
52
52
  backtrace: exception.backtrace
53
- )
53
+ }
54
+ attrs[:status] = "open" if existing.status == "closed"
55
+ existing.update!(**attrs)
54
56
  existing
55
57
  else
56
58
  Event.create!(
@@ -69,7 +71,7 @@ module Snitch
69
71
  def enqueue_report(record)
70
72
  ReportExceptionJob.perform_later(record.id)
71
73
  rescue => e
72
- Rails.logger.error("[Snitch] Failed to enqueue report job: #{e.message}") if defined?(Rails)
74
+ Rails.logger.error("[Snitch] Failed to enqueue report job: #{e.message}") if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
73
75
  end
74
76
  end
75
77
  end
@@ -24,6 +24,11 @@ module Snitch
24
24
  def comment_on_issue(event)
25
25
  body = build_comment_body(event)
26
26
  @client.add_comment(@repo, event.github_issue_number, body)
27
+ reopen_issue(event) if event.status == "open"
28
+ end
29
+
30
+ def reopen_issue(event)
31
+ @client.update_issue(@repo, event.github_issue_number, state: "open")
27
32
  end
28
33
 
29
34
  private
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Snitch
4
+ class ResolveEventJob < ActiveJob::Base
5
+ queue_as :default
6
+
7
+ retry_on StandardError, wait: :polynomially_later, attempts: 3
8
+
9
+ def perform(github_issue_number)
10
+ Event.where(github_issue_number: github_issue_number, status: "open")
11
+ .update_all(status: "closed")
12
+ end
13
+ end
14
+ end
@@ -10,7 +10,7 @@ module Snitch
10
10
  begin
11
11
  ExceptionHandler.handle(e, env)
12
12
  rescue => handler_error
13
- Rails.logger.error("[Snitch] Handler error: #{handler_error.message}") if defined?(Rails)
13
+ Rails.logger.error("[Snitch] Handler error: #{handler_error.message}") if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
14
14
  end
15
15
  raise e
16
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Snitch
4
- VERSION = '0.3.1'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/snitch.rb CHANGED
@@ -8,6 +8,7 @@ require "snitch/exception_handler"
8
8
  require "snitch/github_client"
9
9
  require "snitch/models/event" if defined?(ActiveRecord)
10
10
  require "snitch/jobs/report_exception_job" if defined?(ActiveJob)
11
+ require "snitch/jobs/resolve_event_job" if defined?(ActiveJob)
11
12
  require "snitch/engine" if defined?(Rails)
12
13
 
13
14
  module Snitch
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RiseKit
@@ -132,6 +132,7 @@ files:
132
132
  - app/assets/builds/snitch/application.css
133
133
  - app/assets/stylesheets/snitch/application.css
134
134
  - app/controllers/snitch/snitches_controller.rb
135
+ - app/controllers/snitch/webhooks_controller.rb
135
136
  - app/views/layouts/snitch/application.html.erb
136
137
  - app/views/snitch/snitches/index.html.erb
137
138
  - app/views/snitch/snitches/show.html.erb
@@ -148,6 +149,7 @@ files:
148
149
  - lib/snitch/fingerprint.rb
149
150
  - lib/snitch/github_client.rb
150
151
  - lib/snitch/jobs/report_exception_job.rb
152
+ - lib/snitch/jobs/resolve_event_job.rb
151
153
  - lib/snitch/middleware.rb
152
154
  - lib/snitch/models/event.rb
153
155
  - lib/snitch/version.rb