From 7383347147efb348f12b50a8b109585509dfdaab Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Fri, 27 May 2022 13:08:16 +0100 Subject: [PATCH] implementation of the sentry plugin integration --- Gemfile | 1 + integration_tests/sentry_test.rb | 27 ++++++++-- lib/httpx/adapters/sentry.rb | 89 ++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index cb91af18..f87a3bce 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ group :test do gem "minitest" gem "minitest-proveit" gem "ruby-ntlm" + gem "sentry-ruby" if RUBY_VERSION >= "2.4" gem "spy" gem "webmock" gem "websocket-driver" diff --git a/integration_tests/sentry_test.rb b/integration_tests/sentry_test.rb index dcb4888f..3b49e07a 100644 --- a/integration_tests/sentry_test.rb +++ b/integration_tests/sentry_test.rb @@ -4,8 +4,10 @@ require "logger" require "stringio" require "test_helper" require "support/http_helpers" -require "sentry-ruby" -require "httpx/adapters/sentry" +begin + require "httpx/adapters/sentry" +rescue LoadError +end class SentryTest < Minitest::Test include HTTPHelpers @@ -50,6 +52,21 @@ class SentryTest < Minitest::Test end end + def test_sentry_post_request + before_pii = Sentry.configuration.send_default_pii + begin + Sentry.configuration.send_default_pii = true + transaction = Sentry.start_transaction + Sentry.get_current_scope.set_span(transaction) + + response = HTTPX.post(build_uri("/post"), form: { foo: "bar" }) + verify_status(response, 200) + verify_spans(transaction, response, verb: "POST") + ensure + Sentry.configuration.send_default_pii = before_pii + end + end + def test_sentry_multiple_requests transaction = Sentry.start_transaction Sentry.get_current_scope.set_span(transaction) @@ -62,7 +79,7 @@ class SentryTest < Minitest::Test private - def verify_spans(transaction, *responses) + def verify_spans(transaction, *responses, verb: nil, description: nil) assert transaction.span_recorder.spans.count == responses.size + 1 assert transaction.span_recorder.spans[0] == transaction @@ -74,7 +91,7 @@ class SentryTest < Minitest::Test assert !request_span.start_timestamp.nil? assert !request_span.timestamp.nil? assert request_span.start_timestamp != request_span.timestamp - assert request_span.description == response.uri + assert request_span.description == (description || "#{verb || "GET"} #{response.uri}") assert request_span.data == { status: response.status } end end @@ -98,4 +115,4 @@ class SentryTest < Minitest::Test def origin "https://#{httpbin}" end -end +end if RUBY_VERSION >= "2.4.0" diff --git a/lib/httpx/adapters/sentry.rb b/lib/httpx/adapters/sentry.rb index 9b00ec1d..3f73548f 100644 --- a/lib/httpx/adapters/sentry.rb +++ b/lib/httpx/adapters/sentry.rb @@ -1,7 +1,96 @@ # frozen_string_literal: true +require "sentry-ruby" + module HTTPX::Plugins module Sentry + module Tracer + module_function + + def call(request) + sentry_span = start_sentry_span + + return unless sentry_span + + set_sentry_trace_header(request, sentry_span) + + request.on(:response, &method(:finish_sentry_span).curry(3)[sentry_span, request]) + end + + def start_sentry_span + return unless ::Sentry.initialized? && (span = ::Sentry.get_current_scope.get_span) + return if span.sampled == false + + span.start_child(op: "httpx.client", start_timestamp: ::Sentry.utc_now.to_f) + end + + def set_sentry_trace_header(request, sentry_span) + return unless sentry_span + + trace = ::Sentry.get_current_client.generate_sentry_trace(sentry_span) + request.headers[::Sentry::SENTRY_TRACE_HEADER_NAME] = trace if trace + end + + def finish_sentry_span(span, request, response) + return unless ::Sentry.initialized? + + record_sentry_breadcrumb(request, response) + record_sentry_span(request, response, span) + end + + def record_sentry_breadcrumb(req, res) + return unless ::Sentry.configuration.breadcrumbs_logger.include?(:http_logger) + + request_info = extract_request_info(req) + + data = if response.is_a?(HTTPX::ErrorResponse) + { error: res.message, **request_info } + else + { status: res.status, **request_info } + end + + crumb = ::Sentry::Breadcrumb.new( + level: :info, + category: "httpx", + type: :info, + data: data + ) + ::Sentry.add_breadcrumb(crumb) + end + + def record_sentry_span(req, res, sentry_span) + return unless sentry_span + + request_info = extract_request_info(req) + sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}") + sentry_span.set_data(:status, res.status) + sentry_span.set_timestamp(::Sentry.utc_now.to_f) + end + + def extract_request_info(req) + uri = req.uri + + result = { + method: req.verb.to_s.upcase, + } + + if ::Sentry.configuration.send_default_pii + uri += "?#{req.query}" unless req.query.empty? + result[:body] = req.body.to_s unless req.body.empty? || req.body.unbounded_body? + end + + result[:url] = uri.to_s + + result + end + end + + module ConnectionMethods + def send(request) + Tracer.call(request) + super + end + end end end