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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bca289e9c14290633d82aff1222693bf779c7bfca2da23a8688d5fc4dc713077
4
- data.tar.gz: 5c7e2a1b13f6fa5b028b95ce4ad9614a367b32cb3e13de98010ecb3d5a99ad6b
3
+ metadata.gz: 6527e49521148326dc463e191ed0fe51097c0625e7a09b89074615eb6b21f431
4
+ data.tar.gz: 3502ab8e704d8e0d995cdd17fb8d06fc36d9c6bae13122455cb33b013c9eff3c
5
5
  SHA512:
6
- metadata.gz: 6669af0bcaa175df2396c6e0ea557db4f58db4aca8d794a450d822737b734187454a4bd09f507b585905a328d236d5ace2d03ed2717d33c4548c70628e8f2554
7
- data.tar.gz: 87486bd08b6cbb223abebab6bf39d12b54dd4399fcf9ec37cae7d3f75eb889a12e45b32c18cc6df31635c39bee9a93785a29d7a9609073dad224e8ed27e8b7ff
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
- **Send Rails exceptions to Discord via an incoming webhook**
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
- RailsDiscordNotifier is a lightweight gem that captures unhandled exceptions in your Rails application and delivers detailed error reports directly to a Discord channel via a configurable webhook. You own the codebase and can customize the format, embed fields, and delivery settings to suit your needs.
15
+ ## Installation
6
16
 
7
- ## Features
17
+ ```ruby
18
+ gem "rails_discord_notifier", "~> 0.3"
19
+ ```
8
20
 
9
- - Middleware-based interception of all uncaught exceptions
10
- - Rich embed messages with exception class, message, backtrace, request method & URL, and timestamp
11
- - Configurable via environment variables and initializer
12
- - Safe error handling: notification failures are logged but don’t interrupt your app
21
+ ```bash
22
+ bundle install
23
+ rails generate rails_discord_notifier:install
24
+ ```
13
25
 
14
- ## Installation
26
+ Then set your webhook. The gem works out which service it is from the URL.
15
27
 
16
- Add the gem directly from RubyGems.org by including it in your Gemfile:
28
+ **Discord:** Server Settings → Integrations → Webhooks → Copy URL.
17
29
 
18
- ```ruby
19
- gem "rails_discord_notifier", "~> 0.1.0"
30
+ ```bash
31
+ export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/…/…"
20
32
  ```
21
33
 
22
- Then install:
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
- bundle install
38
+ export GOOGLE_CHAT_WEBHOOK_URL="https://chat.googleapis.com/v1/spaces/…/messages?key=…&token=…"
26
39
  ```
27
40
 
28
- Or use Bundler:
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 add rails_discord_notifier
48
+ bundle exec rails_discord_notifier test
32
49
  ```
33
50
 
34
- ## Usage
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
- Generate the default initializer in your Rails app:
56
+ ## What a report looks like
37
57
 
38
- ```bash
39
- rails generate rails_discord_notifier:install
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 `config/initializers/rails_discord_notifier.rb`, configure your webhook and options:
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
- # Discord webhook URL (required)
47
- config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL")
104
+ config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL", nil)
105
+ config.release = ENV.fetch("GIT_COMMIT_SHA", nil)
48
106
 
49
- # Bot username (optional, defaults to ENV["ERROR_BOT_NAME"] or "Error Bot")
50
- config.username = ENV.fetch("ERROR_BOT_NAME", "Error Bot")
107
+ config.ignored_exceptions += %w[MyApp::HarmlessError]
51
108
 
52
- # Bot avatar URL (optional)
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
- Ensure the middleware is loaded (added automatically by the Railtie).
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
- Any uncaught exception in your controllers or middleware stack will now send an embed to Discord before re-raising.
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
- ## Configuration Options
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
- | Option | Type | Default | Description |
64
- |---------------|--------|--------------------------------------|------------------------------------|
65
- | `webhook_url` | String | **Required** | Discord incoming webhook URL |
66
- | `username` | String | fetched from ENV `ERROR_BOT_NAME` (defaults to `"Error Bot"`) | Bot display name |
67
- | `avatar_url` | String | fetched from ENV `ERROR_BOT_AVATAR_URL` (defaults to `nil`) | URL of the bot avatar image |
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
- 1. Checkout the repo
72
- 2. Run `bin/setup` to install dependencies
73
- 3. Run `bundle exec rspec` to execute the test suite
74
- 4. Build and install locally:
75
- ```bash
76
- gem build rails_discord_notifier.gemspec
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
- ## Contributing
211
+ To release: update `lib/rails_discord_notifier/version.rb` and `CHANGELOG.md`,
212
+ then `bundle exec rake release`.
85
213
 
86
- Bug reports and pull requests are welcome on GitHub:
87
- [https://github.com/gettinDrikkieWithIt/rails_discord_notifier](https://github.com/gettinDrikkieWithIt/rails_discord_notifier)
214
+ ## Contributing
88
215
 
89
- Please follow the project’s Code of Conduct.
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
- This gem is available as open source under the terms of the MIT License.
94
- See [LICENSE.txt](LICENSE.txt) for details.
222
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -1,3 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "rails_discord_notifier"
5
+
6
+ exit RailsDiscordNotifier::CLI.new(ARGV).run
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators"
2
4
 
3
5
  module RailsDiscordNotifier
@@ -1,5 +1,50 @@
1
+ # frozen_string_literal: true
2
+
1
3
  RailsDiscordNotifier.configure do |config|
2
- config.webhook_url = ENV.fetch("DISCORD_WEBHOOK_URL")
3
- config.username = ENV.fetch("ERROR_BOT_NAME", "Error Bot")
4
- # config.avatar_url = "https://example.com/avatar.png"
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