exception_handling 3.1.2 → 3.2.0.pre.dc.1

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: 2233f129161cf047d7859362e4801aab61ee8f5c6a91224733b92c5c0222120c
4
- data.tar.gz: 16c764f0a44dac9de1774cdc5716a81e80488664f7b5e2d658dd2435f8dcfc8b
3
+ metadata.gz: f292c98e64a5e880cc6a09979d1b9dbebd1fbe2a074f9d98a3a84391d6829107
4
+ data.tar.gz: b69dcabd0bada51da5ff4010c29a6aeb42e7a44f79841c5f7e14a55990a488fe
5
5
  SHA512:
6
- metadata.gz: acd751fa75679ea47611198b450caa91a2d46e9b36766f070d8344a499370004da310b54df8b841220fe74a35b36498317cdf7a333d84c7c7abcc374ef95ef17
7
- data.tar.gz: 25af611bb83d53bd23e038f38b5a5bf33f99b166d14e4dfd080a4824429b1d334a57560c8e739a6e6b5fdc4aa9776f6c18a0110b1452f08345ea2d3647d8104a
6
+ metadata.gz: 9bc146dff9c619c77c3f9b17bac3b83bbb48787df2a07aa368a8fdee2ebd2d35260b903424abef82d91a3f5a75d44561cbb0812159ab27d9e4b5c7ff7dd5a493
7
+ data.tar.gz: e2ff09d2805cafc8d18993051c4ac568168a73a8de9eab1074de69de819b2e7025e7bd4eb6339f2a06b8ccfa4ab86c2f36f65e89052132f0b416924f8623c274
@@ -1,28 +1,9 @@
1
- ---
2
1
  name: Pipeline
3
- on: [push]
2
+ on:
3
+ push:
4
+ schedule:
5
+ - cron: '0 1 * * 1' # Every Monday at 1AM UTC
4
6
  jobs:
5
7
  tests:
