diff --git a/sgx-bwmsgsv2.rb b/sgx-bwmsgsv2.rb index f5b243007e49472945637083e9027f456ae51e31..7ae3f8d1024a1024b14874b9abbdf792d8d3338d 100755 --- a/sgx-bwmsgsv2.rb +++ b/sgx-bwmsgsv2.rb @@ -114,27 +114,77 @@ def anonymous_tel?(dest) end class SGXClient < Blather::Client - def register_handler(type, *guards, &block) - super(type, *guards) { |*args| wrap_handler(*args, &block) } + def handle_data(stanza) + promise = EMPromise.resolve(nil).then { + with_sentry(stanza) do |scope| + super + rescue StandardError => e + handle_error(scope, stanza, e) + end + }.catch { |e| panic(e) } + promise.sync if ENV["ENV"] == "test" + promise end - def register_handler_before(type, *guards, &block) - check_handler(type, guards) - handler = lambda { |*args| wrap_handler(*args, &block) } + # Override the default call_handler to syncify during testing. + def call_handler(handler, guards, stanza) + result = if guards.first.respond_to?(:to_str) + found = stanza.find(*guards) + throw :pass if found.empty? + + handler.call(stanza, found) + else + throw :pass if guarded?(guards, stanza) + + handler.call(stanza) + end - @handlers[type] ||= [] - @handlers[type].unshift([guards, handler]) + # Up to here, identical to upstream impl + + return result unless result.is_a?(Promise) + + result.sync if ENV["ENV"] == "test" + result end protected - def wrap_handler(*args) - v = yield(*args) - v = v.sync if ENV['ENV'] == 'test' && v.is_a?(Promise) - v.catch(&method(:panic)) if v.is_a?(Promise) - true # Do not run other handlers unless throw :pass - rescue Exception => e - panic(e) + def with_sentry(stanza) + Sentry.clone_hub_to_current_thread + + Sentry.with_scope do |scope| + setup_scope(stanza, scope) + yield scope + ensure + scope.get_transaction&.then do |tx| + tx.set_status("ok") unless tx.status + tx.finish + end + end + end + + def setup_scope(stanza, scope) + name = stanza.respond_to?(:node) ? stanza.node : stanza.name + scope.clear_breadcrumbs + scope.set_transaction_name(name) + scope.set_user(jid: stanza.from&.stripped.to_s) + + transaction = Sentry.start_transaction( + name: name, + op: "blather.handle_data" + ) + scope.set_span(transaction) if transaction + end + + def handle_error(scope, stanza, e) + puts "Error raised during #{scope.transaction_name}: #{e.class}" + puts e.message + puts e.backtrace + Sentry.capture_exception(e) unless e.is_a?(Sentry::Error) + scope.get_transaction&.set_status("internal_error") + return if e.respond_to?(:replied?) && e.replied? + + SGXbwmsgsv2.write_to_stream stanza.as_error("internal-server-error", :cancel) end end diff --git a/test/test_component.rb b/test/test_component.rb index 9ee3d95cd50b270b6f36f072635bf5012366d7ae..20eee5ec8b8619d89af609324d42af451d7c7ac7 100644 --- a/test/test_component.rb +++ b/test/test_component.rb @@ -513,4 +513,33 @@ class ComponentTest < Minitest::Test end end em :test_port_out_pin_validation + + def test_sentry_captures_handler_exception + captured_exceptions = [] + repo = SGXbwmsgsv2.instance_variable_get(:@registration_repo) + + Sentry.stub :capture_exception, ->(*args, **) { captured_exceptions << args.first } do + # repo.find is called during message processing to look up creds; + # raising here bubbles up to SGXClient#handle_error + repo.stub :find, ->(_) { raise StandardError, "test error" } do + m = Blather::Stanza::Message.new("+15551234567@component", "hello") + m.from = "test@example.com" + process_stanza(m) + end + end + + assert_equal 1, captured_exceptions.length + assert_instance_of StandardError, captured_exceptions.first + assert_equal "test error", captured_exceptions.first.message + + assert_equal 1, written.length + stanza = Blather::XMPPNode.parse(written.first.to_xml) + assert stanza.error? + error = stanza.find_first("error") + assert_equal "cancel", error["type"] + assert_equal "internal-server-error", xmpp_error_name(error) + + SGXbwmsgsv2.instance_variable_set(:@written, []) + end + em :test_sentry_captures_handler_exception end