rails_discord_notifier 0.1.1 → 0.3.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 +139 -0
- data/README.md +180 -52
- 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 +48 -3
- data/lib/rails_discord_notifier/cli.rb +106 -0
- data/lib/rails_discord_notifier/client.rb +73 -0
- data/lib/rails_discord_notifier/configuration.rb +158 -0
- data/lib/rails_discord_notifier/error_subscriber.rb +40 -0
- data/lib/rails_discord_notifier/google_chat_payload.rb +51 -0
- data/lib/rails_discord_notifier/middleware.rb +15 -53
- data/lib/rails_discord_notifier/notifier.rb +143 -0
- data/lib/rails_discord_notifier/payload.rb +285 -0
- data/lib/rails_discord_notifier/railtie.rb +13 -7
- data/lib/rails_discord_notifier/secret_scrubber.rb +59 -0
- data/lib/rails_discord_notifier/version.rb +1 -1
- data/lib/rails_discord_notifier.rb +63 -7
- data/sig/rails_discord_notifier.rbs +75 -1
- metadata +58 -29
- 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: 6527e49521148326dc463e191ed0fe51097c0625e7a09b89074615eb6b21f431
|
|
4
|
+
data.tar.gz: 3502ab8e704d8e0d995cdd17fb8d06fc36d9c6bae13122455cb33b013c9eff3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2933ce5afd5ee5ae228d589c4c556fdde5dee1def8146a07204fc2230b80ccdbe89dbcbd055786af2389653518d8518e3de564d1dc3147851a8236a50f48ac5f
|
|
7
|
+
data.tar.gz: 01a951280d579cac677a76e6e2120b9fb30b4c8c94fd584459423a0b4e1b7d65e8ab178584ef4e9b41178a216a3b010d2a0d34950d481e0ef341d28050609bc5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,144 @@
|
|
|
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.3.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Google Chat support.** Set `webhook_url` to a Google Chat incoming webhook
|
|
15
|
+
(`https://chat.googleapis.com/v1/spaces/…`) and reports are sent as a Google
|
|
16
|
+
Chat text message instead of a Discord embed. They are redacted and size-limited
|
|
17
|
+
the same way, and fit within Google Chat's 32,000 byte limit. `username` and
|
|
18
|
+
`avatar_url` don't apply to Google Chat.
|
|
19
|
+
- `rails_discord_notifier test` also reads `GOOGLE_CHAT_WEBHOOK_URL`.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- **Chat markup in reports is defused for Google Chat.** Attacker-influenced
|
|
24
|
+
text (exception message, path, User-Agent) can no longer produce an
|
|
25
|
+
`<users/all>` mention or a `<url|text>` masked link.
|
|
26
|
+
- **The CLI no longer echoes a rejected webhook URL.** The URL is a credential
|
|
27
|
+
and the output may land in CI logs.
|
|
28
|
+
- **Message scrubbing can no longer stall the process.** On Ruby 3.1 the scrubber
|
|
29
|
+
regex took cubic time, so a request that put about 4 KB of text into an
|
|
30
|
+
exception message held the interpreter lock for about 13 seconds. Matching is
|
|
31
|
+
now linear on every supported Ruby.
|
|
32
|
+
- **The scrubber catches more credential shapes:** `Authorization` values with
|
|
33
|
+
the `Bearer`, `Basic`, `Token`, `Digest` or `Negotiate` scheme, JSON and
|
|
34
|
+
`inspect`-style quoted keys (`"password":"x"`, `"password" => "x"`), and
|
|
35
|
+
passwords in URLs (`redis://:pw@host`, `postgres://user:pw@host`).
|
|
36
|
+
- **`Rails.error` context and `config.context` are now filtered** with the same
|
|
37
|
+
parameter filter as request params, including nested values and
|
|
38
|
+
`ActionController::Parameters`. They were previously sent verbatim. If the
|
|
39
|
+
filter itself raises, the context is left out and a warning is logged.
|
|
40
|
+
- **Delivery threads are capped at 10 in flight.** Before, an error storm
|
|
41
|
+
against a slow webhook started one thread per report without limit. Reports
|
|
42
|
+
beyond the cap are dropped and logged.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Text that isn't valid UTF-8 no longer drops the report.** A binary header
|
|
47
|
+
value (which Rack passes through) or a stray byte in an exception message made
|
|
48
|
+
JSON encoding fail. Invalid bytes are now replaced with `�`, and text in other
|
|
49
|
+
encodings (e.g. Windows-1252) is converted.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **Repeats are recognised by route, not path.** The throttle fingerprint uses
|
|
54
|
+
controller and action instead of the raw request path, so `/users/1`,
|
|
55
|
+
`/users/2` and `/users/1.json` hitting the same bug count as one error.
|
|
56
|
+
- Delivery log lines no longer name Discord ("webhook rejected the message",
|
|
57
|
+
"rate limited by the webhook"). Update any log alerts that match the old text.
|
|
58
|
+
|
|
59
|
+
## [0.2.0] - 2026-09-17
|
|
60
|
+
|
|
61
|
+
This release fixes three defects that could leak credentials or silently drop
|
|
62
|
+
error reports. Upgrading is strongly recommended.
|
|
63
|
+
|
|
64
|
+
### Security
|
|
65
|
+
|
|
66
|
+
- **Nested parameters are now redacted.** Filtering previously only inspected
|
|
67
|
+
top-level keys, so `user[password]` was posted to Discord in cleartext.
|
|
68
|
+
Filtering is now recursive via `ActiveSupport::ParameterFilter` and always
|
|
69
|
+
applies a superset of the host application's `config.filter_parameters`.
|
|
70
|
+
- **Query strings are now redacted.** The `URL` field used the raw request URL,
|
|
71
|
+
so a secret in the query string (`?api_key=...`) was posted verbatim.
|
|
72
|
+
- **Exception messages are scrubbed.** `token=…`, `password="…"` and
|
|
73
|
+
`Api_Key: …` fragments in an exception or `cause` message are replaced with
|
|
74
|
+
`[FILTERED]`, so a database connection error no longer posts its password.
|
|
75
|
+
Best effort only - a message is free text, unlike structured parameters.
|
|
76
|
+
- **Webhook URLs are validated.** Only `https://` URLs on `discord.com` or
|
|
77
|
+
`discordapp.com` (or their subdomains) are accepted, which prevents a
|
|
78
|
+
misconfigured or hostile URL from receiving exception reports.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **Reports are no longer silently dropped.** Payloads are truncated to
|
|
83
|
+
Discord's documented limits (1024 per field, 256 title, 4096 description,
|
|
84
|
+
6000 per embed). Oversized messages previously returned HTTP 400 and were
|
|
85
|
+
discarded, losing the error being reported.
|
|
86
|
+
- **Delivery failures are now visible.** The HTTP response was previously
|
|
87
|
+
discarded; non-2xx responses are logged, and 429s are logged with `Retry-After`.
|
|
88
|
+
- **Network timeouts are set** (2s open, 5s read, 5s write). Delivery previously
|
|
89
|
+
had no timeout at all and could hang a request thread indefinitely.
|
|
90
|
+
- `Rails.logger` is no longer assumed to exist; the logger is configurable and
|
|
91
|
+
falls back to `$stderr`.
|
|
92
|
+
- `require "time"` and `require "rack"` are now explicit, rather than relying on
|
|
93
|
+
another gem having loaded them.
|
|
94
|
+
- `rails_discord_notifier/middleware` can be required on its own.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **Background jobs, rake tasks and runners are now covered** via an
|
|
99
|
+
`ActiveSupport::ErrorReporter` subscriber (Rails 7.0+). Previously only
|
|
100
|
+
exceptions raised inside a web request were reported.
|
|
101
|
+
- **Asynchronous delivery** (`config.async`, on by default), so reporting no
|
|
102
|
+
longer adds a blocking HTTP round-trip to a failing request. In-flight
|
|
103
|
+
deliveries are waited for at process exit, so reports from rake tasks and
|
|
104
|
+
runners are not lost when the process ends (`Notifier#flush`).
|
|
105
|
+
- **Throttling** (`config.throttle_period`, 60s by default) collapses repeats of
|
|
106
|
+
the same error, so an error storm cannot flood the channel or trip Discord's
|
|
107
|
+
rate limit. The same exception object is never reported twice.
|
|
108
|
+
- **An ignore list** (`config.ignored_exceptions`) pre-populated with routing,
|
|
109
|
+
`RecordNotFound` and other client-caused noise.
|
|
110
|
+
- **`config.enabled`**, defaulting to off in development and test.
|
|
111
|
+
- **`rails_discord_notifier test`** posts a sample report to your webhook and
|
|
112
|
+
prints Discord's response. The executable previously did nothing.
|
|
113
|
+
- **Richer reports**: request ID, environment, release, hostname, the exception's
|
|
114
|
+
`cause`, and arbitrary extra fields via `config.context`.
|
|
115
|
+
- `RailsDiscordNotifier.notify` for reporting an exception by hand.
|
|
116
|
+
- `config.include_params`, `config.filter_parameters`, `config.backtrace_lines`,
|
|
117
|
+
`config.install_middleware`, `config.install_error_subscriber`.
|
|
118
|
+
- RBS signatures for the public API.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- **`configure` no longer raises** when `webhook_url` is missing. A missing
|
|
123
|
+
environment variable logs a warning and leaves the notifier disabled instead
|
|
124
|
+
of taking down application boot.
|
|
125
|
+
- Configuration moved to a `Configuration` object. The previous flat accessors
|
|
126
|
+
(`RailsDiscordNotifier.webhook_url = ...`) still work.
|
|
127
|
+
- Depends on `actionpack`, `activesupport`, `railties`, `rack` and `logger`
|
|
128
|
+
rather than the whole `rails` meta-gem. Minimum Rails is now 6.1.
|
|
129
|
+
- The timestamp uses Discord's native embed `timestamp` instead of a field.
|
|
130
|
+
- Backtraces default to 10 lines (was 5), trimmed to fit Discord's field limit.
|
|
131
|
+
|
|
132
|
+
## [0.1.2] - 2025-06-10
|
|
133
|
+
|
|
134
|
+
- Add `frozen_string_literal` comments throughout
|
|
135
|
+
- Require MFA for RubyGems pushes
|
|
136
|
+
- Move development dependencies out of the gemspec and into the Gemfile
|
|
137
|
+
|
|
138
|
+
## [0.1.1] - 2025-06-03
|
|
139
|
+
|
|
140
|
+
- Fix security and stability issues in the middleware
|
|
141
|
+
|
|
3
142
|
## [0.1.0] - 2025-05-28
|
|
4
143
|
|
|
5
144
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,94 +1,222 @@
|
|
|
1
1
|
# RailsDiscordNotifier
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Send Rails exceptions to a Discord channel or a Google Chat space via an
|
|
4
|
+
incoming webhook.
|
|
5
|
+
|
|
6
|
+
- Catches unhandled exceptions from **web requests** (Rack middleware) and from
|
|
7
|
+
**background jobs, rake tasks and runners** (`Rails.error` subscriber, Rails 7.0+)
|
|
8
|
+
- **Redacts secrets** from params and query strings, using your app's own
|
|
9
|
+
`config.filter_parameters` plus a built-in list
|
|
10
|
+
- **Never slows down or breaks your app**: delivery happens off the request
|
|
11
|
+
thread, with short timeouts, and every failure is logged rather than raised
|
|
12
|
+
- **Never floods your channel**: repeats of the same error are collapsed, and
|
|
13
|
+
routing/404-style noise is ignored by default
|
|
4
14
|
|
|
5
|
-
|
|
15
|
+
## Installation
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
```ruby
|
|
18
|
+
gem "rails_discord_notifier", "~> 0.3"
|
|
19
|
+
```
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
```bash
|
|
22
|
+
bundle install
|
|
23
|
+
rails generate rails_discord_notifier:install
|
|
24
|
+
```
|
|
13
25
|
|
|
14
|
-
|
|
26
|
+
Then set your webhook. The gem works out which service it is from the URL.
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
**Discord:** Server Settings → Integrations → Webhooks → Copy URL.
|
|
17
29
|
|
|
18
|
-
```
|
|
19
|
-
|
|
30
|
+
```bash
|
|
31
|
+
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/…/…"
|
|
20
32
|
```
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
**Google Chat:** open the space → Apps & integrations → Webhooks → Add webhook,
|
|
35
|
+
then copy the URL. Your Workspace admin must allow incoming webhooks.
|
|
23
36
|
|
|
24
37
|
```bash
|
|
25
|
-
|
|
38
|
+
export GOOGLE_CHAT_WEBHOOK_URL="https://chat.googleapis.com/v1/spaces/…/messages?key=…&token=…"
|
|
26
39
|
```
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
The generated initializer reads `DISCORD_WEBHOOK_URL`, then
|
|
42
|
+
`GOOGLE_CHAT_WEBHOOK_URL`. Treat either URL as a password: anyone holding it
|
|
43
|
+
can post to your channel.
|
|
44
|
+
|
|
45
|
+
## Verify it works
|
|
29
46
|
|
|
30
47
|
```bash
|
|
31
|
-
bundle
|
|
48
|
+
bundle exec rails_discord_notifier test
|
|
32
49
|
```
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
This posts a real sample report to your webhook and prints what the service
|
|
52
|
+
said, so you find out now rather than during your next incident. It reads
|
|
53
|
+
`DISCORD_WEBHOOK_URL`, then `GOOGLE_CHAT_WEBHOOK_URL`, or takes `--webhook URL`
|
|
54
|
+
(which ends up in your shell history, so prefer the environment variable).
|
|
35
55
|
|
|
36
|
-
|
|
56
|
+
## What a report looks like
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
In Discord, an embed titled with the failing request, followed by the details
|
|
59
|
+
you need to find the error:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Exception in POST /orders
|
|
63
|
+
ActiveRecord::RecordInvalid: Validation failed: Email can't be blank
|
|
64
|
+
Caused by ArgumentError: missing email
|
|
65
|
+
|
|
66
|
+
URL https://shop.example.com/orders?utm_source=newsletter
|
|
67
|
+
Controller orders Action create
|
|
68
|
+
Environment production Release a1b2c3d
|
|
69
|
+
Request ID 8f3c… Host web-01
|
|
70
|
+
Params { "order": { "email": "", "card_token": "[FILTERED]" } }
|
|
71
|
+
Backtrace `app/controllers/orders_controller.rb:42:in 'create'` …
|
|
40
72
|
```
|
|
41
73
|
|
|
42
|
-
In
|
|
74
|
+
In Google Chat, the same details as a single text message, with the params and
|
|
75
|
+
backtrace in code blocks. Chat has no per-message sender name or avatar: those
|
|
76
|
+
come from the name and avatar you gave the webhook when you created it.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
Everything is optional except `webhook_url`.
|
|
81
|
+
|
|
82
|
+
| Option | Default | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `webhook_url` | `nil` | **Required.** Must be `https://` on `discord.com`, `discordapp.com` or `chat.googleapis.com`. Anything else leaves the notifier disabled. |
|
|
85
|
+
| `enabled` | `false` in development and test, `true` elsewhere | Master switch. |
|
|
86
|
+
| `username` | `"Error Bot"` | Bot display name. Discord only. |
|
|
87
|
+
| `avatar_url` | `nil` | Bot avatar image. Discord only. |
|
|
88
|
+
| `async` | `true` | Deliver on a separate thread so a failing request is not slowed down. |
|
|
89
|
+
| `throttle_period` | `60` | Seconds to collapse repeats of the same error. `0` disables. |
|
|
90
|
+
| `ignored_exceptions` | routing/404-style noise (see below) | Exception class names, matched including subclasses. |
|
|
91
|
+
| `include_params` | `true` | Include filtered request parameters. |
|
|
92
|
+
| `filter_parameters` | `[]` | Extra redaction matchers, unioned with your app's `config.filter_parameters` and the gem's built-in list. |
|
|
93
|
+
| `backtrace_lines` | `10` | Backtrace lines to send, trimmed to fit the message size limit. |
|
|
94
|
+
| `open_timeout` / `read_timeout` / `write_timeout` | `2` / `5` / `5` | Seconds. |
|
|
95
|
+
| `environment` | `Rails.env`, else `RAILS_ENV`/`RACK_ENV` | Shown as a field, and decides the `enabled` default. |
|
|
96
|
+
| `release` | `nil` | Shown as a field; handy for matching an error to a deploy. |
|
|
97
|
+
| `context` | `nil` | Callable receiving the Rack env (`nil` outside a request), returning a hash of extra fields. |
|
|
98
|
+
| `logger` | `Rails.logger`, else `$stderr` | Where delivery problems are logged. |
|
|
99
|
+
| `install_middleware` | `true` | Install the Rack middleware. |
|
|
100
|
+
| `install_error_subscriber` | `true` | Subscribe to `Rails.error` (Rails 7.0+). |
|
|
43
101
|
|
|
44
102
|
```ruby
|
|
45
103
|
RailsDiscordNotifier.configure do |config|
|
|
46
|
-
|
|
47
|
-
config.
|
|
104
|
+
config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL", nil)
|
|
105
|
+
config.release = ENV.fetch("GIT_COMMIT_SHA", nil)
|
|
48
106
|
|
|
49
|
-
|
|
50
|
-
config.username = ENV.fetch("ERROR_BOT_NAME", "Error Bot")
|
|
107
|
+
config.ignored_exceptions += %w[MyApp::HarmlessError]
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
# config.avatar_url = ENV.fetch("ERROR_BOT_AVATAR_URL", nil)
|
|
109
|
+
config.context = ->(env) { { "User" => env&.dig("warden")&.user&.id } }
|
|
54
110
|
end
|
|
55
111
|
```
|
|
56
112
|
|
|
57
|
-
|
|
113
|
+
A missing or malformed `webhook_url` logs a warning and disables the notifier.
|
|
114
|
+
It will never take down application boot.
|
|
115
|
+
|
|
116
|
+
### Ignored by default
|
|
117
|
+
|
|
118
|
+
`ActionController::RoutingError`, `ActiveRecord::RecordNotFound`,
|
|
119
|
+
`ActionController::InvalidAuthenticityToken`, `ActionController::UnknownFormat`,
|
|
120
|
+
`ActionController::BadRequest`, `Rack::QueryParser::ParameterTypeError` and
|
|
121
|
+
similar client-caused errors. See `Configuration::DEFAULT_IGNORED_EXCEPTIONS`
|
|
122
|
+
for the full list. Replace it wholesale with `config.ignored_exceptions = [...]`
|
|
123
|
+
or extend it with `+=`.
|
|
124
|
+
|
|
125
|
+
## Secret redaction
|
|
126
|
+
|
|
127
|
+
Parameters and query strings are filtered recursively with
|
|
128
|
+
`ActiveSupport::ParameterFilter`, using the union of:
|
|
129
|
+
|
|
130
|
+
1. your application's `config.filter_parameters`,
|
|
131
|
+
2. anything you add via `config.filter_parameters`, and
|
|
132
|
+
3. the gem's built-in list (`passw`, `secret`, `token`, `_key`, `crypt`, `salt`,
|
|
133
|
+
`certificate`, `otp`, `ssn`, `signature`, `authorization`, `cookie`,
|
|
134
|
+
`session`, `credential`).
|
|
58
135
|
|
|
59
|
-
|
|
136
|
+
Because it is a union, enabling this gem can never redact less than your
|
|
137
|
+
application already does. To send no parameters at all, set
|
|
138
|
+
`config.include_params = false`. The same filter applies to extra fields from
|
|
139
|
+
`config.context` and from `Rails.error` context.
|
|
60
140
|
|
|
61
|
-
|
|
141
|
+
Exception messages are free text rather than structured parameters, so they get
|
|
142
|
+
a best-effort scrub instead: `token=abc`, `password="x"`, `Api_Key: abc`,
|
|
143
|
+
`"password":"x"`, `Authorization: Bearer abc` and `redis://:password@host` style
|
|
144
|
+
fragments are replaced with `[FILTERED]`. This catches the usual culprits (a
|
|
145
|
+
database connection error carrying a password, an HTTP error carrying a URL with
|
|
146
|
+
a token) but cannot be exhaustive the way parameter filtering is.
|
|
62
147
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
148
|
+
Path segments are sent as they are. If a route puts a secret in the path (for
|
|
149
|
+
example `/password_resets/:token`), move it to the query string or the body,
|
|
150
|
+
where it is filtered.
|
|
151
|
+
|
|
152
|
+
Request bodies, headers and cookies are never sent.
|
|
153
|
+
|
|
154
|
+
In Google Chat, text like `<users/all>` or `<https://…|click here>` would become
|
|
155
|
+
an @all mention or a disguised link. Since anyone who can make your app raise
|
|
156
|
+
controls part of the report (the path, the User-Agent, often the exception
|
|
157
|
+
message), the gem defuses these with an invisible zero-width space after each
|
|
158
|
+
`<`. The report looks the same but can't ping the space or hide a link. The
|
|
159
|
+
space is still there if you copy text out of the report (for example
|
|
160
|
+
``in `<main>'`` from a backtrace), so retype the `<` if a search comes up empty.
|
|
161
|
+
|
|
162
|
+
## Google Chat rate limits
|
|
163
|
+
|
|
164
|
+
Google Chat accepts about one message per second per space, shared with
|
|
165
|
+
anything else posting there. Repeats of the same error are already collapsed
|
|
166
|
+
(`throttle_period`), but a burst of *different* errors beyond that rate is
|
|
167
|
+
rejected by Google with HTTP 429. Those rejections are logged, not retried.
|
|
168
|
+
|
|
169
|
+
## Reporting an exception yourself
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
rescue SomeError => e
|
|
173
|
+
RailsDiscordNotifier.notify(e, context: { "Tenant" => tenant.id })
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## How it gets your exceptions
|
|
178
|
+
|
|
179
|
+
| Source | Mechanism | Requires |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| Controllers, routing, Rack apps | `RailsDiscordNotifier::Middleware` | any supported Rails |
|
|
182
|
+
| Active Job, rake tasks, runners, `Rails.error.handle/record` | `RailsDiscordNotifier::ErrorSubscriber` | Rails 7.0+ |
|
|
183
|
+
| Anything else | `RailsDiscordNotifier.notify` | — |
|
|
184
|
+
|
|
185
|
+
The middleware observes and re-raises, so it never changes how your application
|
|
186
|
+
handles errors. When both entry points see the same exception, it is reported once.
|
|
187
|
+
|
|
188
|
+
"The same error" means the same exception class, raised from the same line, on
|
|
189
|
+
the same controller action. So `/orders/1` and `/orders/2` failing the same way
|
|
190
|
+
count as one error for `throttle_period`. At most 10 reports are delivered at
|
|
191
|
+
once; during a storm, anything beyond that is dropped with a log line rather
|
|
192
|
+
than starting more threads.
|
|
193
|
+
|
|
194
|
+
Asynchronous deliveries still in flight are waited for at process exit, so a
|
|
195
|
+
report from a rake task or `rails runner` is not lost when the process ends. Call
|
|
196
|
+
`RailsDiscordNotifier.notifier.flush` to wait for them explicitly.
|
|
197
|
+
|
|
198
|
+
## Compatibility
|
|
199
|
+
|
|
200
|
+
Ruby 3.1+, Rails 6.1+. Every supported combination is exercised in CI.
|
|
68
201
|
|
|
69
202
|
## Development
|
|
70
203
|
|
|
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
|
-
```
|
|
204
|
+
```bash
|
|
205
|
+
bin/setup # install dependencies
|
|
206
|
+
bundle exec rspec # run the test suite
|
|
207
|
+
bundle exec rubocop # lint
|
|
208
|
+
bundle exec appraisal install && bundle exec appraisal rspec # all Rails versions
|
|
209
|
+
```
|
|
83
210
|
|
|
84
|
-
|
|
211
|
+
To release: update `lib/rails_discord_notifier/version.rb` and `CHANGELOG.md`,
|
|
212
|
+
then `bundle exec rake release`.
|
|
85
213
|
|
|
86
|
-
|
|
87
|
-
[https://github.com/gettinDrikkieWithIt/rails_discord_notifier](https://github.com/gettinDrikkieWithIt/rails_discord_notifier)
|
|
214
|
+
## Contributing
|
|
88
215
|
|
|
89
|
-
|
|
216
|
+
Bug reports and pull requests are welcome at
|
|
217
|
+
<https://github.com/gettinDrikkieWithIt/rails_discord_notifier>.
|
|
218
|
+
Please follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
90
219
|
|
|
91
220
|
## License
|
|
92
221
|
|
|
93
|
-
|
|
94
|
-
See [LICENSE.txt](LICENSE.txt) for details.
|
|
222
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/exe/rails_discord_notifier
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
RailsDiscordNotifier.configure do |config|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
#
|
|
4
|
+
# Required. A Discord (https://discord.com/api/webhooks/...) or Google Chat
|
|
5
|
+
# (https://chat.googleapis.com/v1/spaces/...) webhook URL. The message format
|
|
6
|
+
# follows the URL. If it is missing or malformed the notifier stays disabled and
|
|
7
|
+
# logs a warning rather than breaking boot. Keep it out of source control.
|
|
8
|
+
config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL", nil).presence || ENV.fetch("GOOGLE_CHAT_WEBHOOK_URL", nil)
|
|
9
|
+
|
|
10
|
+
# Appearance of the bot in Discord. Google Chat uses the name and avatar set on
|
|
11
|
+
# the webhook itself.
|
|
12
|
+
config.username = ENV.fetch("ERROR_BOT_NAME", "Error Bot")
|
|
13
|
+
# config.avatar_url = ENV.fetch("ERROR_BOT_AVATAR_URL", nil)
|
|
14
|
+
|
|
15
|
+
# Reporting is off in development and test by default.
|
|
16
|
+
# config.enabled = Rails.env.production?
|
|
17
|
+
|
|
18
|
+
# Deliver off the request thread. Set to false only if you need delivery to be
|
|
19
|
+
# synchronous (for example inside a test).
|
|
20
|
+
# config.async = true
|
|
21
|
+
|
|
22
|
+
# Collapse repeats of the same error for this many seconds. 0 disables it.
|
|
23
|
+
# config.throttle_period = 60
|
|
24
|
+
|
|
25
|
+
# Network timeouts, in seconds. Kept short so a slow webhook never slows you down.
|
|
26
|
+
# config.open_timeout = 2
|
|
27
|
+
# config.read_timeout = 5
|
|
28
|
+
# config.write_timeout = 5
|
|
29
|
+
|
|
30
|
+
# Client-caused noise that is ignored by default. Append your own:
|
|
31
|
+
# config.ignored_exceptions += %w[MyApp::HarmlessError]
|
|
32
|
+
|
|
33
|
+
# Request parameters are included, filtered with your app's
|
|
34
|
+
# config.filter_parameters plus the gem's own list. Turn them off entirely with:
|
|
35
|
+
# config.include_params = false
|
|
36
|
+
# config.filter_parameters += %w[national_id]
|
|
37
|
+
|
|
38
|
+
# Extra fields on every report. Receives the Rack env (nil outside a request).
|
|
39
|
+
# config.context = lambda do |env|
|
|
40
|
+
# { "User" => env&.dig("warden")&.user&.id }
|
|
41
|
+
# end
|
|
42
|
+
|
|
43
|
+
# Shown as a field, handy for matching an error to a deploy.
|
|
44
|
+
# config.release = ENV.fetch("GIT_COMMIT_SHA", nil)
|
|
45
|
+
|
|
46
|
+
# Entry points. Middleware catches web requests; the Rails.error subscriber
|
|
47
|
+
# catches jobs, rake tasks and runners (Rails 7.0+).
|
|
48
|
+
# config.install_middleware = true
|
|
49
|
+
# config.install_error_subscriber = true
|
|
5
50
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
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 the service 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 or Google Chat 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
|
+
then $GOOGLE_CHAT_WEBHOOK_URL)
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def initialize(argv, out: $stdout, env: ENV)
|
|
22
|
+
@argv = Array(argv)
|
|
23
|
+
@out = out
|
|
24
|
+
@env = env
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns a process exit status.
|
|
28
|
+
def run
|
|
29
|
+
case @argv.first
|
|
30
|
+
when "test" then run_test
|
|
31
|
+
when "version" then print_version
|
|
32
|
+
when "help", nil then usage(0)
|
|
33
|
+
else usage(1)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def run_test
|
|
40
|
+
config = test_config
|
|
41
|
+
unless config.valid_webhook_url?
|
|
42
|
+
@out.puts(webhook_help)
|
|
43
|
+
return 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@out.puts("Posting a sample exception report to your webhook…")
|
|
47
|
+
deliver(config)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def deliver(config)
|
|
51
|
+
if Client.new(config).post(sample_payload(config))
|
|
52
|
+
@out.puts("Sent. Check your channel.")
|
|
53
|
+
0
|
|
54
|
+
else
|
|
55
|
+
@out.puts("The webhook did not accept the message. See the logged response above.")
|
|
56
|
+
1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def sample_payload(config)
|
|
61
|
+
exception = begin
|
|
62
|
+
raise "RailsDiscordNotifier test message - everything is wired up correctly"
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
e
|
|
65
|
+
end
|
|
66
|
+
Payload.build(exception, config: config).to_h
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_config
|
|
70
|
+
Configuration.new.tap do |config|
|
|
71
|
+
config.webhook_url = webhook_url
|
|
72
|
+
config.enabled = true
|
|
73
|
+
config.async = false
|
|
74
|
+
config.logger = Logger.new(@out)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def webhook_url
|
|
79
|
+
index = @argv.index("--webhook")
|
|
80
|
+
return @argv[index + 1] if index && @argv[index + 1]
|
|
81
|
+
|
|
82
|
+
# Blank counts as unset: deploy templates often export unused variables as "".
|
|
83
|
+
@env.values_at("DISCORD_WEBHOOK_URL", "GOOGLE_CHAT_WEBHOOK_URL").find { |url| !url.to_s.strip.empty? }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Never echoes the URL: it is a credential and this may be a CI log.
|
|
87
|
+
def webhook_help
|
|
88
|
+
if webhook_url.to_s.strip.empty?
|
|
89
|
+
"No webhook configured. Set DISCORD_WEBHOOK_URL or GOOGLE_CHAT_WEBHOOK_URL, or pass --webhook URL."
|
|
90
|
+
else
|
|
91
|
+
"Invalid webhook: expected an https://discord.com/api/webhooks/... " \
|
|
92
|
+
"or https://chat.googleapis.com/v1/spaces/... URL"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_version
|
|
97
|
+
@out.puts(VERSION)
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def usage(status)
|
|
102
|
+
@out.puts(USAGE)
|
|
103
|
+
status
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|