6
- name: Unit Tests
7
- runs-on: ubuntu-latest
8
- strategy:
9
- fail-fast: false
10
- matrix:
11
- ruby: [2.7, '3.0', 3.1, 3.2, 3.3]
12
- gemfile:
13
- - Gemfile
14
- - gemfiles/rails_6_0.gemfile
15
- - gemfiles/rails_6_1.gemfile
16
- - gemfiles/rails_7_0.gemfile
17
- - gemfiles/rails_7_1.gemfile
18
- env:
19
- BUNDLE_GEMFILE: ${{ matrix.gemfile }}
20
- steps:
21
- - uses: actions/checkout@v2
22
- - uses: ruby/setup-ruby@v1
23
- with:
24
- ruby-version: ${{ matrix.ruby }}
25
- bundler: 2.3.26
26
- bundler-cache: true
27
- - name: Unit tests
28
- run: bundle exec rspec
8
+ uses: Invoca/ruby-test-matrix-workflow/.github/workflows/ruby-test-matrix.yml@main
9
+ secrets: inherit
data/.gitignore CHANGED
@@ -4,3 +4,4 @@
4
4
  spec/reports/*
5
5
  gemfiles/*.lock
6
6
  pkg/
7
+ coverage/
data/Appraisals CHANGED
@@ -1,17 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- appraise "rails-6-0" do
4
- gem 'activesupport', '~> 6.0.0'
5
- end
6
-
7
- appraise "rails-6-1" do
8
- gem 'activesupport', '~> 6.1.0'
9
- end
10
-
11
- appraise "rails-7-0" do
12
- gem 'activesupport', '~> 7.0.0'
13
- end
14
-
15
- appraise "rails-7-1" do
16
- gem 'activesupport', '~> 7.1.0'
17
- end
3
+ require "appraisal/matrix"
4
+ appraisal_matrix(rails: "7.0")
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
4
 
5
5
  **Note:** this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [3.2.0] - 2026-08-04
8
+ ### Added
9
+ - Optional Sentry notifications from `send_external_notifications` via explicit `ExceptionHandling.enable_sentry` (requires `Sentry` to be defined and initialized)
10
+ - Sentry events include ExceptionHandling context, tags, controller name, and filter-based fingerprinting (aligned with Honeybadger grouping via `filter_name`)
11
+ - `send_to_sentry` exception filter flag (default `false`); filters with `send_to_honeybadger: true` also send to Sentry to ease migration
12
+
7
13
  ## [3.1.2] - 2024-05-08
8
14
  ### Fixed
9
15
  - Fixed deprecation warnings that were coming from usage of `silence` when using this gem with Rails 7.1
data/Gemfile CHANGED
@@ -5,7 +5,8 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  gem 'activesupport'
8
- gem 'appraisal', '~> 2.2'
8
+ gem 'appraisal'
9
+ gem 'appraisal-matrix'
9
10
  gem 'gem-release'
10
11
  gem 'honeybadger', '~> 4.11'
11
12
  gem 'pry'
@@ -14,4 +15,13 @@ gem 'rake'
14
15
  gem 'rspec'
15
16
  gem 'rspec_junit_formatter'
16
17
  gem 'rubocop'
18
+ gem 'simplecov', '~> 0.22'
19
+ gem 'simplecov-lcov', '~> 0.8'
17
20
  gem 'test-unit'
21
+
22
+
23
+ gem "concurrent-ruby", "~> 1.3", "< 1.3.5"
24
+
25
+ gem "base64", ">= 0.2.0"
26
+ gem "bigdecimal", ">= 3.1"
27
+ gem "mutex_m", ">= 0.2.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- exception_handling (3.1.2)
4
+ exception_handling (3.2.0.pre.dc.1)
5
5
  activesupport (>= 5.2)
6
6
  contextual_logger (~> 1.0)
7
7
  escalate (~> 0.3)
@@ -25,25 +25,29 @@ GEM
25
25
  bundler
26
26
  rake
27
27
  thor (>= 0.14.0)
28
+ appraisal-matrix (0.3.0)
29
+ appraisal (~> 2.2)
28
30
  ast (2.4.2)
29
31
  base64 (0.2.0)
30
32
  bigdecimal (3.1.8)
31
33
  byebug (11.1.3)
32
34
  coderay (1.1.3)
33
- concurrent-ruby (1.2.3)
35
+ concurrent-ruby (1.3.4)
34
36
  connection_pool (2.4.1)
35
- contextual_logger (1.2.0)
36
- activesupport
37
+ contextual_logger (1.5.0)
38
+ activesupport (>= 6.0)
37
39
  json
40
+ date (3.5.1)
38
41
  diff-lcs (1.5.1)
42
+ docile (1.4.1)
39
43
  drb (2.2.1)
40
44
  escalate (0.3.0)
41
45
  gem-release (2.2.2)
42
46
  honeybadger (4.12.2)
43
47
  i18n (1.14.5)
44
48
  concurrent-ruby (~> 1.0)
45
- invoca-utils (0.5.1)
46
- activesupport (>= 5.0)
49
+ invoca-utils (0.7.0)
50
+ activesupport (>= 6.0)
47
51
  json (2.7.2)
48
52
  language_server-protocol (3.17.0.3)
49
53
  method_source (1.1.0)
@@ -60,7 +64,8 @@ GEM
60
64
  pry-byebug (3.10.1)
61
65
  byebug (~> 11.0)
62
66
  pry (>= 0.13, < 0.15)
63
- psych (5.1.2)
67
+ psych (5.4.0)
68
+ date
64
69
  stringio
65
70
  racc (1.7.3)
66
71
  rainbow (3.1.1)
@@ -96,7 +101,14 @@ GEM
96
101
  rubocop-ast (1.31.3)
97
102
  parser (>= 3.3.1.0)
98
103
  ruby-progressbar (1.13.0)
99
- stringio (3.1.0)
104
+ simplecov (0.22.0)
105
+ docile (~> 1.1)
106
+ simplecov-html (~> 0.11)
107
+ simplecov_json_formatter (~> 0.1)
108
+ simplecov-html (0.13.2)
109
+ simplecov-lcov (0.9.0)
110
+ simplecov_json_formatter (0.1.4)
111
+ stringio (3.2.0)
100
112
  test-unit (3.6.2)
101
113
  power_assert
102
114
  thor (1.3.1)
@@ -109,16 +121,23 @@ PLATFORMS
109
121
 
110
122
  DEPENDENCIES
111
123
  activesupport
112
- appraisal (~> 2.2)
124
+ appraisal
125
+ appraisal-matrix
126
+ base64 (>= 0.2.0)
127
+ bigdecimal (>= 3.1)
128
+ concurrent-ruby (~> 1.3, < 1.3.5)
113
129
  exception_handling!
114
130
  gem-release
115
131
  honeybadger (~> 4.11)
132
+ mutex_m (>= 0.2.0)
116
133
  pry
117
134
  pry-byebug
118
135
  rake
119
136
  rspec
120
137
  rspec_junit_formatter
121
138
  rubocop
139
+ simplecov (~> 0.22)
140
+ simplecov-lcov (~> 0.8)
122
141
  test-unit
123
142
 
124
143
  BUNDLED WITH
data/README.md CHANGED
@@ -39,6 +39,32 @@ ExceptionHandling.honeybadger_auto_tagger = ->(exception) { [] } # See "Automati
39
39
  ExceptionHandling.add_honeybadger_tag_from_log_context("tag-name", path: ["path", "in", "log", "context"])
40
40
  ```
41
41
 
42
+ ### External error trackers (Honeybadger / Sentry)
43
+
44
+ ExceptionHandling notifies configured external services from `log_error` (and related paths):
45
+
46
+ - **Honeybadger** — when the `Honeybadger` constant is defined
47
+ - **Sentry** — only after you call `ExceptionHandling.enable_sentry` (requires `Sentry` to be defined and initialized)
48
+
49
+ Both can run at the same time. This gem does not depend on `sentry-ruby`; initialize Sentry in the host application, then opt in:
50
+
51
+ ```ruby
52
+ Sentry.init do |config|
53
+ config.dsn = ENV["SENTRY_DSN"]
54
+ end
55
+
56
+ ExceptionHandling.enable_sentry
57
+ ```
58
+
59
+ When enabled, ExceptionHandling sends the same contextual sections it builds for Honeybadger (request/session/environment/log context, etc.), maps tags into Sentry tag key/values, sets controller context when present, and fingerprints matched filters by `filter_name` so grouping stays aligned across services.
60
+
61
+ Matched exception filters in `exception_filters.yml` control delivery:
62
+
63
+ - `send_to_honeybadger: true` — send to Honeybadger (and also to Sentry during migration, when Sentry is enabled)
64
+ - `send_to_sentry: true` — send to Sentry (when Sentry is enabled)
65
+ - Unmatched exceptions are sent when the corresponding service is active
66
+ - Matched filters with both flags false skip both services
67
+
42
68
  ## Usage
43
69
 
44
70
  Mixin the `ExceptionHandling::Methods` module into your controllers, models or classes. The example below adds it to all the controllers in you rails application:
data/catalog-info.yaml ADDED
@@ -0,0 +1,39 @@
1
+ # This file is partially auto-generated by the invoca-backstage-tools gem
2
+ # The following fields should not be edited manually as they are auto-generated
3
+ # based on the contents of the repo:
4
+ # - metadata.name
5
+ # - metadata.title
6
+ # - metadata.description
7
+ # - annotations.github.com/project-slug
8
+ # - invoca.com/version-repository-location
9
+ # - invoca.com/version-repository-name
10
+ # - spec.type
11
+ # - spec.owner
12
+ ---
13
+ apiVersion: backstage.io/v1alpha1
14
+ kind: Component
15
+ metadata:
16
+ name: exception_handling-gem
17
+ title: ExceptionHandling
18
+ annotations:
19
+ buildkite.com/project-slug: Invoca/exception_handling
20
+ github.com/project-slug: Invoca/exception_handling
21
+ invoca.com/version-repository-location: rubygems
22
+ invoca.com/version-repository-name: exception_handling
23
+ endoflife.date/products: ruby@3.3
24
+ tags:
25
+ - ruby
26
+ - open-source
27
+ - gem
28
+ - public
29
+ - rails-dependent
30
+ description: Exception handling logger
31
+ spec:
32
+ type: library
33
+ lifecycle: production
34
+ owner: octothorpe
35
+ system: platform-observability
36
+ dependsOn:
37
+ - component:escalate-gem
38
+ - component:invoca-utils-gem
39
+ - component:contextual_logger-gem
@@ -2,8 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 7.0.0"
6
- gem "appraisal", "~> 2.2"
5
+ gem "activesupport"
6
+ gem "appraisal"
7
+ gem "appraisal-matrix"
7
8
  gem "gem-release"
8
9
  gem "honeybadger", "~> 4.11"
9
10
  gem "pry"
@@ -13,5 +14,12 @@ gem "rspec"
13
14
  gem "rspec_junit_formatter"
14
15
  gem "rubocop"
15
16
  gem "test-unit"
17
+ gem "concurrent-ruby", "~> 1.3", "< 1.3.5"
18
+ gem "base64", ">= 0.2.0"
19
+ gem "bigdecimal", ">= 3.1"
20
+ gem "mutex_m", ">= 0.2.0"
21
+ gem "rails", "~> 7.0.0"
22
+ gem "simplecov", "~> 0.22"
23
+ gem "simplecov-lcov", "~> 0.8"
16
24
 
17
25
  gemspec path: "../"
@@ -2,8 +2,9 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 7.1.0"
6
- gem "appraisal", "~> 2.2"
5
+ gem "activesupport"
6
+ gem "appraisal"
7
+ gem "appraisal-matrix"
7
8
  gem "gem-release"
8
9
  gem "honeybadger", "~> 4.11"
9
10
  gem "pry"
@@ -13,5 +14,12 @@ gem "rspec"
13
14
  gem "rspec_junit_formatter"
14
15
  gem "rubocop"
15
16
  gem "test-unit"
17
+ gem "concurrent-ruby", "~> 1.3", "< 1.3.5"
18
+ gem "base64", ">= 0.2.0"
19
+ gem "bigdecimal", ">= 3.1"
20
+ gem "mutex_m", ">= 0.2.0"
21
+ gem "rails", "~> 7.1.0"
22
+ gem "simplecov", "~> 0.22"
23
+ gem "simplecov-lcov", "~> 0.8"
16
24
 
17
25
  gemspec path: "../"
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activesupport"
6
+ gem "appraisal"
7
+ gem "appraisal-matrix"
8
+ gem "gem-release"
9
+ gem "honeybadger", "~> 4.11"
10
+ gem "pry"
11
+ gem "pry-byebug"
12
+ gem "rake"
13
+ gem "rspec"
14
+ gem "rspec_junit_formatter"
15
+ gem "rubocop"
16
+ gem "test-unit"
17
+ gem "concurrent-ruby", "~> 1.3", "< 1.3.5"
18
+ gem "base64", ">= 0.2.0"
19
+ gem "bigdecimal", ">= 3.1"
20
+ gem "mutex_m", ">= 0.2.0"
21
+ gem "rails", "~> 7.2.0"
22
+ gem "simplecov", "~> 0.22"
23
+ gem "simplecov-lcov", "~> 0.8"
24
+
25
+ gemspec path: "../"
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activesupport"
6
+ gem "appraisal"
7
+ gem "appraisal-matrix"
8
+ gem "gem-release"
9
+ gem "honeybadger", "~> 4.11"
10
+ gem "pry"
11
+ gem "pry-byebug"
12
+ gem "rake"
13
+ gem "rspec"
14
+ gem "rspec_junit_formatter"
15
+ gem "rubocop"
16
+ gem "test-unit"
17
+ gem "concurrent-ruby", "~> 1.3", "< 1.3.5"
18
+ gem "base64", ">= 0.2.0"
19
+ gem "bigdecimal", ">= 3.1"
20
+ gem "mutex_m", ">= 0.2.0"
21
+ gem "rails", "~> 8.0.0"
22
+ gem "simplecov", "~> 0.22"
23
+ gem "simplecov-lcov", "~> 0.8"
24
+
25
+ gemspec path: "../"
@@ -6,12 +6,13 @@ module ExceptionHandling
6
6
 
7
7
  CONFIGURATION_SECTIONS = {
8
8
  send_to_honeybadger: false, # should be sent to honeybadger?
9
+ send_to_sentry: false, # should be sent to sentry?
9
10
  send_metric: true, # should the metric be sent.
10
11
  metric_name: nil, # Will be derived from section name if not passed
11
12
  notes: nil # Will be included in exception email if set, used to keep notes and relevant links
12
13
  }.freeze
13
14
 
14
- attr_reader :filter_name, :send_to_honeybadger, :send_metric, :metric_name, :notes
15
+ attr_reader :filter_name, :send_to_honeybadger, :send_to_sentry, :send_metric, :metric_name, :notes
15
16
 
16
17
  def initialize(filter_name, configuration)
17
18
  @filter_name = filter_name
@@ -21,6 +22,7 @@ module ExceptionHandling
21
22
 
22
23
  @configuration = CONFIGURATION_SECTIONS.merge(configuration)
23
24
  @send_to_honeybadger = @configuration[:send_to_honeybadger]
25
+ @send_to_sentry = @configuration[:send_to_sentry]
24
26
  @send_metric = @configuration[:send_metric]
25
27
  @metric_name = (@configuration[:metric_name] || @filter_name).to_s.gsub(" ", "_")
26
28
  @notes = @configuration[:notes]
@@ -78,6 +78,14 @@ module ExceptionHandling
78
78
  ExceptionHandling.honeybadger_defined? && (!exception_description || exception_description.send_to_honeybadger)
79
79
  end
80
80
 
81
+ def send_to_sentry?
82
+ ExceptionHandling.sentry_enabled? && (
83
+ !exception_description ||
84
+ exception_description.send_to_sentry ||
85
+ exception_description.send_to_honeybadger
86
+ )
87
+ end
88
+
81
89
  def honeybadger_context_data
82
90
  @honeybadger_context_data ||= enhanced_data_to_honeybadger_context
83
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExceptionHandling
4
- VERSION = '3.1.2'
4
+ VERSION = '3.2.0.pre.dc.1'
5
5
  end
@@ -211,6 +211,9 @@ module ExceptionHandling # never included
211
211
  if honeybadger_defined?
212
212
  results[:honeybadger_status] = send_exception_to_honeybadger_unless_filtered(exception_info)
213
213
  end
214
+ if sentry_enabled?
215
+ results[:sentry_status] = send_exception_to_sentry_unless_filtered(exception_info)
216
+ end
214
217
  results
215
218
  end
216
219
 
@@ -249,6 +252,39 @@ module ExceptionHandling # never included
249
252
  :failure
250
253
  end
251
254
 
255
+ # Returns :success or :failure or :skipped
256
+ def send_exception_to_sentry_unless_filtered(exception_info)
257
+ if exception_info.send_to_sentry?
258
+ send_exception_to_sentry(exception_info)
259
+ else
260
+ log_info("Filtered exception using '#{exception_info.exception_description.filter_name}'; not sending notification to Sentry")
261
+ :skipped
262
+ end
263
+ end
264
+
265
+ #
266
+ # Log exception to Sentry.
267
+ #
268
+ # Returns :success or :failure
269
+ #
270
+ def send_exception_to_sentry(exception_info)
271
+ exception = exception_info.exception
272
+ exception_description = exception_info.exception_description
273
+
274
+ response = Sentry.capture_exception(exception) do |scope|
275
+ tags = tags_hash_for_sentry(exception_info)
276
+ scope.set_tags(tags) if tags.any?
277
+ scope.set_context("exception_handling", exception_info.honeybadger_context_data)
278
+ scope.set_context("controller", { name: exception_info.controller_name }) if exception_info.controller_name.present?
279
+ scope.set_fingerprint([exception_description.filter_name.to_s]) if exception_description
280
+ end
281
+ response ? :success : :failure
282
+ rescue Exception => ex
283
+ warn("ExceptionHandling.send_exception_to_sentry rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.message}:\n#{ex.class}: #{ex.message}\n#{ex.backtrace.join("\n")}")
284
+ write_exception_to_log(ex, "ExceptionHandling.send_exception_to_sentry rescued exception while logging #{exception_info.exception_context}:\n#{exception.class}: #{exception.message}", exception_info.timestamp)
285
+ :failure
286
+ end
287
+
252
288
  #
253
289
  # Check if Honeybadger defined.
254
290
  #
@@ -256,6 +292,22 @@ module ExceptionHandling # never included
256
292
  Object.const_defined?("Honeybadger")
257
293
  end
258
294
 
295
+ #
296
+ # Whether ExceptionHandling should notify Sentry. Off by default; call enable_sentry after Sentry.init.
297
+ #
298
+ def sentry_enabled?
299
+ !!@sentry_enabled
300
+ end
301
+
302
+ #
303
+ # Opt in to Sentry notifications. Requires the Sentry constant and a prior Sentry.init.
304
+ #
305
+ def enable_sentry
306
+ Object.const_defined?("Sentry") or raise ArgumentError, "Sentry is not defined"
307
+ Sentry.initialized? or raise ArgumentError, "Sentry is not initialized"
308
+ @sentry_enabled = true
309
+ end
310
+
259
311
  #
260
312
  # Expects passed in hash to only include keys which be directly set on the Honeybadger config
261
313
  #
@@ -371,6 +423,24 @@ module ExceptionHandling # never included
371
423
  ).uniq
372
424
  end
373
425
 
426
+ # Convert Honeybadger-style tag strings into a Sentry tags hash.
427
+ # Tags containing ":" are split into key/value; others become key => true.
428
+ #
429
+ # @param exception_info [ExceptionInfo]
430
+ # @return [Hash{String => String, TrueClass}]
431
+ def tags_hash_for_sentry(exception_info)
432
+ tags_for_honeybadger(exception_info).each_with_object({}) do |tag, hash|
433
+ tag = tag.to_s.strip
434
+ next if tag.empty?
435
+
436
+ if (separator_index = tag.index(":"))
437
+ hash[tag[0...separator_index]] = tag[(separator_index + 1)..]
438
+ else
439
+ hash[tag] = true
440
+ end
441
+ end
442
+ end
443
+
374
444
  # @param exception [Exception]
375
445
  #
376
446
  # @return [Array<String>]
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless ENV["NO_COVERAGE"] == "true"
4
+ require "simplecov"
5
+
6
+ SimpleCov.start do
7
+ add_filter "/spec/"
8
+ track_files "lib/**/*.rb"
9
+
10
+ if ENV["GITHUB_ACTIONS"]
11
+ require "simplecov-lcov"
12
+
13
+ SimpleCov::Formatter::LcovFormatter.config do |config|
14
+ config.report_with_single_file = true
15
+ config.single_report_path = "coverage/lcov.info"
16
+ end
17
+
18
+ formatter SimpleCov::Formatter::LcovFormatter
19
+ else
20
+ formatter SimpleCov::Formatter::HTMLFormatter
21
+ end
22
+ end
23
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "simplecov_helper"
4
+
3
5
  require 'rspec'
