exception_handling 3.2.0.pre.dc.0 → 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: 5a1419e36b2b184e6e49ad6b943537b1f5efaf54680aef66251093c242e93168
4
- data.tar.gz: 641de9b2e454c20cb754469fce0f781136baefb14af6913ab25fce3f0263bae4
3
+ metadata.gz: f292c98e64a5e880cc6a09979d1b9dbebd1fbe2a074f9d98a3a84391d6829107
4
+ data.tar.gz: b69dcabd0bada51da5ff4010c29a6aeb42e7a44f79841c5f7e14a55990a488fe
5
5
  SHA512:
6
- metadata.gz: d3ba9e37ec6c14e508341b604df6ffaeed7c88106ca19305f69a2990f6eefcf03a8ae41a3fb6f7245165e00cb910940ae99842f6d73d9c641d8a9f9c2293a883
7
- data.tar.gz: a9402fb31a6cce4d2181d9b304376412c7acbf63ffb4e592615da2a2bfd77330c8e6ef1a9bb4c3e1428f6dd9bec32295c5f572860117912cb4f34c6f9e49df27
6
+ metadata.gz: 9bc146dff9c619c77c3f9b17bac3b83bbb48787df2a07aa368a8fdee2ebd2d35260b903424abef82d91a3f5a75d44561cbb0812159ab27d9e4b5c7ff7dd5a493
7
+ data.tar.gz: e2ff09d2805cafc8d18993051c4ac568168a73a8de9eab1074de69de819b2e7025e7bd4eb6339f2a06b8ccfa4ab86c2f36f65e89052132f0b416924f8623c274
data/CHANGELOG.md CHANGED
@@ -6,7 +6,8 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
 
7
7
  ## [3.2.0] - 2026-08-04
8
8
  ### Added
