rails_discord_notifier 0.1.0 → 0.2.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/CHANGELOG.md +90 -0
- data/README.md +134 -53
- data/exe/rails_discord_notifier +3 -0
- data/lib/generators/rails_discord_notifier/install_generator.rb +2 -0
- data/lib/generators/rails_discord_notifier/templates/rails_discord_notifier.rb +46 -3
- data/lib/rails_discord_notifier/cli.rb +102 -0
- data/lib/rails_discord_notifier/client.rb +73 -0
- data/lib/rails_discord_notifier/configuration.rb +146 -0
- data/lib/rails_discord_notifier/error_subscriber.rb +39 -0
- data/lib/rails_discord_notifier/middleware.rb +15 -53
- data/lib/rails_discord_notifier/notifier.rb +130 -0
- data/lib/rails_discord_notifier/payload.rb +277 -0
- data/lib/rails_discord_notifier/railtie.rb +13 -7
- data/lib/rails_discord_notifier/secret_scrubber.rb +33 -0
- data/lib/rails_discord_notifier/version.rb +1 -1
- data/lib/rails_discord_notifier.rb +61 -7
- data/sig/rails_discord_notifier.rbs +70 -1
- metadata +56 -28
- data/.idea/.gitignore +0 -8
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/rails_discord_notifier.iml +0 -84
- data/.idea/vcs.xml +0 -6
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
- data/Rakefile +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d11c790a2d82647c56a5c2e01a96e9be1d55dc89d5d7042c3b0f06baca195ff
|
|
4
|
+
data.tar.gz: 56f2cadcc53da57692a4e54ad203d5a818294f790bd3934fa514213df442b667
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d07906322b8103001b7185d6594a55ea09e271fe1aca239cf69e1a93aa0e316a7c0440e4268b40be6476c97432c44861b8bc83bed4d51a8c2318e09bf23ff807
|
|
7
|
+
data.tar.gz: 6a411b50c842d47a2944c12fd89d1f1e27dab0dd0aa1bf8285e1bdb0a38e3c2651454f2310012a7f2a5d3b4fc8374bb62d4802d6f7d7d02ccc6856a8acad3145
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
1
8
|
## [Unreleased]
|
|
2
9
|
|
|
10
|
+
## [0.2.0] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
This release fixes three defects that could leak credentials or silently drop
|
|
13
|
+
error reports. Upgrading is strongly recommended.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **Nested parameters are now redacted.** Filtering previously only inspected
|
|
18
|
+
top-level keys, so `user[password]` was posted to Discord in cleartext.
|
|
19
|
+
Filtering is now recursive via `ActiveSupport::ParameterFilter` and always
|
|
20
|
+
applies a superset of the host application's `config.filter_parameters`.
|
|
21
|
+
- **Query strings are now redacted.** The `URL` field used the raw request URL,
|
|
22
|
+
so a secret in the query string (`?api_key=...`) was posted verbatim.
|
|
23
|
+
- **Exception messages are scrubbed.** `token=…`, `password="…"` and
|
|
24
|
+
`Api_Key: …` fragments in an exception or `cause` message are replaced with
|
|
25
|
+
`[FILTERED]`, so a database connection error no longer posts its password.
|
|
26
|
+
Best effort only - a message is free text, unlike structured parameters.
|
|
27
|
+
- **Webhook URLs are validated.** Only `https://` URLs on `discord.com` or
|
|
28
|
+
`discordapp.com` (or their subdomains) are accepted, which prevents a
|
|
29
|
+
misconfigured or hostile URL from receiving exception reports.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Reports are no longer silently dropped.** Payloads are truncated to
|
|
34
|
+
Discord's documented limits (1024 per field, 256 title, 4096 description,
|
|
35
|
+
6000 per embed). Oversized messages previously returned HTTP 400 and were
|
|
36
|
+
discarded, losing the error being reported.
|
|
37
|
+
- **Delivery failures are now visible.** The HTTP response was previously
|
|
38
|
+
discarded; non-2xx responses are logged, and 429s are logged with `Retry-After`.
|
|
39
|
+
- **Network timeouts are set** (2s open, 5s read, 5s write). Delivery previously
|
|
40
|
+
had no timeout at all and could hang a request thread indefinitely.
|
|
41
|
+
- `Rails.logger` is no longer assumed to exist; the logger is configurable and
|
|
42
|
+
falls back to `$stderr`.
|
|
43
|
+
- `require "time"` and `require "rack"` are now explicit, rather than relying on
|
|
44
|
+
another gem having loaded them.
|
|
45
|
+
- `rails_discord_notifier/middleware` can be required on its own.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **Background jobs, rake tasks and runners are now covered** via an
|
|
50
|
+
`ActiveSupport::ErrorReporter` subscriber (Rails 7.0+). Previously only
|
|
51
|
+
exceptions raised inside a web request were reported.
|
|
52
|
+
- **Asynchronous delivery** (`config.async`, on by default), so reporting no
|
|
53
|
+
longer adds a blocking HTTP round-trip to a failing request. In-flight
|
|
54
|
+
deliveries are waited for at process exit, so reports from rake tasks and
|
|
55
|
+
runners are not lost when the process ends (`Notifier#flush`).
|
|
56
|
+
- **Throttling** (`config.throttle_period`, 60s by default) collapses repeats of
|
|
57
|
+
the same error, so an error storm cannot flood the channel or trip Discord's
|
|
58
|
+
rate limit. The same exception object is never reported twice.
|
|
59
|
+
- **An ignore list** (`config.ignored_exceptions`) pre-populated with routing,
|
|
60
|
+
`RecordNotFound` and other client-caused noise.
|
|
61
|
+
- **`config.enabled`**, defaulting to off in development and test.
|
|
62
|
+
- **`rails_discord_notifier test`** posts a sample report to your webhook and
|
|
63
|
+
prints Discord's response. The executable previously did nothing.
|
|
64
|
+
- **Richer reports**: request ID, environment, release, hostname, the exception's
|
|
65
|
+
`cause`, and arbitrary extra fields via `config.context`.
|
|
66
|
+
- `RailsDiscordNotifier.notify` for reporting an exception by hand.
|
|
67
|
+
- `config.include_params`, `config.filter_parameters`, `config.backtrace_lines`,
|
|
68
|
+
`config.install_middleware`, `config.install_error_subscriber`.
|
|
69
|
+
- RBS signatures for the public API.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- **`configure` no longer raises** when `webhook_url` is missing. A missing
|
|
74
|
+
environment variable logs a warning and leaves the notifier disabled instead
|
|
75
|
+
of taking down application boot.
|
|
76
|
+
- Configuration moved to a `Configuration` object. The previous flat accessors
|
|
77
|
+
(`RailsDiscordNotifier.webhook_url = ...`) still work.
|
|
78
|
+
- Depends on `actionpack`, `activesupport`, `railties`, `rack` and `logger`
|
|
79
|
+
rather than the whole `rails` meta-gem. Minimum Rails is now 6.1.
|
|
80
|
+
- The timestamp uses Discord's native embed `timestamp` instead of a field.
|
|
81
|
+
- Backtraces default to 10 lines (was 5), trimmed to fit Discord's field limit.
|
|
82
|
+
|
|
83
|
+
## [0.1.2] - 2025-06-10
|
|
84
|
+
|
|
85
|
+
- Add `frozen_string_literal` comments throughout
|
|
86
|
+
- Require MFA for RubyGems pushes
|
|
87
|
+
- Move development dependencies out of the gemspec and into the Gemfile
|
|
88
|
+
|
|
89
|
+
## [0.1.1] - 2025-06-03
|
|
90
|
+
|
|
91
|
+
- Fix security and stability issues in the middleware
|
|
92
|
+
|
|
3
93
|
## [0.1.0] - 2025-05-28
|
|
4
94
|
|
|
5
95
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,94 +1,175 @@
|
|
|
1
1
|
# RailsDiscordNotifier
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Send Rails exceptions to a Discord channel via an incoming webhook.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
5
|
+
- Catches unhandled exceptions from **web requests** (Rack middleware) and from
|
|
6
|
+
**background jobs, rake tasks and runners** (`Rails.error` subscriber, Rails 7.0+)
|
|
7
|
+
- **Redacts secrets** from params and query strings, using your app's own
|
|
8
|
+
`config.filter_parameters` plus a built-in list
|
|
9
|
+
- **Never slows down or breaks your app**: delivery happens off the request
|
|
10
|
+
thread, with short timeouts, and every failure is logged rather than raised
|
|
11
|
+
- **Never floods your channel**: repeats of the same error are collapsed, and
|
|
12
|
+
routing/404-style noise is ignored by default
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
Add the gem directly from RubyGems.org by including it in your Gemfile:
|
|
17
|
-
|
|
18
16
|
```ruby
|
|
19
|
-
gem "rails_discord_notifier", "~> 0.
|
|
17
|
+
gem "rails_discord_notifier", "~> 0.2"
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
Then install:
|
|
23
|
-
|
|
24
20
|
```bash
|
|
25
21
|
bundle install
|
|
22
|
+
rails generate rails_discord_notifier:install
|
|
26
23
|
```
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
Then set your webhook (Server Settings → Integrations → Webhooks → Copy URL):
|
|
29
26
|
|
|
30
27
|
```bash
|
|
31
|
-
|
|
28
|
+
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/…/…"
|
|
32
29
|
```
|
|
33
30
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
Generate the default initializer in your Rails app:
|
|
31
|
+
## Verify it works
|
|
37
32
|
|
|
38
33
|
```bash
|
|
39
|
-
|
|
34
|
+
bundle exec rails_discord_notifier test
|
|
40
35
|
```
|
|
41
36
|
|
|
42
|
-
|
|
37
|
+
This posts a real sample report to your webhook and prints what Discord said,
|
|
38
|
+
so you find out now rather than during your next incident.
|
|
39
|
+
|
|
40
|
+
## What a report looks like
|
|
41
|
+
|
|
42
|
+
An embed titled with the failing request, followed by the details you need to
|
|
43
|
+
find the error:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Exception in POST /orders
|
|
47
|
+
ActiveRecord::RecordInvalid: Validation failed: Email can't be blank
|
|
48
|
+
Caused by ArgumentError: missing email
|
|
49
|
+
|
|
50
|
+
URL https://shop.example.com/orders?utm_source=newsletter
|
|
51
|
+
Controller orders Action create
|
|
52
|
+
Environment production Release a1b2c3d
|
|
53
|
+
Request ID 8f3c… Host web-01
|
|
54
|
+
Params { "order": { "email": "", "card_token": "[FILTERED]" } }
|
|
55
|
+
Backtrace `app/controllers/orders_controller.rb:42:in 'create'` …
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
Everything is optional except `webhook_url`.
|
|
61
|
+
|
|
62
|
+
| Option | Default | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `webhook_url` | `nil` | **Required.** Must be `https://` on `discord.com` or `discordapp.com`. Anything else leaves the notifier disabled. |
|
|
65
|
+
| `enabled` | `false` in development and test, `true` elsewhere | Master switch. |
|
|
66
|
+
| `username` | `"Error Bot"` | Bot display name in Discord. |
|
|
67
|
+
| `avatar_url` | `nil` | Bot avatar image. |
|
|
68
|
+
| `async` | `true` | Deliver on a separate thread so a failing request is not slowed down. |
|
|
69
|
+
| `throttle_period` | `60` | Seconds to collapse repeats of the same error. `0` disables. |
|
|
70
|
+
| `ignored_exceptions` | routing/404-style noise (see below) | Exception class names, matched including subclasses. |
|
|
71
|
+
| `include_params` | `true` | Include filtered request parameters. |
|
|
72
|
+
| `filter_parameters` | `[]` | Extra redaction matchers, unioned with your app's `config.filter_parameters` and the gem's built-in list. |
|
|
73
|
+
| `backtrace_lines` | `10` | Backtrace lines to send, trimmed to fit Discord's field limit. |
|
|
74
|
+
| `open_timeout` / `read_timeout` / `write_timeout` | `2` / `5` / `5` | Seconds. |
|
|
75
|
+
| `environment` | `Rails.env`, else `RAILS_ENV`/`RACK_ENV` | Shown as a field, and decides the `enabled` default. |
|
|
76
|
+
| `release` | `nil` | Shown as a field; handy for matching an error to a deploy. |
|
|
77
|
+
| `context` | `nil` | Callable receiving the Rack env (`nil` outside a request), returning a hash of extra fields. |
|
|
78
|
+
| `logger` | `Rails.logger`, else `$stderr` | Where delivery problems are logged. |
|
|
79
|
+
| `install_middleware` | `true` | Install the Rack middleware. |
|
|
80
|
+
| `install_error_subscriber` | `true` | Subscribe to `Rails.error` (Rails 7.0+). |
|
|
43
81
|
|
|
44
82
|
```ruby
|
|
45
83
|
RailsDiscordNotifier.configure do |config|
|
|
46
|
-
|
|
47
|
-
config.
|
|
84
|
+
config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL", nil)
|
|
85
|
+
config.release = ENV.fetch("GIT_COMMIT_SHA", nil)
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
config.username = ENV.fetch("ERROR_BOT_NAME", "Error Bot")
|
|
87
|
+
config.ignored_exceptions += %w[MyApp::HarmlessError]
|
|
51
88
|
|
|
52
|
-
|
|
53
|
-
# config.avatar_url = ENV.fetch("ERROR_BOT_AVATAR_URL", nil)
|
|
89
|
+
config.context = ->(env) { { "User" => env&.dig("warden")&.user&.id } }
|
|
54
90
|
end
|
|
55
91
|
```
|
|
56
92
|
|
|
57
|
-
|
|
93
|
+
A missing or malformed `webhook_url` logs a warning and disables the notifier.
|
|
94
|
+
It will never take down application boot.
|
|
95
|
+
|
|
96
|
+
### Ignored by default
|
|
97
|
+
|
|
98
|
+
`ActionController::RoutingError`, `ActiveRecord::RecordNotFound`,
|
|
99
|
+
`ActionController::InvalidAuthenticityToken`, `ActionController::UnknownFormat`,
|
|
100
|
+
`ActionController::BadRequest`, `Rack::QueryParser::ParameterTypeError` and
|
|
101
|
+
similar client-caused errors. See `Configuration::DEFAULT_IGNORED_EXCEPTIONS`
|
|
102
|
+
for the full list. Replace it wholesale with `config.ignored_exceptions = [...]`
|
|
103
|
+
or extend it with `+=`.
|
|
104
|
+
|
|
105
|
+
## Secret redaction
|
|
106
|
+
|
|
107
|
+
Parameters and query strings are filtered recursively with
|
|
108
|
+
`ActiveSupport::ParameterFilter`, using the union of:
|
|
58
109
|
|
|
59
|
-
|
|
110
|
+
1. your application's `config.filter_parameters`,
|
|
111
|
+
2. anything you add via `config.filter_parameters`, and
|
|
112
|
+
3. the gem's built-in list (`passw`, `secret`, `token`, `_key`, `crypt`, `salt`,
|
|
113
|
+
`certificate`, `otp`, `ssn`, `signature`, `authorization`, `cookie`,
|
|
114
|
+
`session`, `credential`).
|
|
60
115
|
|
|
61
|
-
|
|
116
|
+
Because it is a union, enabling this gem can never redact less than your
|
|
117
|
+
application already does. To send no parameters at all, set
|
|
118
|
+
`config.include_params = false`.
|
|
62
119
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
120
|
+
Exception messages are free text rather than structured parameters, so they get
|
|
121
|
+
a best-effort scrub instead: `token=abc`, `password="x"` and `Api_Key: abc` style
|
|
122
|
+
fragments are replaced with `[FILTERED]`. This catches the usual culprits (a
|
|
123
|
+
database connection error carrying a password, an HTTP error carrying a URL with
|
|
124
|
+
a token) but cannot be exhaustive the way parameter filtering is.
|
|
125
|
+
|
|
126
|
+
Request bodies, headers and cookies are never sent.
|
|
127
|
+
|
|
128
|
+
## Reporting an exception yourself
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
rescue SomeError => e
|
|
132
|
+
RailsDiscordNotifier.notify(e, context: { "Tenant" => tenant.id })
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## How it gets your exceptions
|
|
137
|
+
|
|
138
|
+
| Source | Mechanism | Requires |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| Controllers, routing, Rack apps | `RailsDiscordNotifier::Middleware` | any supported Rails |
|
|
141
|
+
| Active Job, rake tasks, runners, `Rails.error.handle/record` | `RailsDiscordNotifier::ErrorSubscriber` | Rails 7.0+ |
|
|
142
|
+
| Anything else | `RailsDiscordNotifier.notify` | — |
|
|
143
|
+
|
|
144
|
+
The middleware observes and re-raises, so it never changes how your application
|
|
145
|
+
handles errors. When both entry points see the same exception, it is reported once.
|
|
146
|
+
|
|
147
|
+
Asynchronous deliveries still in flight are waited for at process exit, so a
|
|
148
|
+
report from a rake task or `rails runner` is not lost when the process ends. Call
|
|
149
|
+
`RailsDiscordNotifier.notifier.flush` to wait for them explicitly.
|
|
150
|
+
|
|
151
|
+
## Compatibility
|
|
152
|
+
|
|
153
|
+
Ruby 3.1+, Rails 6.1+. Every supported combination is exercised in CI.
|
|
68
154
|
|
|
69
155
|
## Development
|
|
70
156
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
gem install ./rails_discord_notifier-#{RailsDiscordNotifier::VERSION}.gem
|
|
78
|
-
```
|
|
79
|
-
5. To release (public gem) update `lib/rails_discord_notifier/version.rb` and run:
|
|
80
|
-
```bash
|
|
81
|
-
bundle exec rake release
|
|
82
|
-
```
|
|
157
|
+
```bash
|
|
158
|
+
bin/setup # install dependencies
|
|
159
|
+
bundle exec rspec # run the test suite
|
|
160
|
+
bundle exec rubocop # lint
|
|
161
|
+
bundle exec appraisal install && bundle exec appraisal rspec # all Rails versions
|
|
162
|
+
```
|
|
83
163
|
|
|
84
|
-
|
|
164
|
+
To release: update `lib/rails_discord_notifier/version.rb` and `CHANGELOG.md`,
|
|
165
|
+
then `bundle exec rake release`.
|
|
85
166
|
|
|
86
|
-
|
|
87
|
-
[https://github.com/gettinDrikkieWithIt/rails_discord_notifier](https://github.com/gettinDrikkieWithIt/rails_discord_notifier)
|
|
167
|
+
## Contributing
|
|
88
168
|
|
|
89
|
-
|
|
169
|
+
Bug reports and pull requests are welcome at
|
|
170
|
+
<https://github.com/gettinDrikkieWithIt/rails_discord_notifier>.
|
|
171
|
+
Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
90
172
|
|
|
91
173
|
## License
|
|
92
174
|
|
|
93
|
-
|
|
94
|
-
See [LICENSE.txt](LICENSE.txt) for details.
|
|
175
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/exe/rails_discord_notifier
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
RailsDiscordNotifier.configure do |config|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
#
|
|
4
|
+
# Required. Must be an https://discord.com/api/webhooks/... URL.
|
|
5
|
+
# If it is missing or malformed the notifier stays disabled and logs a warning
|
|
6
|
+
# rather than breaking boot.
|
|
7
|
+
config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL", nil)
|
|
8
|
+
|
|
9
|
+
# Appearance of the bot in Discord.
|
|
10
|
+
config.username = ENV.fetch("ERROR_BOT_NAME", "Error Bot")
|
|
11
|
+
# config.avatar_url = ENV.fetch("ERROR_BOT_AVATAR_URL", nil)
|
|
12
|
+
|
|
13
|
+
# Reporting is off in development and test by default.
|
|
14
|
+
# config.enabled = Rails.env.production?
|
|
15
|
+
|
|
16
|
+
# Deliver off the request thread. Set to false only if you need delivery to be
|
|
17
|
+
# synchronous (for example inside a test).
|
|
18
|
+
# config.async = true
|
|
19
|
+
|
|
20
|
+
# Collapse repeats of the same error for this many seconds. 0 disables it.
|
|
21
|
+
# config.throttle_period = 60
|
|
22
|
+
|
|
23
|
+
# Network timeouts, in seconds. Kept short so a slow Discord never slows you down.
|
|
24
|
+
# config.open_timeout = 2
|
|
25
|
+
# config.read_timeout = 5
|
|
26
|
+
# config.write_timeout = 5
|
|
27
|
+
|
|
28
|
+
# Client-caused noise that is ignored by default. Append your own:
|
|
29
|
+
# config.ignored_exceptions += %w[MyApp::HarmlessError]
|
|
30
|
+
|
|
31
|
+
# Request parameters are included, filtered with your app's
|
|
32
|
+
# config.filter_parameters plus the gem's own list. Turn them off entirely with:
|
|
33
|
+
# config.include_params = false
|
|
34
|
+
# config.filter_parameters += %w[national_id]
|
|
35
|
+
|
|
36
|
+
# Extra fields on every report. Receives the Rack env (nil outside a request).
|
|
37
|
+
# config.context = lambda do |env|
|
|
38
|
+
# { "User" => env&.dig("warden")&.user&.id }
|
|
39
|
+
# end
|
|
40
|
+
|
|
41
|
+
# Shown as a field, handy for matching an error to a deploy.
|
|
42
|
+
# config.release = ENV.fetch("GIT_COMMIT_SHA", nil)
|
|
43
|
+
|
|
44
|
+
# Entry points. Middleware catches web requests; the Rails.error subscriber
|
|
45
|
+
# catches jobs, rake tasks and runners (Rails 7.0+).
|
|
46
|
+
# config.install_middleware = true
|
|
47
|
+
# config.install_error_subscriber = true
|
|
5
48
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsDiscordNotifier
|
|
4
|
+
# `rails_discord_notifier test` posts a real message to your webhook and prints
|
|
5
|
+
# what Discord said. It answers the first question every user of this gem has:
|
|
6
|
+
# "is my webhook actually wired up?"
|
|
7
|
+
class CLI
|
|
8
|
+
USAGE = <<~TEXT
|
|
9
|
+
Usage: rails_discord_notifier <command> [options]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
test Send a sample exception report to your Discord webhook
|
|
13
|
+
version Print the gem version
|
|
14
|
+
help Show this message
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--webhook URL Webhook to use (defaults to $DISCORD_WEBHOOK_URL)
|
|
18
|
+
TEXT
|
|
19
|
+
|
|
20
|
+
def initialize(argv, out: $stdout, env: ENV)
|
|
21
|
+
@argv = Array(argv)
|
|
22
|
+
@out = out
|
|
23
|
+
@env = env
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns a process exit status.
|
|
27
|
+
def run
|
|
28
|
+
case @argv.first
|
|
29
|
+
when "test" then run_test
|
|
30
|
+
when "version" then print_version
|
|
31
|
+
when "help", nil then usage(0)
|
|
32
|
+
else usage(1)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def run_test
|
|
39
|
+
config = test_config
|
|
40
|
+
unless config.valid_webhook_url?
|
|
41
|
+
@out.puts(webhook_help)
|
|
42
|
+
return 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@out.puts("Posting a sample exception report to Discord…")
|
|
46
|
+
deliver(config)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def deliver(config)
|
|
50
|
+
if Client.new(config).post(sample_payload(config))
|
|
51
|
+
@out.puts("Sent. Check your Discord channel.")
|
|
52
|
+
0
|
|
53
|
+
else
|
|
54
|
+
@out.puts("Discord did not accept the message. See the logged response above.")
|
|
55
|
+
1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sample_payload(config)
|
|
60
|
+
exception = begin
|
|
61
|
+
raise "RailsDiscordNotifier test message - everything is wired up correctly"
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
e
|
|
64
|
+
end
|
|
65
|
+
Payload.new(exception, config: config).to_h
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_config
|
|
69
|
+
Configuration.new.tap do |config|
|
|
70
|
+
config.webhook_url = webhook_url
|
|
71
|
+
config.enabled = true
|
|
72
|
+
config.async = false
|
|
73
|
+
config.logger = Logger.new(@out)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def webhook_url
|
|
78
|
+
index = @argv.index("--webhook")
|
|
79
|
+
return @argv[index + 1] if index && @argv[index + 1]
|
|
80
|
+
|
|
81
|
+
@env["DISCORD_WEBHOOK_URL"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def webhook_help
|
|
85
|
+
if webhook_url.to_s.strip.empty?
|
|
86
|
+
"No webhook configured. Set DISCORD_WEBHOOK_URL or pass --webhook URL."
|
|
87
|
+
else
|
|
88
|
+
"Invalid webhook: expected an https://discord.com/api/webhooks/... URL, got #{webhook_url}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def print_version
|
|
93
|
+
@out.puts(VERSION)
|
|
94
|
+
0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def usage(status)
|
|
98
|
+
@out.puts(USAGE)
|
|
99
|
+
status
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module RailsDiscordNotifier
|
|
8
|
+
# Posts a prepared payload to the Discord webhook.
|
|
9
|
+
#
|
|
10
|
+
# Never raises: a notifier that breaks the request it is reporting on is worse
|
|
11
|
+
# than no notifier. Every failure is logged and swallowed.
|
|
12
|
+
class Client
|
|
13
|
+
SUCCESS = (200..299)
|
|
14
|
+
|
|
15
|
+
def initialize(config)
|
|
16
|
+
@config = config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns true when Discord accepted the message.
|
|
20
|
+
def post(payload)
|
|
21
|
+
uri = target_uri
|
|
22
|
+
return false if uri.nil?
|
|
23
|
+
|
|
24
|
+
accepted?(http(uri).request(build_request(uri, payload)))
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
config.logger.error("[RailsDiscordNotifier] delivery failed: #{e.class}: #{e.message}")
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :config
|
|
33
|
+
|
|
34
|
+
def target_uri
|
|
35
|
+
return nil unless config.valid_webhook_url?
|
|
36
|
+
|
|
37
|
+
URI.parse(config.webhook_url.to_s.strip)
|
|
38
|
+
rescue URI::InvalidURIError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def http(uri)
|
|
43
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
|
44
|
+
http.use_ssl = uri.scheme == "https"
|
|
45
|
+
http.open_timeout = config.open_timeout
|
|
46
|
+
http.read_timeout = config.read_timeout
|
|
47
|
+
http.write_timeout = config.write_timeout
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_request(uri, payload)
|
|
52
|
+
Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json").tap do |request|
|
|
53
|
+
request.body = JSON.generate(payload)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Logs whatever Discord said, and answers whether the message got through.
|
|
58
|
+
def accepted?(response)
|
|
59
|
+
return true if SUCCESS.cover?(response.code.to_i)
|
|
60
|
+
|
|
61
|
+
if response.code.to_i == 429
|
|
62
|
+
config.logger.warn(
|
|
63
|
+
"[RailsDiscordNotifier] rate limited by Discord, retry after #{response["Retry-After"] || "unknown"}s"
|
|
64
|
+
)
|
|
65
|
+
else
|
|
66
|
+
config.logger.error(
|
|
67
|
+
"[RailsDiscordNotifier] Discord rejected the message: HTTP #{response.code} #{response.body}"
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|