4
6
  require 'rspec/mocks'
5
7
  require 'rspec_junit_formatter'
@@ -35,6 +35,12 @@ module ExceptionHandling
35
35
  expect(!ExceptionDescription.new(:filter1, error: "my error message").send_to_honeybadger).to be_truthy
36
36
  end
37
37
 
38
+ it "allow send_to_sentry to be specified and have it disabled by default" do
39
+ expect(ExceptionDescription.new(:filter1, error: "my error message", send_to_sentry: false).send_to_sentry).to eq(false)
40
+ expect(ExceptionDescription.new(:filter1, error: "my error message", send_to_sentry: true).send_to_sentry).to eq(true)
41
+ expect(ExceptionDescription.new(:filter1, error: "my error message").send_to_sentry).to eq(false)
42
+ end
43
+
38
44
  it "allow send_metric to be configured" do
39
45
  expect(!ExceptionDescription.new(:filter1, error: "my error message", send_metric: false).send_metric).to be_truthy
40
46
  expect(ExceptionDescription.new(:filter1, error: "my error message").send_metric).to be_truthy
@@ -452,6 +452,48 @@ module ExceptionHandling
452
452
  end
453
453
  end
454
454
 
455
+ context "send_to_sentry?" do
456
+ it "be enabled when Sentry is enabled and exception is not in the filter list" do
457
+ allow(ExceptionHandling).to receive(:sentry_enabled?).and_return(true)
458
+ exception_info = ExceptionInfo.new(StandardError.new("something went wrong"), nil, Time.now)
459
+ expect(exception_info.exception_description).to be_nil
460
+ expect(exception_info.send_to_sentry?).to eq(true)
461
+ end
462
+
463
+ it "be enabled when Sentry is enabled and filter has send_to_sentry true" do
464
+ allow(ExceptionHandling).to receive(:sentry_enabled?).and_return(true)
465
+ exception = StandardError.new("No route matches")
466
+ exception_info = ExceptionInfo.new(exception, nil, Time.now)
467
+ allow(exception_info.exception_description).to receive(:send_to_sentry).and_return(true)
468
+ allow(exception_info.exception_description).to receive(:send_to_honeybadger).and_return(false)
469
+ expect(exception_info.send_to_sentry?).to eq(true)
470
+ end
471
+
472
+ it "be enabled when Sentry is enabled and filter has send_to_honeybadger true only" do
473
+ allow(ExceptionHandling).to receive(:sentry_enabled?).and_return(true)
474
+ exception = StandardError.new("No route matches")
475
+ exception_info = ExceptionInfo.new(exception, nil, Time.now)
476
+ allow(exception_info.exception_description).to receive(:send_to_sentry).and_return(false)
477
+ allow(exception_info.exception_description).to receive(:send_to_honeybadger).and_return(true)
478
+ expect(exception_info.send_to_sentry?).to eq(true)
479
+ end
480
+
481
+ it "be disabled when Sentry is enabled and both filter flags are false" do
482
+ allow(ExceptionHandling).to receive(:sentry_enabled?).and_return(true)
483
+ exception = StandardError.new("No route matches")
484
+ exception_info = ExceptionInfo.new(exception, nil, Time.now)
485
+ allow(exception_info.exception_description).to receive(:send_to_sentry).and_return(false)
486
+ allow(exception_info.exception_description).to receive(:send_to_honeybadger).and_return(false)
487
+ expect(exception_info.send_to_sentry?).to eq(false)
488
+ end
489
+
490
+ it "be disabled when Sentry is not enabled" do
491
+ allow(ExceptionHandling).to receive(:sentry_enabled?).and_return(false)
492
+ exception_info = ExceptionInfo.new(StandardError.new("something went wrong"), nil, Time.now)
493
+ expect(exception_info.send_to_sentry?).to eq(false)
494
+ end
495
+ end
496
+
455
497
  context "honeybadger_context_data" do
