oopsie_exceptions 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +133 -32
- data/lib/generators/oopsie_exceptions/install_generator.rb +10 -15
- data/lib/generators/oopsie_exceptions/templates/initializer.rb +34 -21
- data/lib/oopsie_exceptions/active_job_extension.rb +35 -0
- data/lib/oopsie_exceptions/railtie.rb +2 -15
- data/lib/oopsie_exceptions/version.rb +1 -1
- data/lib/oopsie_exceptions/webhook_job.rb +29 -0
- data/lib/oopsie_exceptions.rb +3 -2
- metadata +5 -4
- data/lib/generators/oopsie_exceptions/templates/delivery_job.rb +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3c1bf7176b541c4c19f09ca56724e8c873e1bae149ac9ff9963a4f4fb4f1156
|
|
4
|
+
data.tar.gz: 692fd55d89e3235279b1ba98764f1fbeaeab5e11204c3733674f7a8958956262
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c8a3bfd9907819f35ecd979d114e9784daf71ff7c5b2264246782458463c15271a71319208fe5798de1c11178c7bf643e8bcda693124ed74e7f2a50cc25ef17
|
|
7
|
+
data.tar.gz: 9d4f5168d31b516127abc0afa2e894ec02c28cdf076e82f96d555f81c1e38fb3884d3fbca46307572ce2f9749798cf1ace9c4e2fa6d98a6263908c4a695a14a1
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight exception capture and webhook delivery for Rails. Like Sentry/Rollbar, but self-hosted and webhook-driven.
|
|
4
4
|
|
|
5
|
-
Captures unhandled exceptions from web requests and background jobs, enriches them with request/user/server context, and
|
|
5
|
+
Captures unhandled exceptions from web requests and background jobs, enriches them with request/user/server context, and POSTs structured JSON payloads to one or more webhook endpoints.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -18,49 +18,99 @@ Then run the install generator:
|
|
|
18
18
|
bin/rails generate oopsie_exceptions:install
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
- `config/initializers/oopsie_exceptions.rb` — configuration
|
|
23
|
-
- `app/jobs/oopsie_exceptions/delivery_job.rb` — async webhook delivery
|
|
21
|
+
That creates a single file: `config/initializers/oopsie_exceptions.rb`. No code lands in `app/` — the Rack middleware, Rails error subscriber, ActiveJob hook, and webhook delivery job all live inside the gem.
|
|
24
22
|
|
|
25
23
|
## Configuration
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
The most common pattern is to configure a different webhook per environment, with the URL and auth token coming from environment variables. Here's a real-world initializer:
|
|
28
26
|
|
|
29
27
|
```ruby
|
|
28
|
+
# config/initializers/oopsie_exceptions.rb
|
|
30
29
|
OopsieExceptions.configure do |config|
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if Rails.env.development?
|
|
31
|
+
config.add_webhook(
|
|
32
|
+
"http://localhost:3099/api/v1/exceptions",
|
|
33
|
+
headers: { "Authorization" => "Bearer #{ENV['OOPSIE_DEV_TOKEN']}" },
|
|
34
|
+
name: "oopsie-local"
|
|
35
|
+
)
|
|
36
|
+
config.async_delivery = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if Rails.env.production?
|
|
40
|
+
config.add_webhook(
|
|
41
|
+
"https://oopsie.example.com/api/v1/exceptions",
|
|
42
|
+
headers: { "Authorization" => "Bearer #{ENV['OOPSIE_PROD_TOKEN']}" },
|
|
43
|
+
name: "oopsie-prod"
|
|
44
|
+
)
|
|
45
|
+
config.async_delivery = true
|
|
46
|
+
end
|
|
33
47
|
|
|
34
48
|
config.app_name = "MyApp"
|
|
35
49
|
config.environment = Rails.env
|
|
36
|
-
config.
|
|
50
|
+
config.filter_parameters = %w[password password_confirmation secret token api_key]
|
|
51
|
+
config.enabled = Rails.env.development? || Rails.env.production?
|
|
52
|
+
|
|
53
|
+
# Attach the current user and controller#action to every exception.
|
|
54
|
+
# `env` is the Rack env — runs once per request.
|
|
55
|
+
config.context_builder = ->(env) {
|
|
56
|
+
warden = env["warden"]
|
|
57
|
+
user = warden&.user
|
|
58
|
+
params = env["action_dispatch.request.path_parameters"]
|
|
59
|
+
{
|
|
60
|
+
user: user ? { id: user.id, email: user.email } : nil,
|
|
61
|
+
action: params ? "#{params[:controller]}##{params[:action]}" : nil
|
|
62
|
+
}.compact
|
|
63
|
+
}
|
|
37
64
|
end
|
|
38
65
|
```
|
|
39
66
|
|
|
40
|
-
|
|
67
|
+
### Configuration options
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
| Option | Default | Description |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `add_webhook(url, headers:, name:)` | — | Register a webhook. Call multiple times for fan-out. |
|
|
72
|
+
| `app_name` | Rails app module name | Identifier sent in every payload. |
|
|
73
|
+
| `environment` | `Rails.env` | Environment label sent in every payload. |
|
|
74
|
+
| `enabled` | `true` | Master kill switch. Set false in test/dev. |
|
|
75
|
+
| `async_delivery` | `true` | Deliver via ActiveJob. Set false to POST inline. |
|
|
76
|
+
| `filter_parameters` | `password, secret, token, api_key, ...` | Param names to redact in payloads. |
|
|
77
|
+
| `filter_headers` | `Authorization, Cookie, Set-Cookie` | Headers to strip from payloads. |
|
|
78
|
+
| `capture_request_body` | `false` | Include first 10KB of JSON request bodies. |
|
|
79
|
+
| `ignored_exceptions` | 404s, routing errors, etc. | Exception class names to silently drop. |
|
|
80
|
+
| `ignore_exception(*names)` | — | Append to `ignored_exceptions`. |
|
|
81
|
+
| `context_builder` | `nil` | `->(env) { Hash }` — extra context per request. |
|
|
82
|
+
| `before_notify` | `nil` | `->(payload) { payload }` — mutate or drop payloads. |
|
|
83
|
+
| `timeout` / `open_timeout` | `10` / `5` | HTTP timeouts (seconds). |
|
|
46
84
|
|
|
47
|
-
##
|
|
85
|
+
## What gets captured automatically
|
|
48
86
|
|
|
49
|
-
|
|
87
|
+
- **Web requests** — A Rack middleware inserted after `ActionDispatch::DebugExceptions` catches unhandled exceptions and 5xx responses.
|
|
88
|
+
- **Background jobs** — `ActiveJob::Base.execute` is wrapped at the class level (the same approach Appsignal uses), so every queue adapter (Solid Queue, Sidekiq, Async, etc.) is covered. Catches `DeserializationError` and missing job classes too.
|
|
89
|
+
- **`Rails.error` reports** — Subscribed via `Rails.error.subscribe`, so anything reported through the framework error reporter (`Rails.error.report` / `Rails.error.handle`) flows through.
|
|
90
|
+
|
|
91
|
+
Each captured exception is enriched with request URL/method/IP/params/headers, the current user (via `context_builder` or `set_context`), server hostname/PID/Ruby version, and a UTC timestamp.
|
|
92
|
+
|
|
93
|
+
## Adding context per-request
|
|
94
|
+
|
|
95
|
+
If you don't want to use `context_builder`, you can set context from a controller:
|
|
50
96
|
|
|
51
97
|
```ruby
|
|
52
|
-
|
|
98
|
+
class ApplicationController < ActionController::Base
|
|
99
|
+
before_action :set_oopsie_context
|
|
53
100
|
|
|
54
|
-
private
|
|
101
|
+
private
|
|
55
102
|
|
|
56
|
-
def set_oopsie_context
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
def set_oopsie_context
|
|
104
|
+
OopsieExceptions.set_context(
|
|
105
|
+
user: current_user ? { id: current_user.id, email: current_user.email } : nil,
|
|
106
|
+
action: "#{self.class.name}##{action_name}"
|
|
107
|
+
)
|
|
108
|
+
end
|
|
61
109
|
end
|
|
62
110
|
```
|
|
63
111
|
|
|
112
|
+
`context_builder` is preferred — it runs in the middleware before the request hits any controller, so it captures context even on errors raised before `before_action` runs.
|
|
113
|
+
|
|
64
114
|
## Manual reporting
|
|
65
115
|
|
|
66
116
|
```ruby
|
|
@@ -71,38 +121,89 @@ rescue => e
|
|
|
71
121
|
end
|
|
72
122
|
```
|
|
73
123
|
|
|
124
|
+
Or scope context to a block:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
OopsieExceptions.with_context(tenant_id: 42) do
|
|
128
|
+
do_work
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
74
132
|
## Multiple webhooks
|
|
75
133
|
|
|
76
134
|
```ruby
|
|
77
|
-
config.add_webhook "https://your-api.com/exceptions"
|
|
135
|
+
config.add_webhook "https://your-api.com/exceptions",
|
|
136
|
+
headers: { "Authorization" => "Bearer #{ENV['PRIMARY_TOKEN']}" }
|
|
137
|
+
|
|
78
138
|
config.add_webhook "https://hooks.slack.com/services/..."
|
|
79
139
|
config.add_webhook "https://discord.com/api/webhooks/..."
|
|
80
140
|
```
|
|
81
141
|
|
|
82
|
-
Each endpoint receives every exception.
|
|
142
|
+
Each endpoint receives every exception. Failures on one webhook don't affect the others.
|
|
83
143
|
|
|
84
144
|
## Payload format
|
|
85
145
|
|
|
86
|
-
Each webhook receives a JSON
|
|
146
|
+
Each webhook receives a JSON POST with:
|
|
87
147
|
|
|
88
148
|
- **exception** — class, message, backtrace, cause chain
|
|
89
149
|
- **request** — URL, method, IP, params, headers, user agent
|
|
90
|
-
- **context** — user
|
|
150
|
+
- **context** — `user`, `action`, `job`, plus anything you set via `context_builder` / `set_context`
|
|
91
151
|
- **server** — hostname, PID, Ruby/Rails versions
|
|
92
|
-
- **app** —
|
|
152
|
+
- **app** — `app_name`, `environment`
|
|
93
153
|
- **timestamp** — UTC ISO8601
|
|
154
|
+
- **handled** — `true` for `OopsieExceptions.report(..., handled: true)`, `false` for unhandled
|
|
155
|
+
|
|
156
|
+
## Filtering noise
|
|
157
|
+
|
|
158
|
+
By default these are dropped:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
ActionController::RoutingError
|
|
162
|
+
ActionController::UnknownFormat
|
|
163
|
+
ActionController::BadRequest
|
|
164
|
+
ActionDispatch::Http::MimeNegotiation::InvalidType
|
|
165
|
+
AbstractController::ActionNotFound
|
|
166
|
+
ActiveRecord::RecordNotFound
|
|
167
|
+
ActionController::UnknownHttpMethod
|
|
168
|
+
```
|
|
94
169
|
|
|
95
|
-
|
|
170
|
+
Add your own:
|
|
96
171
|
|
|
97
|
-
|
|
172
|
+
```ruby
|
|
173
|
+
config.ignore_exception "MyApp::IgnorableError", "ThirdParty::Timeout"
|
|
174
|
+
```
|
|
98
175
|
|
|
99
|
-
|
|
176
|
+
Sensitive params (`password`, `token`, `secret`, `api_key`) and headers (`Authorization`, `Cookie`, `Set-Cookie`) are redacted from payloads automatically.
|
|
177
|
+
|
|
178
|
+
## Mutating or dropping payloads
|
|
100
179
|
|
|
101
180
|
```ruby
|
|
102
|
-
config.
|
|
103
|
-
|
|
181
|
+
config.before_notify = ->(payload) {
|
|
182
|
+
payload[:context][:deploy_sha] = ENV["GIT_SHA"]
|
|
183
|
+
return nil if payload[:exception][:message].include?("known noise")
|
|
184
|
+
payload
|
|
185
|
+
}
|
|
104
186
|
```
|
|
105
187
|
|
|
188
|
+
Return `nil` to drop the notification entirely.
|
|
189
|
+
|
|
190
|
+
## Upgrading from earlier versions
|
|
191
|
+
|
|
192
|
+
If you're coming from an older version of the gem that generated `app/jobs/oopsie_exceptions/delivery_job.rb` in your app, **delete that file**. The gem now ships its own `OopsieExceptions::WebhookJob` and the host-app file is obsolete.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
rm app/jobs/oopsie_exceptions/delivery_job.rb
|
|
196
|
+
rmdir app/jobs/oopsie_exceptions # if empty
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Or just re-run the install generator and it will detect and remove the legacy file for you:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bin/rails generate oopsie_exceptions:install
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
No initializer changes are required.
|
|
206
|
+
|
|
106
207
|
## License
|
|
107
208
|
|
|
108
209
|
MIT — see [LICENSE.txt](LICENSE.txt).
|
|
@@ -5,32 +5,27 @@ module OopsieExceptions
|
|
|
5
5
|
class InstallGenerator < Rails::Generators::Base
|
|
6
6
|
source_root File.expand_path("templates", __dir__)
|
|
7
7
|
|
|
8
|
-
desc "Creates an OopsieExceptions initializer
|
|
8
|
+
desc "Creates an OopsieExceptions initializer"
|
|
9
9
|
|
|
10
10
|
def create_initializer
|
|
11
11
|
template "initializer.rb", "config/initializers/oopsie_exceptions.rb"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
14
|
+
def remove_legacy_delivery_job
|
|
15
|
+
legacy_path = "app/jobs/oopsie_exceptions/delivery_job.rb"
|
|
16
|
+
return unless File.exist?(File.join(destination_root, legacy_path))
|
|
17
|
+
|
|
18
|
+
say ""
|
|
19
|
+
say "Found legacy #{legacy_path} from a previous version.", :yellow
|
|
20
|
+
say "The gem now ships its own webhook job — this file is no longer used."
|
|
21
|
+
remove_file legacy_path
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
def show_post_install
|
|
19
25
|
say ""
|
|
20
26
|
say "OopsieExceptions installed!", :green
|
|
21
27
|
say ""
|
|
22
|
-
say "Next
|
|
23
|
-
say " 1. Edit config/initializers/oopsie_exceptions.rb to add your webhook URLs"
|
|
24
|
-
say " 2. Optionally add user context in ApplicationController:"
|
|
25
|
-
say ""
|
|
26
|
-
say " before_action :set_oopsie_context"
|
|
27
|
-
say ""
|
|
28
|
-
say " def set_oopsie_context"
|
|
29
|
-
say " OopsieExceptions.set_context("
|
|
30
|
-
say " user: current_user ? { id: current_user.id, email: current_user.email } : nil,"
|
|
31
|
-
say " action: \"\#{self.class.name}#\#{action_name}\""
|
|
32
|
-
say " )"
|
|
33
|
-
say " end"
|
|
28
|
+
say "Next step: edit config/initializers/oopsie_exceptions.rb and add your webhook URLs."
|
|
34
29
|
say ""
|
|
35
30
|
end
|
|
36
31
|
end
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
OopsieExceptions.configure do |config|
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
# Webhook endpoints — exceptions get POSTed as JSON.
|
|
3
|
+
# Configure per-environment so dev errors don't hit your prod tracker.
|
|
4
|
+
if Rails.env.development?
|
|
5
|
+
config.add_webhook(
|
|
6
|
+
"http://localhost:3099/api/v1/exceptions",
|
|
7
|
+
headers: { "Authorization" => "Bearer #{ENV['OOPSIE_DEV_TOKEN']}" },
|
|
8
|
+
name: "oopsie-local"
|
|
9
|
+
)
|
|
10
|
+
config.async_delivery = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if Rails.env.production?
|
|
14
|
+
config.add_webhook(
|
|
15
|
+
"https://oopsie.example.com/api/v1/exceptions",
|
|
16
|
+
headers: { "Authorization" => "Bearer #{ENV['OOPSIE_PROD_TOKEN']}" },
|
|
17
|
+
name: "oopsie-prod"
|
|
18
|
+
)
|
|
19
|
+
config.async_delivery = true
|
|
20
|
+
end
|
|
6
21
|
|
|
7
22
|
# config.add_webhook "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
|
|
8
23
|
# config.add_webhook "https://discord.com/api/webhooks/YOUR/DISCORD/WEBHOOK"
|
|
@@ -10,32 +25,30 @@ OopsieExceptions.configure do |config|
|
|
|
10
25
|
config.app_name = Rails.application.class.module_parent_name
|
|
11
26
|
config.environment = Rails.env
|
|
12
27
|
|
|
13
|
-
# Deliver webhooks async via ActiveJob (set false for sync delivery)
|
|
14
|
-
config.async_delivery = true
|
|
15
|
-
|
|
16
|
-
# Exceptions that won't be reported (404s, bot garbage, etc.)
|
|
17
|
-
# config.ignore_exception "ActionController::RoutingError"
|
|
18
|
-
|
|
19
28
|
# Filter sensitive params from payloads (inherits from Rails by default)
|
|
20
29
|
config.filter_parameters = Rails.application.config.filter_parameters.map(&:to_s)
|
|
21
30
|
|
|
22
|
-
#
|
|
23
|
-
config.enabled = Rails.env.
|
|
31
|
+
# Master kill switch — leave off in test
|
|
32
|
+
config.enabled = Rails.env.development? || Rails.env.production?
|
|
24
33
|
|
|
25
|
-
#
|
|
26
|
-
# config.
|
|
27
|
-
# payload[:context][:deploy_sha] = ENV["GIT_SHA"]
|
|
28
|
-
# payload # return nil to skip this notification
|
|
29
|
-
# }
|
|
34
|
+
# Exceptions that won't be reported (404s, bot garbage, etc.)
|
|
35
|
+
# config.ignore_exception "MyApp::IgnorableError"
|
|
30
36
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
37
|
+
# Attach the current user and controller#action to every exception.
|
|
38
|
+
# `env` is the Rack env — runs once per request, before any controller code.
|
|
33
39
|
# config.context_builder = ->(env) {
|
|
34
40
|
# warden = env["warden"]
|
|
35
41
|
# user = warden&.user
|
|
42
|
+
# params = env["action_dispatch.request.path_parameters"]
|
|
36
43
|
# {
|
|
37
44
|
# user: user ? { id: user.id, email: user.email } : nil,
|
|
38
|
-
# action:
|
|
39
|
-
# }
|
|
45
|
+
# action: params ? "#{params[:controller]}##{params[:action]}" : nil
|
|
46
|
+
# }.compact
|
|
47
|
+
# }
|
|
48
|
+
|
|
49
|
+
# Optional: modify or drop payloads before sending
|
|
50
|
+
# config.before_notify = ->(payload) {
|
|
51
|
+
# payload[:context][:deploy_sha] = ENV["GIT_SHA"]
|
|
52
|
+
# payload # return nil to skip this notification
|
|
40
53
|
# }
|
|
41
54
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OopsieExceptions
|
|
4
|
+
# Class-level extension for ActiveJob::Base. Wraps `execute(job_data)`, the
|
|
5
|
+
# entry point queue adapters call, so we capture every unhandled exception
|
|
6
|
+
# — including ones that fire before `perform` runs (e.g. DeserializationError,
|
|
7
|
+
# or a missing job class).
|
|
8
|
+
#
|
|
9
|
+
# This mirrors how Appsignal hooks ActiveJob (lib/appsignal/hooks/active_job.rb).
|
|
10
|
+
module ActiveJobExtension
|
|
11
|
+
def execute(job_data)
|
|
12
|
+
job_context = {
|
|
13
|
+
job: {
|
|
14
|
+
class: job_data["job_class"],
|
|
15
|
+
job_id: job_data["job_id"],
|
|
16
|
+
queue: job_data["queue_name"],
|
|
17
|
+
arguments: job_data["arguments"],
|
|
18
|
+
executions: job_data["executions"],
|
|
19
|
+
provider_job_id: job_data["provider_job_id"]
|
|
20
|
+
}.compact
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
OopsieExceptions.with_context(job_context) do
|
|
24
|
+
super
|
|
25
|
+
rescue Exception => exception
|
|
26
|
+
OopsieExceptions.report(
|
|
27
|
+
exception,
|
|
28
|
+
context: { namespace: "background_job" },
|
|
29
|
+
handled: false
|
|
30
|
+
)
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -8,21 +8,8 @@ module OopsieExceptions
|
|
|
8
8
|
|
|
9
9
|
initializer "oopsie_exceptions.active_job" do
|
|
10
10
|
ActiveSupport.on_load(:active_job) do
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
job: {
|
|
14
|
-
class: job.class.name,
|
|
15
|
-
job_id: job.job_id,
|
|
16
|
-
queue: job.queue_name,
|
|
17
|
-
arguments: job.arguments.map(&:to_s)
|
|
18
|
-
}
|
|
19
|
-
) do
|
|
20
|
-
block.call
|
|
21
|
-
end
|
|
22
|
-
rescue Exception => e
|
|
23
|
-
OopsieExceptions.report(e, context: { namespace: "background_job" }, handled: false)
|
|
24
|
-
raise
|
|
25
|
-
end
|
|
11
|
+
::ActiveJob::Base.extend(OopsieExceptions::ActiveJobExtension)
|
|
12
|
+
require "oopsie_exceptions/webhook_job"
|
|
26
13
|
end
|
|
27
14
|
end
|
|
28
15
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Webhook delivery job. Loaded from the Railtie inside on_load(:active_job),
|
|
4
|
+
# so it's only defined when the host app has ActiveJob available.
|
|
5
|
+
#
|
|
6
|
+
# Inherits from ActiveJob::Base directly (not ApplicationJob) so the gem has
|
|
7
|
+
# zero hard dependencies on host-app code.
|
|
8
|
+
module OopsieExceptions
|
|
9
|
+
class WebhookJob < ActiveJob::Base
|
|
10
|
+
queue_as :default
|
|
11
|
+
|
|
12
|
+
discard_on StandardError do |_job, error|
|
|
13
|
+
logger = OopsieExceptions.configuration.logger
|
|
14
|
+
logger&.error("[OopsieExceptions] WebhookJob discarded permanently: #{error.message}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
retry_on Net::OpenTimeout, Net::ReadTimeout, wait: 5.seconds, attempts: 3
|
|
18
|
+
|
|
19
|
+
def perform(payload_json, webhook_url, headers_json)
|
|
20
|
+
webhook = OopsieExceptions::Configuration::Webhook.new(
|
|
21
|
+
url: webhook_url,
|
|
22
|
+
headers: JSON.parse(headers_json),
|
|
23
|
+
name: webhook_url
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
OopsieExceptions::WebhookClient.post(webhook, payload_json)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/oopsie_exceptions.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "oopsie_exceptions/payload"
|
|
|
7
7
|
require_relative "oopsie_exceptions/webhook_client"
|
|
8
8
|
require_relative "oopsie_exceptions/middleware"
|
|
9
9
|
require_relative "oopsie_exceptions/error_subscriber"
|
|
10
|
+
require_relative "oopsie_exceptions/active_job_extension"
|
|
10
11
|
require_relative "oopsie_exceptions/railtie" if defined?(Rails::Railtie)
|
|
11
12
|
|
|
12
13
|
module OopsieExceptions
|
|
@@ -58,8 +59,8 @@ module OopsieExceptions
|
|
|
58
59
|
|
|
59
60
|
def deliver(payload)
|
|
60
61
|
configuration.webhook_urls.each do |webhook|
|
|
61
|
-
if configuration.async_delivery && defined?(OopsieExceptions::
|
|
62
|
-
|
|
62
|
+
if configuration.async_delivery && defined?(OopsieExceptions::WebhookJob)
|
|
63
|
+
WebhookJob.perform_later(
|
|
63
64
|
payload.to_json,
|
|
64
65
|
webhook.url,
|
|
65
66
|
webhook.headers.to_json
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: oopsie_exceptions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Troy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -36,9 +36,9 @@ files:
|
|
|
36
36
|
- LICENSE.txt
|
|
37
37
|
- README.md
|
|
38
38
|
- lib/generators/oopsie_exceptions/install_generator.rb
|
|
39
|
-
- lib/generators/oopsie_exceptions/templates/delivery_job.rb
|
|
40
39
|
- lib/generators/oopsie_exceptions/templates/initializer.rb
|
|
41
40
|
- lib/oopsie_exceptions.rb
|
|
41
|
+
- lib/oopsie_exceptions/active_job_extension.rb
|
|
42
42
|
- lib/oopsie_exceptions/configuration.rb
|
|
43
43
|
- lib/oopsie_exceptions/context.rb
|
|
44
44
|
- lib/oopsie_exceptions/error_subscriber.rb
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/oopsie_exceptions/railtie.rb
|
|
48
48
|
- lib/oopsie_exceptions/version.rb
|
|
49
49
|
- lib/oopsie_exceptions/webhook_client.rb
|
|
50
|
+
- lib/oopsie_exceptions/webhook_job.rb
|
|
50
51
|
homepage: https://github.com/theinventor/oopsie_exceptions
|
|
51
52
|
licenses:
|
|
52
53
|
- MIT
|
|
@@ -69,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
69
70
|
- !ruby/object:Gem::Version
|
|
70
71
|
version: '0'
|
|
71
72
|
requirements: []
|
|
72
|
-
rubygems_version: 3.5.
|
|
73
|
+
rubygems_version: 3.5.3
|
|
73
74
|
signing_key:
|
|
74
75
|
specification_version: 4
|
|
75
76
|
summary: Lightweight exception capture and webhook delivery for Ruby (framework-agnostic)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module OopsieExceptions
|
|
2
|
-
class DeliveryJob < ApplicationJob
|
|
3
|
-
queue_as :default
|
|
4
|
-
|
|
5
|
-
discard_on StandardError do |job, error|
|
|
6
|
-
Rails.logger.error("[OopsieExceptions] DeliveryJob failed permanently: #{error.message}")
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
retry_on Net::OpenTimeout, Net::ReadTimeout, wait: 5.seconds, attempts: 3
|
|
10
|
-
|
|
11
|
-
def perform(payload_json, webhook_url, headers_json)
|
|
12
|
-
webhook = OopsieExceptions::Configuration::Webhook.new(
|
|
13
|
-
url: webhook_url,
|
|
14
|
-
headers: JSON.parse(headers_json),
|
|
15
|
-
name: webhook_url
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
OopsieExceptions::WebhookClient.post(webhook, payload_json)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|