Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/stripe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "net/http"
require "openssl"
require "rbconfig"
require "fileutils"
require "securerandom"
require "set"
require "socket"
Expand Down Expand Up @@ -36,6 +37,7 @@
require "stripe/request_params"
require "stripe/stripe_context"
require "stripe/util"
require "stripe/telemetry_id"
require "stripe/connection_manager"
require "stripe/multipart_encoder"
require "stripe/api_requestor"
Expand Down
29 changes: 2 additions & 27 deletions lib/stripe/api_requestor.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# frozen_string_literal: true

require "digest"
require "socket"
require "stripe/instrumentation"

Expand Down Expand Up @@ -1068,31 +1067,6 @@ def dup_from_response_headers(headers)
# in so that we can generate a rich user agent header to help debug
# integrations.
class SystemProfiler
UNAME_HASH = begin
parts = []
parts << if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
begin
`ver 2>NUL`.strip
rescue StandardError
""
end
else
begin
`uname -a 2>/dev/null`.strip
rescue StandardError
""
end
end
parts << begin
Socket.gethostname
rescue StandardError
""
end
Digest::MD5.hexdigest(parts.join(" "))
rescue StandardError
""
end

AI_AGENTS = [
# aiAgents: The beginning of the section generated from our OpenAPI spec
%w[ANTIGRAVITY_CLI_ALIAS antigravity],
Expand Down Expand Up @@ -1130,7 +1104,8 @@ def self.user_agent

if Stripe.enable_telemetry?
ua[:platform] = RUBY_PLATFORM
ua[:source] = UNAME_HASH unless UNAME_HASH.empty?
tid = TelemetryId.get
ua[:telemetry_id] = tid if tid
end

ai_agent = detect_ai_agent
Expand Down
65 changes: 65 additions & 0 deletions lib/stripe/telemetry_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module Stripe
module TelemetryId
@cached_id = nil
@loaded = false

def self.config_dir
if Gem.win_platform?
appdata = ENV.fetch("APPDATA", nil)
return nil unless appdata && !appdata.empty?

::File.join(appdata, "Stripe")
else
xdg = ENV.fetch("XDG_CONFIG_HOME", nil)
if xdg && !xdg.empty?
::File.join(xdg, "stripe")
else
::File.expand_path("~/.config/stripe")
end
end
rescue ArgumentError
nil
end

def self.get
return @cached_id if @loaded

dir = config_dir
return nil unless dir

file_path = ::File.join(dir, "telemetry_id")

begin
content = ::File.read(file_path).strip
unless content.empty?
@cached_id = content
return @cached_id
end
rescue SystemCallError
# File doesn't exist or can't be read
end

new_id = SecureRandom.hex(16)

begin
::FileUtils.mkdir_p(dir)
::File.write(file_path, new_id)
rescue SystemCallError
return nil
end

@cached_id = new_id
@cached_id
ensure
@loaded = true
end

# For testing only
def self.reset!
@cached_id = nil
@loaded = false
end
end
end
28 changes: 14 additions & 14 deletions test/stripe/api_requestor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1800,29 +1800,29 @@ class SystemProfilerTest < Test::Unit::TestCase
Stripe.enable_telemetry = false
end

should "omit source when UNAME_HASH is empty" do
original = APIRequestor::SystemProfiler::UNAME_HASH
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, "")
should "include telemetry_id when telemetry is enabled" do
Stripe.enable_telemetry = true
TelemetryId.stubs(:get).returns("abc123def456")
ua = APIRequestor::SystemProfiler.user_agent
refute ua.key?(:source)
assert_equal "abc123def456", ua[:telemetry_id]
ensure
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, original)
Stripe.enable_telemetry = false
end

should "include source when UNAME_HASH is non-empty" do
original = APIRequestor::SystemProfiler::UNAME_HASH
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, "abc123")
should "omit telemetry_id when telemetry is disabled" do
Stripe.enable_telemetry = false
ua = APIRequestor::SystemProfiler.user_agent
refute ua.key?(:telemetry_id)
ensure
Stripe.enable_telemetry = false
end

