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 +4 -4
- data/CHANGELOG.md +2 -1
- data/Gemfile.lock +1 -1
- data/README.md +15 -5
- data/lib/exception_handling/exception_info.rb +1 -1
- data/lib/exception_handling/version.rb +1 -1
- data/lib/exception_handling.rb +41 -6
- data/spec/unit/exception_handling/exception_info_spec.rb +10 -10
- data/spec/unit/exception_handling_spec.rb +60 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f292c98e64a5e880cc6a09979d1b9dbebd1fbe2a074f9d98a3a84391d6829107
|
|
4
|
+
data.tar.gz: b69dcabd0bada51da5ff4010c29a6aeb42e7a44f79841c5f7e14a55990a488fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
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
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** —
|
|
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
|
|
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
|
data/lib/exception_handling.rb
CHANGED
|
@@ -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
|
|
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
|
|
272
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
457
|
-
allow(ExceptionHandling).to receive(:
|
|
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
|
|
464
|
-
allow(ExceptionHandling).to receive(:
|
|
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
|
|
473
|
-
allow(ExceptionHandling).to receive(:
|
|
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
|
|
482
|
-
allow(ExceptionHandling).to receive(:
|
|
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
|
|
491
|
-
allow(ExceptionHandling).to receive(:
|
|
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 "
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
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 "
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-08-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|