diff --git a/lib/dry/monitor/notifications.rb b/lib/dry/monitor/notifications.rb index 658298d..64ec356 100644 --- a/lib/dry/monitor/notifications.rb +++ b/lib/dry/monitor/notifications.rb @@ -41,7 +41,12 @@ def stop(event_id, payload) def instrument(event_id, payload = EMPTY_HASH) result, time = @clock.measure { yield payload } if block_given? - + # We should always try to instrument, even the system-level exceptions + rescue Exception => e # rubocop:disable Lint/RescueException + payload = {} if payload.equal?(EMPTY_HASH) + payload[:exception] = e + raise + ensure process(event_id, payload) do |event, listener| if time listener.(event.payload(payload.merge(time: time))) diff --git a/spec/integration/instrumentation_spec.rb b/spec/integration/instrumentation_spec.rb index d13ab40..d097465 100644 --- a/spec/integration/instrumentation_spec.rb +++ b/spec/integration/instrumentation_spec.rb @@ -63,5 +63,28 @@ outside_block: true, inside_block: true ) end + + MyError = Class.new(StandardError) + it 'still notifies when the instrumented block raises an exception' do + captured = [] + + notifications.subscribe(:sql) do |event| + captured << event + end + + expect { + notifications.instrument(:sql) do + @line = __LINE__ + 1 + raise MyError + end + }.to raise_error(MyError) + + expect(captured).to_not be_empty + exception = captured.first.payload[:exception] + expect(exception).to match kind_of(MyError) + # verify the exception backtrace comes from the instrumented code, not + # the `#instrument` method + expect(exception.backtrace.first).to start_with("#{__FILE__}:#{@line}") + end end end