diff --git a/yes-core/lib/yes/core/failed_subscription_notifier.rb b/yes-core/lib/yes/core/failed_subscription_notifier.rb new file mode 100644 index 0000000..94f95f8 --- /dev/null +++ b/yes-core/lib/yes/core/failed_subscription_notifier.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Yes + module Core + # Reports a permanently dead pg_eventstore subscription to Sentry. + # + # pg_eventstore calls its +failed_subscription_notifier+ exactly once, when a + # subscription exhausts its restarts and stays dead — it is the gem's only + # death signal, and without it that death is silent (B2BY-5189). Per-failure + # errors are only recorded on the subscription row, never raised into Sentry. + class FailedSubscriptionNotifier + # @param subscription [PgEventstore::Subscription] + # @param error [StandardError] + # @return [void] + def call(subscription, error) + Sentry.with_scope do |scope| + scope.set_tags(failed_subscription_notifier: true) # used in Sentry Alerts + + Sentry.capture_exception( + error, + # a death report must never be swallowed by Sentry's excluded_exceptions + hint: { ignore_exclusions: true }, + extra: { id: subscription.id, set: subscription.set, name: subscription.name } + ) + end + end + end + end +end diff --git a/yes-core/lib/yes/core/railtie.rb b/yes-core/lib/yes/core/railtie.rb index f37bf12..3974674 100644 --- a/yes-core/lib/yes/core/railtie.rb +++ b/yes-core/lib/yes/core/railtie.rb @@ -16,6 +16,11 @@ class Railtie < Rails::Railtie with_indifferent_access: Yes::Core::Middlewares::WithIndifferentAccess.new, timestamp: Yes::Core::Middlewares::Timestamp.new } + + # pg_eventstore's only death signal — without it a subscription that + # exhausts its restarts dies silently (B2BY-5189). Present in + # yousty-eventsourcing's Railtie; dropped when it was ported here. + config.failed_subscription_notifier = FailedSubscriptionNotifier.new if defined?(Sentry) end end diff --git a/yes-core/spec/lib/yes/core/failed_subscription_notifier_spec.rb b/yes-core/spec/lib/yes/core/failed_subscription_notifier_spec.rb new file mode 100644 index 0000000..e8cdda6 --- /dev/null +++ b/yes-core/spec/lib/yes/core/failed_subscription_notifier_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.describe Yes::Core::FailedSubscriptionNotifier do + subject(:notify) { described_class.new.call(subscription, error) } + + let(:subscription) { PgEventstore::Subscription.new(id: 42, set: 'Yes', name: 'SomeProcessManager') } + let(:error) { StandardError.new('handler exploded') } + let(:sentry) { class_double('Sentry') } + let(:scope) { double('Sentry::Scope', set_tags: nil) } + + before do + # Sentry is not a dependency of this gem — the railtie only registers the + # notifier when the host app loaded it, so the constant is stubbed here. + stub_const('Sentry', sentry) + allow(sentry).to receive(:with_scope).and_yield(scope) + allow(sentry).to receive(:capture_exception) + end + + it 'reports the death to Sentry with the subscription identity' do + notify + + aggregate_failures do + expect(scope).to have_received(:set_tags).with(failed_subscription_notifier: true) + expect(sentry).to have_received(:capture_exception).with( + error, + hint: { ignore_exclusions: true }, + extra: { id: 42, set: 'Yes', name: 'SomeProcessManager' } + ) + end + end +end