9
- - Optional Sentry notifications from `send_external_notifications` when the `Sentry` constant is defined
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`)
10
11
  - `send_to_sentry` exception filter flag (default `false`); filters with `send_to_honeybadger: true` also send to Sentry to ease migration
11
12
 
12
13
  ## [3.1.2] - 2024-05-08
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- exception_handling (3.2.0.pre.dc.0)
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)
data/README.md CHANGED
@@ -44,15 +44,25 @@ ExceptionHandling.add_honeybadger_tag_from_log_context("tag-name", path: ["path"
44
44
  ExceptionHandling notifies configured external services from `log_error` (and related paths):
45
45
 
46
46
  - **Honeybadger** — when the `Honeybadger` constant is defined
47
- - **Sentry** — when the `Sentry` constant is defined (for example after initializing the Sentry Ruby SDK in your app)
47
+ - **Sentry** — only after you call `ExceptionHandling.enable_sentry` (requires `Sentry` to be defined and initialized)
48
48
 
49
- Both can run at the same time. This gem does not depend on `sentry-ruby`; initialize Sentry in the host application.
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.
50
60
 
51
61
  Matched exception filters in `exception_filters.yml` control delivery:
52
62
 
53
- - `send_to_honeybadger: true` — send to Honeybadger (and also to Sentry during migration)
54
- - `send_to_sentry: true` — send to Sentry
55
- - Unmatched exceptions are sent when the corresponding service is defined
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
56
66
  - Matched filters with both flags false skip both services
57
67
 
58
68
  ## Usage
@@ -79,7 +79,7 @@ module ExceptionHandling
79
79
  end
80
80
 
81
81
  def send_to_sentry?
82
- ExceptionHandling.sentry_defined? && (
82
+ ExceptionHandling.sentry_enabled? && (
83
83
  !exception_description ||
84
84
  exception_description.send_to_sentry ||
85
85
  exception_description.send_to_honeybadger
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExceptionHandling
4
- VERSION = '3.2.0.pre.dc.0'
4
+ VERSION = '3.2.0.pre.dc.1'
5
5
  end
@@ -211,7 +211,7 @@ 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_defined?
214
+ if sentry_enabled?
215
215
  results[:sentry_status] = send_exception_to_sentry_unless_filtered(exception_info)
216
216
  end
217
217
  results
@@ -268,8 +268,16 @@ module ExceptionHandling # never included
268
268
  # Returns :success or :failure
269
269
  #
270
270
  def send_exception_to_sentry(exception_info)
271
- exception = exception_info.exception
272
- response = Sentry.capture_exception(exception)
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
273
281
  response ? :success : :failure
274
282
  rescue Exception => ex
275
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")}")
@@ -285,10 +293,19 @@ module ExceptionHandling # never included
285
293
  end
286
294
 
287
295
  #
288
- # Check if Sentry defined.
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.
289
304
  #
290
- def sentry_defined?
291
- Object.const_defined?("Sentry")
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
292
309
  end
293
310
 
294
311
  #
@@ -406,6 +423,24 @@ module ExceptionHandling # never included
406
423
  ).uniq
407
424
  end
408
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
+
409
444
  # @param exception [Exception]
410
445
  #
411
446
  # @return [Array<String>]
@@ -453,15 +453,15 @@ module ExceptionHandling
453
453
  end
454
454
 
455
455
  context "send_to_sentry?" do
456
- it "be enabled when Sentry is defined and exception is not in the filter list" do
457
- allow(ExceptionHandling).to receive(:sentry_defined?).and_return(true)
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
458
  exception_info = ExceptionInfo.new(StandardError.new("something went wrong"), nil, Time.now)
459
459
  expect(exception_info.exception_description).to be_nil
460
460
  expect(exception_info.send_to_sentry?).to eq(true)
461
461
  end
462
462
 
463
- it "be enabled when Sentry is defined and filter has send_to_sentry true" do
464
- allow(ExceptionHandling).to receive(:sentry_defined?).and_return(true)
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
465
  exception = StandardError.new("No route matches")
466
466
  exception_info = ExceptionInfo.new(exception, nil, Time.now)
467
467
  allow(exception_info.exception_description).to receive(:send_to_sentry).and_return(true)
@@ -469,8 +469,8 @@ module ExceptionHandling
469
469
  expect(exception_info.send_to_sentry?).to eq(true)
470
470
  end
471
471
 
472
- it "be enabled when Sentry is defined and filter has send_to_honeybadger true only" do
473
- allow(ExceptionHandling).to receive(:sentry_defined?).and_return(true)
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
474
  exception = StandardError.new("No route matches")
475
475
  exception_info = ExceptionInfo.new(exception, nil, Time.now)
476
476
  allow(exception_info.exception_description).to receive(:send_to_sentry).and_return(false)
@@ -478,8 +478,8 @@ module ExceptionHandling
478
478
  expect(exception_info.send_to_sentry?).to eq(true)
479
479
  end
480
480
 
481
- it "be disabled when Sentry is defined and both filter flags are false" do
482
- allow(ExceptionHandling).to receive(:sentry_defined?).and_return(true)
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
483
  exception = StandardError.new("No route matches")
484
484
  exception_info = ExceptionInfo.new(exception, nil, Time.now)
485
485
  allow(exception_info.exception_description).to receive(:send_to_sentry).and_return(false)
@@ -487,8 +487,8 @@ module ExceptionHandling
487
487
  expect(exception_info.send_to_sentry?).to eq(false)
488
488
  end
489
489
 
490
- it "be disabled when Sentry is not defined" do
491
- allow(ExceptionHandling).to receive(:sentry_defined?).and_return(false)
490
+ it "be disabled when Sentry is not enabled" do
491
+ allow(ExceptionHandling).to receive(:sentry_enabled?).and_return(false)
492
492
  exception_info = ExceptionInfo.new(StandardError.new("something went wrong"), nil, Time.now)
493
493
  expect(exception_info.send_to_sentry?).to eq(false)
494
494
  end
@@ -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
@@ -691,11 +692,31 @@ describe ExceptionHandling do
691
692
  end
692
693
 
693
694
  context "Sentry integration" do
694
- context "with Sentry not defined" do
695
- before do
696
- allow(ExceptionHandling).to receive(:sentry_defined?).and_return(false)
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)
697
708
  end
698
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
699
720
  it "not invoke send_exception_to_sentry when log_error is executed" do
700
721
  expect(ExceptionHandling).to_not receive(:send_exception_to_sentry)
701
722
  ExceptionHandling.log_error(exception_1)
@@ -705,11 +726,21 @@ describe ExceptionHandling do
705
726
  expect(ExceptionHandling).to_not receive(:send_exception_to_sentry)
706
727
  ExceptionHandling.ensure_safe { raise exception_1 }
707
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
708
738
  end
709
739
 
710
- context "with Sentry defined" do
740
+ context "with Sentry enabled" do
711
741
  let(:sentry_module) do
712
742
  Module.new do
743
+ def self.initialized?; true; end
713
744
  def self.capture_exception(_exception); end
714
745
  end
715
746
  end
@@ -717,6 +748,7 @@ describe ExceptionHandling do
717
748
  before do
718
749
  stub_const("Sentry", sentry_module)
719
750
  allow(Sentry).to receive(:capture_exception).and_return(Object.new)
751
+ ExceptionHandling.enable_sentry
720
752
  end
721
753
 
722
754
  it "not send_exception_to_sentry when log_warning is executed" do
@@ -749,7 +781,30 @@ describe ExceptionHandling do
749
781
  ExceptionHandling.log_error(exception_1)
750
782
  end
751
783
 
752
- it "also notify Honeybadger when both are defined" do
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
753
808
  expect(Honeybadger).to receive(:notify).with(any_args).and_return("hb-id")
754
809
  expect(Sentry).to receive(:capture_exception).with(exception_1).and_return(Object.new)
755
810
  ExceptionHandling.log_error(exception_1)
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.2.0.pre.dc.0
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: 2026-08-04 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