456
498
  before do
457
499
  allow(ExceptionHandling.logger).to receive(:current_context_for_thread).and_return({ cuid: 'ABCD' })
@@ -111,6 +111,7 @@ describe ExceptionHandling do
111
111
  # Reset this for every test since they are applied to the class
112
112
  ExceptionHandling.honeybadger_auto_tagger = nil
113
113
  ExceptionHandling.clear_honeybadger_tags_from_log_context
114
+ ExceptionHandling.instance_variable_set(:@sentry_enabled, false)
114
115
  end
115
116
 
116
117
  context "with warn and honeybadger notify stubbed" do
@@ -690,6 +691,193 @@ describe ExceptionHandling do
690
691
  end
691
692
  end
692
693
 
694
+ context "Sentry integration" do
695
+ context "enable_sentry" do
696
+ it "raises when Sentry is not defined" do
697
+ hide_const("Sentry") if Object.const_defined?("Sentry")
698
+ expect { ExceptionHandling.enable_sentry }.to raise_error(ArgumentError, "Sentry is not defined")
699
+ expect(ExceptionHandling.sentry_enabled?).to eq(false)
700
+ end
701
+
702
+ it "raises when Sentry is defined but not initialized" do
703
+ stub_const("Sentry", Module.new do
704
+ def self.initialized?; false; end
705
+ end)
706
+ expect { ExceptionHandling.enable_sentry }.to raise_error(ArgumentError, "Sentry is not initialized")
707
+ expect(ExceptionHandling.sentry_enabled?).to eq(false)
708
+ end
709
+
710
+ it "enables Sentry notifications when Sentry is defined and initialized" do
711
+ stub_const("Sentry", Module.new do
712
+ def self.initialized?; true; end
713
+ end)
714
+ ExceptionHandling.enable_sentry
715
+ expect(ExceptionHandling.sentry_enabled?).to eq(true)
716
+ end
717
+ end
718
+
719
+ context "with Sentry not enabled" do
720
+ it "not invoke send_exception_to_sentry when log_error is executed" do
721
+ expect(ExceptionHandling).to_not receive(:send_exception_to_sentry)
722
+ ExceptionHandling.log_error(exception_1)
723
+ end
724
+
725
+ it "not invoke send_exception_to_sentry when ensure_safe is executed" do
726
+ expect(ExceptionHandling).to_not receive(:send_exception_to_sentry)
727
+ ExceptionHandling.ensure_safe { raise exception_1 }
728
+ end
729
+
730
+ it "not send to Sentry even when the Sentry constant is defined" do
731
+ stub_const("Sentry", Module.new do
732
+ def self.initialized?; true; end
733
+ def self.capture_exception(_exception); end
734
+ end)
735
+ expect(Sentry).to_not receive(:capture_exception)
736
+ ExceptionHandling.log_error(exception_1)
737
+ end
738
+ end
739
+
740
+ context "with Sentry enabled" do
741
+ let(:sentry_module) do
742
+ Module.new do
743
+ def self.initialized?; true; end
744
+ def self.capture_exception(_exception); end
745
+ end
746
+ end
747
+
748
+ before do
749
+ stub_const("Sentry", sentry_module)
750
+ allow(Sentry).to receive(:capture_exception).and_return(Object.new)
751
+ ExceptionHandling.enable_sentry
752
+ end
753
+
754
+ it "not send_exception_to_sentry when log_warning is executed" do
755
+ expect(ExceptionHandling).to_not receive(:send_exception_to_sentry)
756
+ ExceptionHandling.log_warning("This should not go to sentry")
757
+ end
758
+
759
+ it "not send_exception_to_sentry when log_error is called with a Warning" do
760
+ expect(ExceptionHandling).to_not receive(:send_exception_to_sentry)
761
+ ExceptionHandling.log_error(ExceptionHandling::Warning.new("This should not go to sentry"))
762
+ end
763
+
764
+ it "invoke send_exception_to_sentry when log_error is executed" do
765
+ expect(ExceptionHandling).to receive(:send_exception_to_sentry).with(any_args).and_call_original
766
+ ExceptionHandling.log_error(exception_1)
767
+ end
768
+
769
+ it "invoke send_exception_to_sentry when log_error_rack is executed" do
770
+ expect(ExceptionHandling).to receive(:send_exception_to_sentry).with(any_args).and_call_original
771
+ ExceptionHandling.log_error_rack(exception_1, {}, nil)
772
+ end
773
+
774
+ it "invoke send_exception_to_sentry when ensure_safe is executed" do
775
+ expect(ExceptionHandling).to receive(:send_exception_to_sentry).with(any_args).and_call_original
776
+ ExceptionHandling.ensure_safe { raise exception_1 }
777
+ end
778
+
779
+ it "call Sentry.capture_exception with the exception" do
780
+ expect(Sentry).to receive(:capture_exception).with(exception_1)
781
+ ExceptionHandling.log_error(exception_1)
782
+ end
783
+
784
+ it "passes context, tags, and filter fingerprint to Sentry via the scope" do
785
+ scope = double("Sentry::Scope")
786
+ expect(scope).to receive(:set_context).with("exception_handling", hash_including(:error_class, :timestamp, :server, :backtrace))
787
+ expect(scope).to receive(:set_tags).with(hash_including("critical" => true, "team" => "platform"))
788
+ expect(scope).to receive(:set_fingerprint).with(["Test Exception"])
789
+ expect(scope).to receive(:set_context).with("controller", { name: "some_controller" })
790
+
791
+ expect(Sentry).to receive(:capture_exception).with(instance_of(StandardError)).and_yield(scope).and_return(Object.new)
792
+
793
+ env = { server: "fe98" }
794
+ parameters = { advertiser_id: 435, controller: "some_controller" }
795
+ session = { username: "jsmith" }
796
+ controller = create_dummy_controller(env, parameters, session, "host/path")
797
+ allow(ExceptionHandling).to receive(:server_name).and_return("invoca_fe98")
798
+
799
+ ExceptionHandling.log_error(
800
+ StandardError.new("Some Exception"),
801
+ { "SERVER_NAME" => "exceptional.com" },
802
+ controller,
803
+ honeybadger_tags: ["critical", "team:platform"]
804
+ )
805
+ end
806
+
807
+ it "also notify Honeybadger when both are available" do
808
+ expect(Honeybadger).to receive(:notify).with(any_args).and_return("hb-id")
809
+ expect(Sentry).to receive(:capture_exception).with(exception_1).and_return(Object.new)
810
+ ExceptionHandling.log_error(exception_1)
811
+ end
812
+
813
+ it "not send to Sentry when filter has both flags false" do
814
+ filter_list = {
815
+ NoSentry: {
816
+ error: "suppress Sentry notification",
817
+ send_to_honeybadger: false,
818
+ send_to_sentry: false
819
+ }
820
+ }
821
+ allow(File).to receive(:mtime) { incrementing_mtime }
822
+ expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
823
+
824
+ expect(ExceptionHandling).to receive(:send_exception_to_sentry_unless_filtered).with(any_args).exactly(1).and_call_original
825
+ expect(Sentry).to_not receive(:capture_exception)
826
+ ExceptionHandling.log_error(StandardError.new("suppress Sentry notification"))
827
+ end
828
+
829
+ it "send to Sentry when filter has send_to_honeybadger true only" do
830
+ filter_list = {
831
+ HoneybadgerOnly: {
832
+ error: "honeybadger only filter",
833
+ send_to_honeybadger: true,
834
+ send_to_sentry: false
835
+ }
836
+ }
837
+ allow(File).to receive(:mtime) { incrementing_mtime }
838
+ expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
839
+
840
+ expect(Sentry).to receive(:capture_exception).with(instance_of(StandardError))
841
+ ExceptionHandling.log_error(StandardError.new("honeybadger only filter"))
842
+ end
843
+
844
+ it "send to Sentry when filter has send_to_sentry true only" do
845
+ filter_list = {
846
+ SentryOnly: {
847
+ error: "sentry only filter",
848
+ send_to_honeybadger: false,
849
+ send_to_sentry: true
850
+ }
851
+ }
852
+ allow(File).to receive(:mtime) { incrementing_mtime }
853
+ expect(YAML).to receive(:load_file).with(any_args) { ActiveSupport::HashWithIndifferentAccess.new(filter_list) }.at_least(1)
854
+
855
+ expect(Sentry).to receive(:capture_exception).with(instance_of(StandardError))
856
+ ExceptionHandling.log_error(StandardError.new("sentry only filter"))
857
+ end
858
+
859
+ it "still complete log_error and notify Honeybadger if Sentry.capture_exception raises" do
860
+ expect(Honeybadger).to receive(:notify).with(any_args).and_return("hb-id")
861
+ expect(Sentry).to receive(:capture_exception).and_raise(StandardError, "Sentry Notification Failure")
862
+ expect { ExceptionHandling.log_error(exception_1) }.not_to raise_error
863
+ end
864
+
865
+ it "return :failure when Sentry.capture_exception returns nil" do
866
+ expect(Sentry).to receive(:capture_exception).and_return(nil)
867
+ expect(ExceptionHandling.send_exception_to_sentry(
868
+ ExceptionHandling::ExceptionInfo.new(exception_1, "", Time.now)
869
+ )).to eq(:failure)
870
+ end
871
+
872
+ it "return :success when Sentry.capture_exception returns an event" do
873
+ expect(Sentry).to receive(:capture_exception).and_return(Object.new)
874
+ expect(ExceptionHandling.send_exception_to_sentry(
875
+ ExceptionHandling::ExceptionInfo.new(exception_1, "", Time.now)
876
+ )).to eq(:success)
877
+ end
878
+ end
879
+ end
880
+
693
881
  class EventResponse
