eussiror 0.2.2 → 0.5.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: 54a8483b3b8378abea17e270fa6ed1f70316caaa5409ad8302bc9ed34e61a4c8
4
- data.tar.gz: ab05adc5d9249b40d0cdc5086c950939422b20be14d1984133da111a068f9a6f
3
+ metadata.gz: c4476191c4d0ae4fb165cb1542996388759025d3edc3948d9f2089bd79c1ff55
4
+ data.tar.gz: 041de6349fae9a39222aeba2991cb1ba5ca379b264fefd9ac49e35c0458eaf81
5
5
  SHA512:
6
- metadata.gz: d71a26061ddea12db66a67febb49d80c43e44b99f76565f02cd37a3d404b3e034eba7285b8d8fc89b2f0ecb4106e23e7f3ce03f3967a55459fad5e24a6671a04
7
- data.tar.gz: '06915b0ab2745c2e35a5631b111b3e38d3d543917effdeb2f1d6a0873d67ab20b551fbc559a86f1d5a7d6b5945503d8dbe99805344c642c196d1c4c999e86172'
6
+ metadata.gz: 1ac670063fc01936242fa48834725077733094df0c9e9dc3bbdc71e6dc3069558bcb0a6872f3ccbd7881723173b588c31f272da4b62d00cca201f48e87dd90dc
7
+ data.tar.gz: a239a33a76c34dedf39667ad4a642b31132ed63ca73789ef422d0f51199ae7102a136223bb8fd6fdbc5212dd7bbc9282f010f595288e38b1bcad53e7fc8e67f3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-04-02
11
+
12
+ ### Changed
13
+ - **Breaking:** Replace Rack middleware with `Rails.error.subscribe` (`ActiveSupport::ErrorReporter`). Eussiror now catches **all** unhandled exceptions (HTTP requests, ActiveJob, Action Cable, Rake, etc.), not just HTTP 500s.
14
+ - Issue titles use source-aware tags (`[request]`, `[job]`, `[cable]`, `[error]`) instead of `[500]`.
15
+ - `Eussiror::Middleware` has been removed; `Eussiror::ErrorSubscriber` replaces it.
16
+ - Source classification is now hybrid (strict mapping + heuristics on source strings) to reduce false `[error]` tags across Rails/runtime variants.
17
+
18
+ ### Added
19
+ - `Eussiror::ErrorSubscriber` — `Rails.error` subscriber registered via the Railtie.
20
+ - `Configuration#report_handled_errors` (default `false`) to optionally report errors caught by `Rails.error.handle`.
21
+ - **Source** line in issue Context section for non-default sources (e.g. `job`, `request`).
22
+ - Additional release environment variables (`SOURCE_VERSION`, `RAILWAY_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, `CI_COMMIT_SHA`, `GITHUB_SHA`) alongside existing keys.
23
+ - `Eussiror::ContextExtractor` to normalize heterogeneous Rails.error context payloads (string/symbol keys, nested env/request/headers, request objects).
24
+
25
+ ## [0.3.0] - 2026-04-02
26
+
27
+ ### Added
28
+ - `Configuration#issue_privacy` (`:minimal`, `:standard`, `:full`) and `Configuration#environment_name`.
29
+ - `Eussiror::IssueFormatting` for structured GitHub issue bodies and repeat-occurrence comments.
30
+ - Post-install notice after `rails generate eussiror:install`.
31
+
32
+ ### Changed
33
+ - Issue and comment content use the new formatting; optional release from `RELEASE`, `HEROKU_SLUG_COMMIT`, `REVISION`, or `GIT_COMMIT`.
34
+
10
35
  ## [0.2.2] - 2026-02-26
11
36
 
12
37
  ### Fixed
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![CI](https://github.com/EquipeTechnique/eussiror/actions/workflows/ci.yml/badge.svg)](https://github.com/EquipeTechnique/eussiror/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
8
 
9
- **Eussiror** automatically creates GitHub issues when your Rails application returns a 500 error in production. If the same error already has an open issue, it adds a comment with the new occurrence timestamp instead — keeping your issue tracker clean and deduplicated.
9
+ **Eussiror** automatically creates GitHub issues when your Rails application raises an unhandled exception whether from an HTTP request, an ActiveJob, Action Cable, or any other Rails execution context. If the same error already has an open issue, it adds a comment with the new occurrence instead — keeping your issue tracker clean and deduplicated.
10
10
 
11
11
  ---
12
12
 
@@ -50,7 +50,9 @@ bundle install
50
50
  rails generate eussiror:install
51
51
  ```
52
52
 
53
- The generator creates `config/initializers/eussiror.rb` with all available options commented out. To undo the installation:
53
+ The generator creates `config/initializers/eussiror.rb` with all available options commented out and prints a short **post-install notice** in the terminal (next steps, token safety, `issue_privacy`).
54
+
55
+ To undo the installation:
54
56
 
55
57
  ```bash
56
58
  rails destroy eussiror:install
@@ -60,61 +62,112 @@ rails destroy eussiror:install
60
62
 
61
63
  ## Configuration
62
64
 
63
- Edit the generated initializer:
65
+ Edit the generated initializer (`config/initializers/eussiror.rb`). Every assignable option is listed below.
64
66
 
65
67
  ```ruby
66
68
  # config/initializers/eussiror.rb
67
69
  Eussiror.configure do |config|
68
- # Required: GitHub personal access token with "repo" scope
70
+ # --- GitHub (required for reporting) ---
69
71
  config.github_token = ENV["GITHUB_TOKEN"]
70
-
71
- # Required: target repository in "owner/repository" format
72
72
  config.github_repository = "your-org/your-repo"
73
73
 
74
- # Environments where 500 errors will be reported (default: ["production"])
74
+ # --- Where and how to report ---
75
75
  config.environments = %w[production]
76
+ # config.issue_privacy = :minimal # :minimal | :standard | :full — see "Issue privacy"
77
+ config.async = true # false = synchronous (e.g. tests)
76
78
 
77
- # Labels applied to every new issue (optional)
78
- config.labels = %w[bug automated]
79
-
80
- # GitHub logins to assign to new issues (optional)
81
- config.assignees = []
79
+ # Also report errors caught by Rails.error.handle (default: false — only unhandled)
80
+ # config.report_handled_errors = false
82
81
 
83
- # Exception classes that should NOT trigger issue creation (optional)
84
- config.ignored_exceptions = %w[ActionController::RoutingError]
82
+ # --- Issue metadata (optional) ---
83
+ # config.labels = %w[bug automated]
84
+ # config.assignees = [] # GitHub usernames (logins), not display names
85
85
 
86
- # Set to false to report synchronously — recommended in test environments
87
- config.async = false
86
+ # --- Filtering (optional) ---
87
+ # config.ignored_exceptions = %w[ActionController::RoutingError]
88
88
  end
89
89
  ```
90
90
 
91
- ### Configuration options
91
+ ### Assignable options (`Eussiror.configure`)
92
92
 
93
93
  | Option | Type | Default | Description |
94
- |---|---|---|---|
95
- | `github_token` | String | `nil` | GitHub token with `repo` (or Issues write) permission |
96
- | `github_repository` | String | `nil` | Target repo in `owner/repo` format |
97
- | `environments` | Array | `["production"]` | Environments where reporting is active |
98
- | `labels` | Array | `[]` | Labels applied to created issues |
99
- | `assignees` | Array | `[]` | GitHub logins assigned to created issues |
100
- | `ignored_exceptions` | Array | `[]` | Exception class names (strings) to skip |
101
- | `async` | Boolean | `true` | Report in a background thread (set `false` in tests) |
94
+ |--------|------|---------|-------------|
95
+ | `github_token` | String | `nil` | Personal access token (classic `repo`, or fine-grained with Issues read/write on the target repo). **Required for any GitHub activity:** if blank, reporting is disabled. |
96
+ | `github_repository` | String | `nil` | Repository in `owner/repo` form. **Required with `github_token`** for reporting. |
97
+ | `environments` | Array of String | `["production"]` | Rails/Rack env names where reporting runs. Current env comes from `Rails.env` (Rails) or `ENV["RAILS_ENV"]` (default `"development"`). |
98
+ | `issue_privacy` | Symbol or String | `:minimal` | How much request/user context is copied into issue bodies and occurrence comments. Must be `minimal`, `standard`, or `full` (setter raises `ArgumentError` otherwise). See **Issue privacy** below. |
99
+ | `async` | Boolean | `true` | If `true`, `ErrorReporter` runs in a `Thread`; if `false`, reporting is synchronous (useful in tests or strict ordering). |
100
+ | `report_handled_errors` | Boolean | `false` | When `true`, errors caught by `Rails.error.handle` are also reported (by default only unhandled errors trigger a GitHub issue). |
101
+ | `labels` | Array of String | `[]` | Labels applied to **new** issues created by Eussiror (must exist on the repo). |
102
+ | `assignees` | Array of String | `[]` | GitHub **login** usernames assigned to new issues (empty = none). |
103
+ | `ignored_exceptions` | Array of String | `[]` | Exception **class names** to never report (e.g. `"ActionController::RoutingError"`). Unknown class names are ignored safely. |
104
+
105
+ Reporting runs only when **`#reporting_enabled?`** is true: configuration is **`#valid?`** (both `github_token` and `github_repository` non-blank after strip) **and** the current environment is listed in `environments`.
106
+
107
+ ### Read-only and predicates (`Eussiror.configuration`)
108
+
109
+ These are not set in the initializer; they are useful for debugging or tests.
110
+
111
+ | Method | Returns | Description |
112
+ |--------|---------|-------------|
113
+ | `#environment_name` | String | Label for the current environment (same source as the env guard: `Rails.env` or `ENV["RAILS_ENV"]`). Shown in GitHub issue **Context**. |
114
+ | `#issue_privacy` | Symbol | Current privacy level after assignment (`:minimal`, `:standard`, or `:full`). |
115
+ | `#valid?` | Boolean | `true` if `github_token` and `github_repository` are both non-blank. |
116
+ | `#reporting_enabled?` | Boolean | `true` if `#valid?` and the current env is in `environments`. |
117
+
118
+ ### Issue privacy
119
+
120
+ GitHub issues may be visible to people outside your core team (public repo, or future collaborators). **`issue_privacy`** controls how much context is copied into each issue:
121
+
122
+ | Value | Request in issue | User in issue | Typical use |
123
+ |-------|------------------|---------------|-------------|
124
+ | `:minimal` | HTTP method + path only | Never | Public repos, OSS, minimal footprint (default) |
125
+ | `:standard` | Also Remote IP and `User-Agent` when present in the Rack `env` | Never | Private repo, ops-friendly debugging |
126
+ | `:full` | Same as `:standard` | **User** section if you set Rack keys below | Private repo, team accepts user context in issues |
127
+
128
+ ### Optional user context (Rack `env`)
129
+
130
+ When `issue_privacy` is `:full`, set these from your middleware (after authentication):
131
+
132
+ | Key | Meaning |
133
+ |-----|---------|
134
+ | `env["eussiror.user_id"]` | Stable user identifier (e.g. database id) |
135
+ | `env["eussiror.user_label"]` | Optional human-readable label (e.g. email or login) — only if your policy allows it |
136
+
137
+ ### Context normalization
138
+
139
+ `Rails.error` can provide context in multiple shapes (flat hash, symbol keys, nested env/request, request object). Eussiror normalizes this before formatting issues/comments.
140
+
141
+ Recognized families of keys (first non-empty wins):
142
+ - **Request method/path:** `REQUEST_METHOD` / `PATH_INFO` (string or symbol), nested `env`/`rack`, request object (`request_method`, `fullpath`, `path`)
143
+ - **IP/user-agent:** `REMOTE_ADDR` / `HTTP_USER_AGENT`, nested `headers["User-Agent"]`, request object (`remote_ip`, `user_agent`, `ip`)
144
+ - **User fields:** `eussiror.user_id`, `eussiror.user_label`, plus `user_id` / `user_label` fallbacks
145
+
146
+ Explicit top-level keys win over nested values when both are present.
147
+
148
+ **Release** in the issue **Context** section is taken from the first non-empty environment variable among: `RELEASE` (recommended explicit override), then `SOURCE_VERSION` (Scalingo and others), `HEROKU_SLUG_COMMIT`, `RAILWAY_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, `REVISION`, `GIT_COMMIT`, `CI_COMMIT_SHA` (GitLab CI, etc.), `GITHUB_SHA` (GitHub Actions).
102
149
 
103
150
  ---
104
151
 
105
152
  ## How it works
106
153
 
107
- When a 500 error occurs:
154
+ Eussiror subscribes to **`Rails.error`** (`ActiveSupport::ErrorReporter`, available since Rails 7.1). Rails wraps every execution context — HTTP requests, ActiveJob, Action Cable, etc. — in this reporter, so Eussiror catches exceptions regardless of origin.
108
155
 
109
- 1. The Rack middleware catches the rendered 500 response.
110
- 2. A **fingerprint** is computed from the exception class, message, and first application backtrace line.
111
- 3. The GitHub API is searched for an open issue containing that fingerprint.
112
- 4. If **no issue exists** a new issue is created with the exception details.
113
- 5. If **an issue exists** → a comment with the current timestamp is added.
156
+ 1. An unhandled exception (or a handled one if `report_handled_errors` is enabled) reaches `Rails.error`.
157
+ 2. `Eussiror::ErrorSubscriber` receives the exception with its `severity`, `source`, and `context`.
158
+ 3. A **fingerprint** is computed from the exception class, message, and first application backtrace line.
159
+ 4. The GitHub API is searched for an open issue containing that fingerprint.
160
+ 5. If **no issue exists** → a new issue is created with structured details.
161
+ 6. If **an issue exists** → a comment with the new occurrence is added.
162
+
163
+ The issue title includes a **source tag** (`[request]`, `[job]`, `[cable]`, or `[error]`) so you can tell at a glance where the exception came from. Source classification uses a hybrid strategy:
164
+ - strict mapping for known Rails sources,
165
+ - heuristic fallback using source prefixes/contains (`ActiveJob`, `ActionCable`, `ActionDispatch`/`Rack`),
166
+ - final fallback to `[error]`.
114
167
 
115
168
  ### Example GitHub issue
116
169
 
117
- **Title:** `[500] RuntimeError: something went wrong`
170
+ **Title:** `[request] RuntimeError: something went wrong`
118
171
 
119
172
  **Body:**
120
173
  ```
@@ -123,8 +176,22 @@ When a 500 error occurs:
123
176
  **Exception:** `RuntimeError`
124
177
  **Message:** something went wrong
125
178
  **First occurrence:** 2026-02-26 10:30:00 UTC
179
+
180
+ ## Context
181
+
182
+ **Environment:** `production`
183
+ **Source:** `request` (omitted when the source is the default "error")
184
+ **Release:** `abc123` (when a release env var from the list in **Optional user context** is set)
185
+
186
+ ## User
187
+
188
+ (Only when issue_privacy is :full and eussiror.user_* keys are set.)
189
+
190
+ ## Request
191
+
126
192
  **Request:** `GET /dashboard`
127
- **Remote IP:** 1.2.3.4
193
+ **Remote IP:** 1.2.3.4 (only when issue_privacy is :standard or :full)
194
+ **User-Agent:** … (same)
128
195
 
129
196
  ## Backtrace
130
197
 
@@ -134,10 +201,16 @@ app/controllers/dashboard_controller.rb:42:in 'index'
134
201
 
135
202
  ### Example occurrence comment
136
203
 
204
+ With `:minimal` (default):
205
+
137
206
  ```
138
207
  **New occurrence:** 2026-02-26 14:55:02 UTC
208
+
209
+ **Request:** `GET /dashboard`
139
210
  ```
140
211
 
212
+ With `:standard` or `:full`, Remote IP and User-Agent are included when present; with `:full`, **User id** appears when `env["eussiror.user_id"]` is set.
213
+
141
214
  ---
142
215
 
143
216
  ## GitHub token setup
@@ -208,10 +281,12 @@ lib/
208
281
  └── eussiror/
209
282
  ├── version.rb # Gem version constant
210
283
  ├── configuration.rb # Configuration value object + guards
211
- ├── railtie.rb # Rails integration: inserts Middleware into the stack
212
- ├── middleware.rb # Rack middleware: detects 500s and calls ErrorReporter
284
+ ├── railtie.rb # Rails integration: subscribes to Rails.error
285
+ ├── error_subscriber.rb # ActiveSupport::ErrorReporter subscriber
213
286
  ├── fingerprint.rb # Computes a stable SHA256 fingerprint per exception type
214
287
  ├── github_client.rb # GitHub REST API v3 calls via Net::HTTP
288
+ ├── issue_formatting.rb # Issue body and occurrence comment text
289
+ ├── release_env.rb # Optional release label from ENV (PaaS/CI keys)
215
290
  └── error_reporter.rb # Orchestrator: fingerprint → search → create or comment
216
291
 
217
292
  lib/generators/eussiror/install/
@@ -219,32 +294,28 @@ lib/generators/eussiror/install/
219
294
  └── templates/initializer.rb.tt # Template for config/initializers/eussiror.rb
220
295
  ```
221
296
 
222
- ### Request / error flow
297
+ ### Error flow
223
298
 
224
299
  ```
225
- HTTP Request
226
-
227
-
228
- Eussiror::Middleware (outermost Rack middleware)
300
+ Any Rails execution context
301
+ (HTTP request, ActiveJob, Action Cable, Rake, etc.)
229
302
 
230
-
231
- ActionDispatch::ShowExceptions (catches Rails exceptions, stores them in env)
303
+ unhandled exception
304
+ Rails.error (ActiveSupport::ErrorReporter)
232
305
 
306
+ ▼ report(error, handled:, severity:, context:, source:)
307
+ Eussiror::ErrorSubscriber
308
+ │ filters: handled? severity == :error?
233
309
 
234
- [... rest of Rails stack ...]
235
-
236
- ▼ (response travels back up)
237
- ActionDispatch::ShowExceptions → sets env["action_dispatch.exception"]
238
- returns HTTP 500 response
310
+ Eussiror::ErrorReporter.report(exception, context, source:)
239
311
 
240
-
241
- Eussiror::Middleware
242
- ├── status == 500 AND env["action_dispatch.exception"] present?
243
- YESErrorReporter.report(exception, env)
244
- │ NO → pass response through unchanged
312
+ ├── Fingerprint.compute(exception)
313
+ ├── GithubClient.find_issue(fingerprint)
314
+ │ found → GithubClient.add_comment (occurrence)
315
+ absentGithubClient.create_issue (structured body)
245
316
 
246
317
 
247
- HTTP Response returned to client
318
+ GitHub Issues
248
319
  ```
249
320
 
250
321
  ### Component responsibilities
@@ -253,18 +324,17 @@ HTTP Response returned to client
253
324
  Top-level module. Holds the singleton `configuration` object and exposes `.configure { |c| }`. All other components read `Eussiror.configuration`.
254
325
 
255
326
  #### `Eussiror::Configuration`
256
- Plain Ruby value object with attr_accessors for every option. Contains the two guard predicates used by `ErrorReporter`:
327
+ Plain Ruby value object with attr_accessors for every option. Exposes `#environment_name` for issue bodies. Contains the two guard predicates used by `ErrorReporter`:
257
328
  - `#valid?` — both token and repository are present
258
329
  - `#reporting_enabled?` — valid config AND current Rails env is in `environments`
259
330
 
331
+ `#issue_privacy` must be `:minimal`, `:standard`, or `:full` (setter raises `ArgumentError` otherwise).
332
+
260
333
  #### `Eussiror::Railtie`
261
- Rails `Railtie` that runs one initializer: it inserts `Eussiror::Middleware` **before** `ActionDispatch::ShowExceptions` in the middleware stack. This positions our middleware as the outermost wrapper, so it sees the fully rendered 500 response on the way back out.
334
+ Rails `Railtie` that runs one initializer: it registers `Eussiror::ErrorSubscriber` with `Rails.error.subscribe`, hooking into every execution context Rails wraps (requests, jobs, channels, etc.).
262
335
 
263
- #### `Eussiror::Middleware`
264
- Rack middleware with a standard `#call(env)` interface.
265
- - On a normal response: passes through.
266
- - On a 500 response with `env["action_dispatch.exception"]`: calls `ErrorReporter.report`.
267
- - On a re-raised exception (non-standard setups): calls `ErrorReporter.report` before re-raising.
336
+ #### `Eussiror::ErrorSubscriber`
337
+ Implements the `ActiveSupport::ErrorReporter` subscriber interface (`#report(error, handled:, severity:, context:, source:)`). Filters out handled errors (unless `report_handled_errors` is enabled) and non-`:error` severities, then delegates to `ErrorReporter`.
268
338
 
269
339
  #### `Eussiror::Fingerprint`
270
340
  Stateless module with a single public method: `.compute(exception) → String`.
@@ -290,21 +360,22 @@ Thin HTTP client wrapping three GitHub REST API v3 endpoints. Uses only `Net::HT
290
360
  | `#add_comment(issue_number, body:)` | `POST /repos/{owner}/{repo}/issues/{n}/comments` | Returns comment id |
291
361
 
292
362
  #### `Eussiror::ErrorReporter`
293
- Stateless module that orchestrates the full reporting flow. Called by the middleware.
363
+ Stateless module that orchestrates the full reporting flow. Called by `ErrorSubscriber`.
294
364
 
295
365
  1. Checks `Eussiror.configuration.reporting_enabled?` — returns early if not.
296
366
  2. Checks `ignored_exceptions` — returns early if matched.
297
- 3. Dispatches in a `Thread.new` when `config.async` is `true` (default), or inline otherwise.
298
- 4. Computes fingerprint searches GitHub creates issue or adds comment.
299
- 5. All GitHub errors are rescued and emitted as `warn` messages the gem **never crashes your app**.
367
+ 3. Maps the `source` string to a human-readable tag (`[request]`, `[job]`, `[cable]`, or `[error]`).
368
+ 4. Dispatches in a `Thread.new` when `config.async` is `true` (default), or inline otherwise.
369
+ 5. Computes fingerprint → searches GitHub creates issue (structured body: Error Details, Context, optional User, Request, Backtrace) or adds an occurrence comment.
370
+ 6. All GitHub errors are rescued and emitted as `warn` messages — the gem **never crashes your app**.
300
371
 
301
372
  #### `Eussiror::Generators::InstallGenerator`
302
- Standard `Rails::Generators::Base` subclass. Copies `templates/initializer.rb.tt` to `config/initializers/eussiror.rb` using Thor's `template` method. Supports `rails destroy eussiror:install` for clean uninstallation.
373
+ Standard `Rails::Generators::Base` subclass. Copies `templates/initializer.rb.tt` to `config/initializers/eussiror.rb` using Thor's `template` method, then prints a **post-install notice** (`show_post_install_notice`). Supports `rails destroy eussiror:install` for clean uninstallation.
303
374
 
304
375
  ### Testing approach
305
376
 
306
- - **Unit specs**: each component is tested in isolation. `GithubClient` uses `WebMock` to stub HTTP calls. `ErrorReporter` uses RSpec doubles for `GithubClient`.
307
- - **Generator spec**: uses Rails generator test helpers (`prepare_destination`, `run_generator`).
377
+ - **Unit specs**: each component is tested in isolation. `GithubClient` uses `WebMock` to stub HTTP calls. `ErrorReporter` and `ErrorSubscriber` use RSpec doubles.
378
+ - **Generator spec**: uses Rails generator test helpers (`prepare_destination`, `invoke_all`) and asserts post-install output.
308
379
  - **Appraisals**: the `Appraisals` file defines three gemfiles (`rails-7.2`, `rails-8.0`, `rails-8.1`) so the full test suite runs against each supported Rails version.
309
380
 
310
381
  ---
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Eussiror
4
4
  class Configuration
5
+ # Allowed values for #issue_privacy: :minimal (default), :standard, :full
6
+ ISSUE_PRIVACY_LEVELS = %i[minimal standard full].freeze
7
+
5
8
  # Required settings
6
9
  attr_accessor :github_token, :github_repository
7
10
 
@@ -17,12 +20,25 @@ module Eussiror
17
20
  # Set to false to report synchronously (useful in tests)
18
21
  attr_accessor :async
19
22
 
23
+ # When true, errors caught by Rails.error.handle are also reported (default: false).
24
+ attr_accessor :report_handled_errors
25
+
26
+ # Controls how much request/user context is included in GitHub issue bodies and
27
+ # occurrence comments. :minimal is safest for public repos; :full for trusted private teams.
28
+ attr_reader :issue_privacy
29
+
20
30
  def initialize
21
- @environments = %w[production]
22
- @labels = []
23
- @assignees = []
24
- @ignored_exceptions = []
25
- @async = true
31
+ @environments = %w[production]
32
+ @labels = []
33
+ @assignees = []
34
+ @ignored_exceptions = []
35
+ @async = true
36
+ @report_handled_errors = false
37
+ @issue_privacy = :minimal
38
+ end
39
+
40
+ def issue_privacy=(value)
41
+ @issue_privacy = normalize_issue_privacy(value)
26
42
  end
27
43
 
28
44
  def valid?
@@ -34,8 +50,25 @@ module Eussiror
34
50
  valid? && environments.include?(current_environment)
35
51
  end
36
52
 
53
+ # Current Rails / Rack environment name (for display in issue bodies).
54
+ #
55
+ # @return [String]
56
+ def environment_name
57
+ current_environment
58
+ end
59
+
37
60
  private
38
61
 
62
+ def normalize_issue_privacy(value)
63
+ return :minimal if value.nil?
64
+
65
+ sym = value.respond_to?(:to_sym) ? value.to_sym : value.to_s.to_sym
66
+ return sym if ISSUE_PRIVACY_LEVELS.include?(sym)
67
+
68
+ raise ArgumentError,
69
+ "issue_privacy must be one of #{ISSUE_PRIVACY_LEVELS.join(', ')}, got #{value.inspect}"
70
+ end
71
+
39
72
  def current_environment
40
73
  return ENV.fetch("RAILS_ENV", "development") unless defined?(Rails)
41
74
  return Rails.env.to_s if Rails.respond_to?(:env)
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eussiror
4
+ # Normalizes heterogeneous Rails.error context payloads into a stable structure
5
+ # consumed by issue formatting.
6
+ module ContextExtractor
7
+ class << self
8
+ def normalize(context)
9
+ ctx = context.is_a?(Hash) ? context : {}
10
+ req = request_object(ctx)
11
+ env = nested_hash(ctx, :env) || nested_hash(ctx, :rack)
12
+ headers = nested_hash(ctx, :headers)
13
+
14
+ {
15
+ request_method: request_method(ctx, env, req),
16
+ path: request_path(ctx, env, req),
17
+ remote_ip: remote_ip(ctx, env, req),
18
+ user_agent: user_agent(ctx, env, headers, req),
19
+ user_id: user_id(ctx, env),
20
+ user_label: user_label(ctx, env)
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def request_method(ctx, env, req)
27
+ resolve_field(
28
+ [ctx, "REQUEST_METHOD"],
29
+ [env, "REQUEST_METHOD"],
30
+ [req, :request_method]
31
+ )
32
+ end
33
+
34
+ def request_path(ctx, env, req)
35
+ resolve_field(
36
+ [ctx, "PATH_INFO"],
37
+ [env, "PATH_INFO"],
38
+ [ctx, "path"],
39
+ [req, :fullpath],
40
+ [req, :path]
41
+ )
42
+ end
43
+
44
+ def remote_ip(ctx, env, req)
45
+ resolve_field(
46
+ [ctx, "REMOTE_ADDR"],
47
+ [env, "REMOTE_ADDR"],
48
+ [ctx, "remote_ip"],
49
+ [req, :remote_ip],
50
+ [ctx, "ip"],
51
+ [req, :ip]
52
+ )
53
+ end
54
+
55
+ def user_agent(ctx, env, headers, req)
56
+ resolve_field(
57
+ [ctx, "HTTP_USER_AGENT"],
58
+ [env, "HTTP_USER_AGENT"],
59
+ [headers, "User-Agent"],
60
+ [headers, "HTTP_USER_AGENT"],
61
+ [req, :user_agent]
62
+ )
63
+ end
64
+
65
+ def user_id(ctx, env)
66
+ resolve_field(
67
+ [ctx, IssueFormatting::USER_ID_KEY],
68
+ [env, IssueFormatting::USER_ID_KEY],
69
+ [ctx, "user_id"]
70
+ )
71
+ end
72
+
73
+ def user_label(ctx, env)
74
+ resolve_field(
75
+ [ctx, IssueFormatting::USER_LABEL_KEY],
76
+ [env, IssueFormatting::USER_LABEL_KEY],
77
+ [ctx, "user_label"]
78
+ )
79
+ end
80
+
81
+ def request_object(context)
82
+ fetch_key(context, "request") ||
83
+ fetch_key(context, "request_object") ||
84
+ fetch_key(context, "action_dispatch.request")
85
+ end
86
+
87
+ def nested_hash(context, key)
88
+ val = fetch_key(context, key)
89
+ val.is_a?(Hash) ? val : nil
90
+ end
91
+
92
+ def from_hash(container, key)
93
+ return nil unless container
94
+
95
+ if container.is_a?(Hash)
96
+ fetch_key(container, key)
97
+ elsif container.respond_to?(key)
98
+ container.public_send(key)
99
+ end
100
+ rescue NoMethodError
101
+ nil
102
+ end
103
+
104
+ def fetch_key(hash, key)
105
+ return nil unless hash.is_a?(Hash)
106
+
107
+ str_key = key.to_s
108
+ hash[str_key] || hash[str_key.to_sym]
109
+ end
110
+
111
+ def resolve_field(*lookups)
112
+ values = lookups.map { |(container, key)| from_hash(container, key) }
113
+ first_present(*values)
114
+ end
115
+
116
+ def first_present(*values)
117
+ values.find { |v| present_string?(v) }
118
+ end
119
+
120
+ def present_string?(value)
121
+ !value.nil? && !value.to_s.strip.empty?
122
+ end
123
+ end
124
+ end
125
+ end
@@ -2,22 +2,31 @@
2
2
 
3
3
  module Eussiror
4
4
  module ErrorReporter
5
- # Maximum number of backtrace lines included in an issue body.
6
5
  MAX_BACKTRACE_LINES = 20
7
6
 
7
+ USER_ID_KEY = IssueFormatting::USER_ID_KEY
8
+ USER_LABEL_KEY = IssueFormatting::USER_LABEL_KEY
9
+
10
+ # Maps Rails.error source strings to short tags for issue titles.
11
+ SOURCE_TAGS = {
12
+ "application" => "error",
13
+ "ActionDispatch::Executor" => "request",
14
+ "ActiveJob" => "job",
15
+ "ActionCable::Connection" => "cable"
16
+ }.freeze
17
+
8
18
  class << self
9
- # Entry point called by the middleware.
10
- # Checks configuration guards, then dispatches async or sync.
11
- def report(exception, env = {})
19
+ def report(exception, context = {}, source: "application")
12
20
  config = Eussiror.configuration
13
21
 
14
22
  return unless config.reporting_enabled?
15
23
  return if ignored?(exception, config)
16
24
 
25
+ tag = source_tag_for(source)
17
26
  if config.async
18
- Thread.new { process(exception, env, config) }
27
+ Thread.new { process(exception, context, config, tag) }
19
28
  else
20
- process(exception, env, config)
29
+ process(exception, context, config, tag)
21
30
  end
22
31
  rescue StandardError => e
23
32
  warn "[Eussiror] ErrorReporter.report raised an unexpected error: #{e.class}: #{e.message}"
@@ -33,7 +42,19 @@ module Eussiror
33
42
  end
34
43
  end
35
44
 
36
- def process(exception, env, config)
45
+ def source_tag_for(source)
46
+ src = source.to_s
47
+ return SOURCE_TAGS[src] if SOURCE_TAGS.key?(src)
48
+
49
+ down = src.downcase
50
+ return "job" if down.include?("activejob")
51
+ return "cable" if down.include?("actioncable")
52
+ return "request" if down.include?("actiondispatch") || down.include?("rack")
53
+
54
+ "error"
55
+ end
56
+
57
+ def process(exception, context, config, tag)
37
58
  fingerprint = Fingerprint.compute(exception)
38
59
  client = GithubClient.new(
39
60
  token: config.github_token,
@@ -43,11 +64,13 @@ module Eussiror
43
64
  existing_issue = client.find_issue(fingerprint)
44
65
 
45
66
  if existing_issue
46
- client.add_comment(existing_issue, body: occurrence_comment)
67
+ client.add_comment(existing_issue, body: IssueFormatting.occurrence_comment(context, config))
47
68
  else
48
69
  client.create_issue(
49
- title: issue_title(exception),
50
- body: issue_body(exception, env, fingerprint),
70
+ title: issue_title(exception, tag),
71
+ body: IssueFormatting.issue_body(
72
+ exception, context, fingerprint, config, MAX_BACKTRACE_LINES, source_tag: tag
73
+ ),
51
74
  labels: config.labels,
52
75
  assignees: config.assignees
53
76
  )
@@ -56,60 +79,9 @@ module Eussiror
56
79
  warn "[Eussiror] Failed to report exception to GitHub: #{e.class}: #{e.message}"
57
80
  end
58
81
 
59
- def issue_title(exception)
82
+ def issue_title(exception, tag)
60
83
  message = exception.message.to_s.lines.first.to_s.strip[0, 120]
61
- "[500] #{exception.class}: #{message}"
62
- end
63
-
64
- def issue_body(exception, env, fingerprint)
65
- request_info = build_request_info(env)
66
- backtrace = format_backtrace(exception)
67
-
68
- <<~BODY
69
- ## Error Details
70
-
71
- **Exception:** `#{exception.class}`
72
- **Message:** #{exception.message}
73
- **First occurrence:** #{current_timestamp}
74
- #{request_info}
75
-
76
- ## Backtrace
77
-
78
- ```
79
- #{backtrace}
80
- ```
81
-
82
- <!-- #{GithubClient::FINGERPRINT_MARKER}:#{fingerprint} -->
83
- BODY
84
- end
85
-
86
- def occurrence_comment
87
- "**New occurrence:** #{current_timestamp}"
88
- end
89
-
90
- def current_timestamp
91
- Time.now.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
92
- end
93
-
94
- def build_request_info(env)
95
- return "" if env.blank?
96
-
97
- method = env["REQUEST_METHOD"]
98
- path = env["PATH_INFO"]
99
- remote_addr = env["REMOTE_ADDR"]
100
-
101
- return "" unless method && path
102
-
103
- parts = ["**Request:** `#{method} #{path}`"]
104
- parts << "**Remote IP:** #{remote_addr}" if remote_addr
105
-
106
- "\n#{parts.join("\n")}"
107
- end
108
-
109
- def format_backtrace(exception)
110
- (exception.backtrace || [])
111
- .first(MAX_BACKTRACE_LINES)
112
- .join("\n")
84
+ "[#{tag}] #{exception.class}: #{message}"
113
85
  end
114
86
  end
115
87
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eussiror
4
+ # Rails.error subscriber (ActiveSupport::ErrorReporter).
5
+ # Registered via Railtie so that Eussiror receives every unhandled exception
6
+ # regardless of origin (HTTP request, ActiveJob, Action Cable, Rake, etc.).
7
+ class ErrorSubscriber
8
+ def report(error, handled:, severity:, context:, source: "application")
9
+ return if handled && !Eussiror.configuration.report_handled_errors
10
+ return unless severity == :error
11
+
12
+ ErrorReporter.report(error, context, source: source)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eussiror
4
+ # Builds GitHub issue bodies and occurrence comments.
5
+ # Accepts either a Rack env hash or a plain Rails.error context hash.
6
+ module IssueFormatting
7
+ USER_ID_KEY = "eussiror.user_id"
8
+ USER_LABEL_KEY = "eussiror.user_label"
9
+
10
+ class << self
11
+ def issue_body(exception, context, fingerprint, config, max_backtrace_lines, source_tag: "error")
12
+ normalized = ContextExtractor.normalize(context)
13
+ <<~BODY
14
+ #{error_details_section(exception)}
15
+ #{context_section(config, source_tag)}
16
+ #{user_section(normalized, config)}
17
+ #{request_section(normalized, config)}
18
+ ## Backtrace
19
+
20
+ ```
21
+ #{format_backtrace(exception, max_backtrace_lines)}
22
+ ```
23
+
24
+ <!-- #{GithubClient::FINGERPRINT_MARKER}:#{fingerprint} -->
25
+ BODY
26
+ end
27
+
28
+ def occurrence_comment(context, config)
29
+ normalized = ContextExtractor.normalize(context)
30
+ occurrence_lines(normalized, config).join("\n\n")
31
+ end
32
+
33
+ private
34
+
35
+ def format_backtrace(exception, max_lines)
36
+ (exception.backtrace || []).first(max_lines).join("\n")
37
+ end
38
+
39
+ def error_details_section(exception)
40
+ ts = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
41
+ <<~SECTION
42
+ ## Error Details
43
+
44
+ **Exception:** `#{exception.class}`
45
+ **Message:** #{exception.message}
46
+ **First occurrence:** #{ts}
47
+ SECTION
48
+ end
49
+
50
+ def context_section(config, source_tag)
51
+ lines = ["**Environment:** `#{config.environment_name}`"]
52
+ lines << "**Source:** `#{source_tag}`" unless source_tag == "error"
53
+ rel = ReleaseEnv.label
54
+ lines << "**Release:** `#{rel}`" if rel
55
+
56
+ <<~SECTION
57
+
58
+ ## Context
59
+
60
+ #{lines.join("\n")}
61
+ SECTION
62
+ end
63
+
64
+ def user_section(normalized, config)
65
+ return "" unless config.issue_privacy == :full
66
+
67
+ uid = normalized[:user_id]
68
+ label = normalized[:user_label]
69
+ return "" if string_blank?(uid) && string_blank?(label)
70
+
71
+ parts = []
72
+ parts << "**User id:** `#{uid}`" unless string_blank?(uid)
73
+ parts << "**User label:** #{label}" unless string_blank?(label)
74
+
75
+ <<~SECTION
76
+
77
+ ## User
78
+
79
+ #{parts.join("\n")}
80
+ SECTION
81
+ end
82
+
83
+ def request_section(normalized, config)
84
+ fragment = build_request_fragment(normalized, config)
85
+ return "" if string_blank?(fragment)
86
+
87
+ <<~SECTION
88
+
89
+ ## Request
90
+
91
+ #{fragment.strip}
92
+ SECTION
93
+ end
94
+
95
+ def build_request_fragment(normalized, config)
96
+ method = normalized[:request_method]
97
+ path = normalized[:path]
98
+ return "" unless method && path
99
+
100
+ lines = ["**Request:** `#{method} #{path}`"]
101
+ return lines.join("\n") if config.issue_privacy == :minimal
102
+
103
+ append_ip_and_agent(lines, normalized)
104
+ lines.join("\n")
105
+ end
106
+
107
+ def append_ip_and_agent(lines, normalized)
108
+ ra = normalized[:remote_ip]
109
+ lines << "**Remote IP:** #{ra}" unless string_blank?(ra)
110
+ ua = normalized[:user_agent]
111
+ lines << "**User-Agent:** #{ua}" unless string_blank?(ua)
112
+ end
113
+
114
+ def occurrence_lines(normalized, config)
115
+ ts = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
116
+ lines = ["**New occurrence:** #{ts}"]
117
+ req = request_summary_line(normalized)
118
+ lines << "**Request:** `#{req}`" if req
119
+ append_occurrence_privacy(lines, normalized, config)
120
+ lines
121
+ end
122
+
123
+ def append_occurrence_privacy(lines, normalized, config)
124
+ case config.issue_privacy
125
+ when :standard, :full
126
+ ra = normalized[:remote_ip]
127
+ lines << "**Remote IP:** #{ra}" unless string_blank?(ra)
128
+ ua = normalized[:user_agent]
129
+ lines << "**User-Agent:** #{ua}" unless string_blank?(ua)
130
+ end
131
+ return unless config.issue_privacy == :full
132
+
133
+ uid = normalized[:user_id]
134
+ lines << "**User id:** `#{uid}`" unless string_blank?(uid)
135
+ end
136
+
137
+ def request_summary_line(normalized)
138
+ method = normalized[:request_method]
139
+ path = normalized[:path]
140
+ return nil unless method && path
141
+
142
+ "#{method} #{path}"
143
+ end
144
+
145
+ def string_blank?(value)
146
+ value.nil? || value.to_s.strip.empty?
147
+ end
148
+ end
149
+ end
150
+ end
@@ -4,10 +4,8 @@ require "rails/railtie"
4
4
 
5
5
  module Eussiror
6
6
  class Railtie < Rails::Railtie
7
- # Insert before ShowExceptions so we wrap the full Rails error rendering.
8
- # On the way back out, we inspect the rendered response and env to detect 500s.
9
- initializer "eussiror.insert_middleware" do |app|
10
- app.middleware.insert_before ActionDispatch::ShowExceptions, Eussiror::Middleware
7
+ initializer "eussiror.subscribe_error_reporter" do
8
+ Rails.error.subscribe(Eussiror::ErrorSubscriber.new)
11
9
  end
12
10
  end
13
11
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eussiror
4
+ # First non-empty value wins. `RELEASE` is the explicit override; others are common PaaS/CI conventions.
5
+ module ReleaseEnv
6
+ KEYS = %w[
7
+ RELEASE
8
+ SOURCE_VERSION
9
+ HEROKU_SLUG_COMMIT
10
+ RAILWAY_GIT_COMMIT_SHA
11
+ RENDER_GIT_COMMIT
12
+ REVISION
13
+ GIT_COMMIT
14
+ CI_COMMIT_SHA
15
+ GITHUB_SHA
16
+ ].freeze
17
+
18
+ def self.label
19
+ KEYS.lazy.map { |k| ENV.fetch(k, nil) }.find { |v| v.to_s.strip.length.positive? }
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Eussiror
4
- VERSION = "0.2.2"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/eussiror.rb CHANGED
@@ -4,8 +4,11 @@ require "eussiror/version"
4
4
  require "eussiror/configuration"
5
5
  require "eussiror/fingerprint"
6
6
  require "eussiror/github_client"
7
+ require "eussiror/release_env"
8
+ require "eussiror/context_extractor"
9
+ require "eussiror/issue_formatting"
7
10
  require "eussiror/error_reporter"
8
- require "eussiror/middleware"
11
+ require "eussiror/error_subscriber"
9
12
  require "eussiror/railtie" if defined?(Rails::Railtie)
10
13
 
11
14
  module Eussiror
@@ -12,6 +12,25 @@ module Eussiror
12
12
  def create_initializer_file
13
13
  template "initializer.rb.tt", "config/initializers/eussiror.rb"
14
14
  end
15
+
16
+ def show_post_install_notice
17
+ say <<~NOTICE, :green
18
+
19
+ ================================================================================
20
+ Eussiror has been installed.
21
+ ================================================================================
22
+
23
+ Next steps:
24
+ 1. Set GITHUB_TOKEN in your environment (never commit it to git).
25
+ 2. Set config.github_repository to your target repo (owner/repo).
26
+ 3. Adjust config.environments if needed (default: production only).
27
+ 4. Review config.issue_privacy — :minimal is safest for public GitHub repos;
28
+ use :standard or :full only when issues are private to your team and you
29
+ accept request / user context in issue bodies.
30
+
31
+ See README.md for GitHub token setup, issue_privacy, and Rack env keys (eussiror.user_id).
32
+ NOTICE
33
+ end
15
34
  end
16
35
  end
17
36
  end
@@ -6,10 +6,19 @@ Eussiror.configure do |config|
6
6
  # Required: target GitHub repository in "owner/repository" format.
7
7
  config.github_repository = "your-org/your-repo"
8
8
 
9
- # Environments where 500 errors will be reported to GitHub.
9
+ # Environments where errors will be reported to GitHub.
10
10
  # Default: ["production"]
11
11
  config.environments = %w[production]
12
12
 
13
+ # How much request/user context to include in issue bodies and occurrence comments.
14
+ # :minimal (default) — method + path only; safest for public repos.
15
+ # :standard — also Remote IP and User-Agent when present.
16
+ # :full — standard + User section from env["eussiror.user_id"] / ["eussiror.user_label"].
17
+ # config.issue_privacy = :minimal
18
+
19
+ # Also report errors caught by Rails.error.handle (default: false — only unhandled).
20
+ # config.report_handled_errors = false
21
+
13
22
  # Labels applied to every new issue created by Eussiror (optional).
14
23
  # config.labels = %w[bug automated]
15
24
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eussiror
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Equipe Technique
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-26 00:00:00.000000000 Z
11
+ date: 2026-04-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -25,10 +25,11 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '7.2'
27
27
  description: |
28
- Eussiror hooks into your Rails app and automatically creates GitHub issues
29
- when unhandled exceptions produce 500 responses in configured environments.
28
+ Eussiror subscribes to Rails.error and automatically creates GitHub issues
29
+ when unhandled exceptions occur whether in HTTP requests, ActiveJob,
30
+ Action Cable, or any other Rails execution context.
30
31
  If an issue already exists for the same error (identified by fingerprint),
31
- it adds a comment with the new occurrence timestamp instead.
32
+ it adds a comment with the new occurrence instead.
32
33
  email: []
33
34
  executables: []
34
35
  extensions: []
@@ -39,11 +40,14 @@ files:
39
40
  - README.md
40
41
  - lib/eussiror.rb
41
42
  - lib/eussiror/configuration.rb
43
+ - lib/eussiror/context_extractor.rb
42
44
  - lib/eussiror/error_reporter.rb
45
+ - lib/eussiror/error_subscriber.rb
43
46
  - lib/eussiror/fingerprint.rb
44
47
  - lib/eussiror/github_client.rb
45
- - lib/eussiror/middleware.rb
48
+ - lib/eussiror/issue_formatting.rb
46
49
  - lib/eussiror/railtie.rb
50
+ - lib/eussiror/release_env.rb
47
51
  - lib/eussiror/version.rb
48
52
  - lib/generators/eussiror/install/install_generator.rb
49
53
  - lib/generators/eussiror/install/templates/initializer.rb.tt
@@ -73,5 +77,5 @@ requirements: []
73
77
  rubygems_version: 3.5.22
74
78
  signing_key:
75
79
  specification_version: 4
76
- summary: Automatically create GitHub issues from Rails 500 errors
80
+ summary: Automatically create GitHub issues from Rails exceptions
77
81
  test_files: []
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Eussiror
4
- class Middleware
5
- def initialize(app)
6
- @app = app
7
- end
8
-
9
- def call(env)
10
- status, headers, body = @app.call(env)
11
-
12
- if status == 500
13
- exception = env["action_dispatch.exception"]
14
- ErrorReporter.report(exception, env) if exception
15
- end
16
-
17
- [status, headers, body]
18
- rescue Exception => e # rubocop:disable Lint/RescueException
19
- # The Rails stack re-raises after ShowExceptions in non-standard setups.
20
- # We still want to capture the exception before propagating it.
21
- ErrorReporter.report(e, env)
22
- raise
23
- end
24
- end
25
- end