should "omit telemetry_id when TelemetryId.get returns nil" do
Stripe.enable_telemetry = true
TelemetryId.stubs(:get).returns(nil)
ua = APIRequestor::SystemProfiler.user_agent
assert_equal "abc123", ua[:source]
refute ua.key?(:telemetry_id)
ensure
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, original)
Stripe.enable_telemetry = false
end
end
Expand Down
135 changes: 135 additions & 0 deletions test/stripe/telemetry_id_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# frozen_string_literal: true

require File.expand_path("../test_helper", __dir__)
require "tmpdir"

module Stripe
class TelemetryIdTest < Test::Unit::TestCase
setup do
TelemetryId.reset!
end

teardown do
TelemetryId.reset!
end

context ".config_dir" do
should "return XDG_CONFIG_HOME/stripe when XDG_CONFIG_HOME is set" do
with_env("XDG_CONFIG_HOME" => "/tmp/xdg") do
assert_equal "/tmp/xdg/stripe", TelemetryId.config_dir
end
end

should "return ~/.config/stripe when XDG_CONFIG_HOME is not set" do
with_env("XDG_CONFIG_HOME" => nil) do
assert_equal ::File.expand_path("~/.config/stripe"), TelemetryId.config_dir
end
end

should "fall back to ~/.config/stripe when XDG_CONFIG_HOME is empty" do
with_env("XDG_CONFIG_HOME" => "") do
assert_equal ::File.expand_path("~/.config/stripe"), TelemetryId.config_dir
end
end
end

context ".get" do
should "return nil when config_dir returns nil" do
TelemetryId.stubs(:config_dir).returns(nil)
assert_nil TelemetryId.get
end

should "return cached id from file when it exists" do
::Dir.mktmpdir do |dir|
file_path = ::File.join(dir, "telemetry_id")
::File.write(file_path, "stored_id_abc123\n")

TelemetryId.stubs(:config_dir).returns(dir)
assert_equal "stored_id_abc123", TelemetryId.get
end
end

should "generate and persist a new id when file does not exist" do
::Dir.mktmpdir do |dir|
TelemetryId.stubs(:config_dir).returns(dir)
id = TelemetryId.get
refute_nil id
assert_equal 32, id.length
assert_equal id, ::File.read(::File.join(dir, "telemetry_id"))
end
end

should "create parent directory when it does not exist" do
::Dir.mktmpdir do |base_dir|
dir = ::File.join(base_dir, "nonexistent", "nested")
TelemetryId.stubs(:config_dir).returns(dir)
id = TelemetryId.get
refute_nil id
assert ::File.exist?(::File.join(dir, "telemetry_id"))
end
end

should "return nil when directory cannot be created" do
TelemetryId.stubs(:config_dir).returns("/nonexistent/readonly/path")
::FileUtils.stubs(:mkdir_p).raises(Errno::EACCES, "permission denied")
assert_nil TelemetryId.get
end

should "cache the result after first call" do
::Dir.mktmpdir do |dir|
TelemetryId.stubs(:config_dir).returns(dir)
first_id = TelemetryId.get
# Modify the file to confirm the second call reads from cache
::File.write(::File.join(dir, "telemetry_id"), "different_value")
second_id = TelemetryId.get
assert_equal first_id, second_id
end
end

should "only call config_dir once even when it returns nil" do
TelemetryId.stubs(:config_dir).returns(nil)
TelemetryId.get
# Second call should use cache; expects config_dir was only called once
TelemetryId.expects(:config_dir).never
TelemetryId.get
end
end

context ".reset!" do
should "clear cached state so next call re-evaluates" do
::Dir.mktmpdir do |dir|
TelemetryId.stubs(:config_dir).returns(dir)
first_id = TelemetryId.get
refute_nil first_id

TelemetryId.reset!
::File.write(::File.join(dir, "telemetry_id"), "new_persisted_id")

second_id = TelemetryId.get
assert_equal "new_persisted_id", second_id
end
end
end

private def with_env(vars)
old_values = {}
vars.each do |key, value|
old_values[key] = ENV.fetch(key, nil)
if value.nil?
ENV.delete(key)
else
ENV[key] = value
end
end
yield
ensure
old_values.each do |key, value|
if value.nil?
ENV.delete(key)
else
ENV[key] = value
end
end
end
end
end
Loading