694
882
  def to_s
695
883
  "message from to_s!"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exception_handling
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.2
4
+ version: 3.2.0.pre.dc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Invoca
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-08 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -103,12 +103,13 @@ files:
103
103
  - LICENSE
104
104
  - README.md
105
105
  - Rakefile
106
+ - catalog-info.yaml
106
107
  - config/exception_filters.yml
107
108
  - exception_handling.gemspec
108
- - gemfiles/rails_6_0.gemfile
109
- - gemfiles/rails_6_1.gemfile
110
109
  - gemfiles/rails_7_0.gemfile
111
110
  - gemfiles/rails_7_1.gemfile
111
+ - gemfiles/rails_7_2.gemfile
112
+ - gemfiles/rails_8_0.gemfile
112
113
  - lib/exception_handling.rb
113
114
  - lib/exception_handling/escalate_callback.rb
114
115
  - lib/exception_handling/exception_catalog.rb
@@ -121,6 +122,7 @@ files:
121
122
  - spec/helpers/controller_helpers.rb
122
123
  - spec/helpers/exception_helpers.rb
123
124
  - spec/rake_test_warning_false.rb
125
+ - spec/simplecov_helper.rb
124
126
  - spec/spec_helper.rb
125
127
  - spec/unit/exception_handling/escalate_callback_spec.rb
