diff --git a/.gitignore b/.gitignore index c6a148946..d1539ac24 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Gemfile.lock tags /.bundle/ coverage/ +.idea/ diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c4cc7c783..3a66b4451 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -18,7 +18,7 @@ Metrics/BlockLength: # Offense count: 8 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 626 + Max: 659 # Offense count: 11 Metrics/CyclomaticComplexity: diff --git a/lib/stripe.rb b/lib/stripe.rb index eedb00c4d..d991acbfa 100644 --- a/lib/stripe.rb +++ b/lib/stripe.rb @@ -124,6 +124,8 @@ module Stripe @open_timeout = 30 @read_timeout = 80 + @enable_telemetry = false + class << self attr_accessor :stripe_account, :api_key, :api_base, :verify_ssl_certs, :api_version, :client_id, :connect_base, :uploads_base, :open_timeout, :read_timeout @@ -225,6 +227,14 @@ def self.max_network_retries=(val) @max_network_retries = val.to_i end + def self.enable_telemetry? + @enable_telemetry + end + + def self.enable_telemetry=(val) + @enable_telemetry = val + end + # Sets some basic information about the running application that's sent along # with API requests. Useful for plugin authors to identify their plugin when # communicating with Stripe. diff --git a/lib/stripe/stripe_client.rb b/lib/stripe/stripe_client.rb index e219a5fbb..1c0d63bba 100644 --- a/lib/stripe/stripe_client.rb +++ b/lib/stripe/stripe_client.rb @@ -12,6 +12,7 @@ class StripeClient def initialize(conn = nil) self.conn = conn || self.class.default_conn @system_profiler = SystemProfiler.new + @last_request_metrics = nil end def self.active_client @@ -113,7 +114,6 @@ def request def execute_request(method, path, api_base: nil, api_key: nil, headers: {}, params: {}) - api_base ||= Stripe.api_base api_key ||= Stripe.api_key @@ -218,6 +218,9 @@ def execute_request_with_rescues(api_base, context) resp = yield context = context.dup_from_response(resp) log_response(context, request_start, resp.status, resp.body) + if Stripe.enable_telemetry? + @last_request_metrics = StripeRequestMetrics.new(context.request_id, Time.now - request_start) + end # We rescue all exceptions from a request so that we have an easy spot to # implement our retry logic across the board. We'll re-raise if it's a type @@ -425,6 +428,10 @@ def request_headers(api_key, method) "Content-Type" => "application/x-www-form-urlencoded", } + if Stripe.enable_telemetry? && !@last_request_metrics.nil? + headers["X-Stripe-Client-Telemetry"] = JSON.generate(last_request_metrics: @last_request_metrics.payload) + end + # It is only safe to retry network failures on post and delete # requests if we add an Idempotency-Key header if %i[post delete].include?(method) && Stripe.max_network_retries > 0 @@ -594,5 +601,23 @@ def user_agent }.delete_if { |_k, v| v.nil? } end end + + # StripeRequestMetrics tracks metadata to be reported to stripe for metrics collection + class StripeRequestMetrics + # The Stripe request ID of the response. + attr_accessor :request_id + + # Request duration + attr_accessor :request_duration + + def initialize(request_id, request_duration) + self.request_id = request_id + self.request_duration = request_duration + end + + def payload + { request_id: request_id, request_duration: request_duration } + end + end end end diff --git a/test/stripe/stripe_client_test.rb b/test/stripe/stripe_client_test.rb index e32929117..3b7c171e5 100644 --- a/test/stripe/stripe_client_test.rb +++ b/test/stripe/stripe_client_test.rb @@ -749,6 +749,51 @@ class StripeClientTest < Test::Unit::TestCase end end end + + context "#telemetry" do + teardown do + # make sure to always set telemetry back to false + # to not mutate global state + Stripe.enable_telemetry = false + end + + should "not send metrics if enable trace flag is not set" do + Stripe.enable_telemetry = false + + trace_metrics_header = nil + stub_request(:get, "#{Stripe.api_base}/v1/charges") + .with do |req| + trace_metrics_header = req.headers["X-Stripe-Client-Telemetry"] + false + end.to_return(body: JSON.generate(object: "charge")) + + Stripe::Charge.list + assert(trace_metrics_header.nil?) + + Stripe::Charge.list + assert(trace_metrics_header.nil?) + end + + should "send metrics if enabled telemetry is true" do + Stripe.enable_telemetry = true + + trace_metrics_header = nil + stub_request(:get, "#{Stripe.api_base}/v1/charges") + .with do |req| + trace_metrics_header = req.headers["X-Stripe-Client-Telemetry"] + false + end.to_return(body: JSON.generate(object: "charge")) + + Stripe::Charge.list + Stripe::Charge.list + + assert(!trace_metrics_header.nil?) + + trace_payload = JSON.parse(trace_metrics_header) + assert(trace_payload["last_request_metrics"]["request_id"] == "req_123") + assert(!trace_payload["last_request_metrics"]["request_duration"].nil?) + end + end end class SystemProfilerTest < Test::Unit::TestCase