126
128
  - spec/unit/exception_handling/exception_catalog_spec.rb
@@ -149,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  - !ruby/object:Gem::Version
150
152
  version: '0'
151
153
  requirements: []
152
- rubygems_version: 3.4.21
154
+ rubygems_version: 3.5.10
153
155
  signing_key:
154
156
  specification_version: 4
155
157
  summary: Invoca's exception handling logger layer, based on exception_notifier.
@@ -157,6 +159,7 @@ test_files:
157
159
  - spec/helpers/controller_helpers.rb
158
160
  - spec/helpers/exception_helpers.rb
159
161
  - spec/rake_test_warning_false.rb
162
+ - spec/simplecov_helper.rb
160
163
  - spec/spec_helper.rb
161
164
  - spec/unit/exception_handling/escalate_callback_spec.rb
162
165
  - spec/unit/exception_handling/exception_catalog_spec.rb
@@ -1,17 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activesupport", "~> 6.0.0"
6
- gem "appraisal", "~> 2.2"
7
- gem "gem-release"
8
- gem "honeybadger", "~> 4.11"
9
- gem "pry"
10
- gem "pry-byebug"
11
- gem "rake"
12
- gem "rspec"
13
- gem "rspec_junit_formatter"
14
- gem "rubocop"
15
- gem "test-unit"
16
-
17
- gemspec path: "../"
@@ -1,17 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activesupport", "~> 6.1.0"
6
- gem "appraisal", "~> 2.2"
7
- gem "gem-release"
8
- gem "honeybadger", "~> 4.11"
9
- gem "pry"
10
- gem "pry-byebug"
11
- gem "rake"
12
- gem "rspec"
13
- gem "rspec_junit_formatter"
14
- gem "rubocop"
15
- gem "test-unit"
16
-
17
- gemspec path: "../"