Skip to content

Commit 44c3f85

Browse files
authored
Merge pull request #1908 from stripe/xavdid/merge-ruby-beta
Merge to beta
2 parents 72aede3 + ca77234 commit 44c3f85

9 files changed

Lines changed: 263 additions & 56 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,22 @@ jobs:
4545
path: '*.gem'
4646

4747
test:
48-
name: Test (${{ matrix.ruby-version }})
49-
runs-on: ubuntu-24.04
48+
name: Test (${{ matrix.ruby-version }}, ${{ matrix.os }})
49+
runs-on: ${{ matrix.os }}
5050
permissions:
5151
contents: read
5252
strategy:
53+
fail-fast: false
5354
matrix:
54-
# following https://docs.stripe.com/sdks/versioning?lang=ruby#stripe-sdk-language-version-support-policy
55-
ruby-version: [2.7, '3.0', 3.1, 3.2, 3.3, 3.4, jruby-9.4.7.0, truffleruby-25.0.0]
55+
os:
56+
- ubuntu-24.04
57+
# https://docs.stripe.com/sdks/versioning?lang=ruby#stripe-sdk-language-version-support-policy
58+
# https://endoflife.date/ruby
59+
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', jruby-9.4.7.0, truffleruby-25.0.0]
60+
include:
61+
- os: windows-latest
62+
# use any modern-ish version
63+
ruby-version: '3.4'
5664
steps:
5765
- uses: extractions/setup-just@v2
5866
- uses: actions/checkout@v3
@@ -62,7 +70,15 @@ jobs:
6270
ruby-version: ${{ matrix.ruby-version }}
6371
- uses: stripe/openapi/actions/stripe-mock@master
6472
- name: test
65-
run: just test typecheck
73+
run: just test
74+
env:
75+
GITHUB_TOKEN: ${{ secrets.github_token }}
76+
# Use IP instead of 'localhost' to avoid slow DNS resolution on Windows
77+
STRIPE_MOCK_HOST: ${{ runner.os == 'Windows' && '127.0.0.1' || 'localhost' }}
78+
- name: typecheck
79+
# sorbet doesn't support windows
80+
if: runner.os != 'Windows'
81+
run: just typecheck
6682
env:
6783
GITHUB_TOKEN: ${{ secrets.github_token }}
6884

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ This release changes the pinned API version to 2026-06-24.preview.
2727
* Add support for event notification `V2MoneyManagementOutboundTransferUnderReviewEvent` with related object `V2::MoneyManagement::OutboundTransfer`
2828
* ⚠️ Remove support for event notifications `V2CoreAccountIncludingConfigurationStorerCapabilityStatusUpdatedEvent` and `V2CoreAccountIncludingConfigurationStorerUpdatedEvent` with related object `V2::Core::Account`
2929

30+
## 19.3.1 - 2026-07-15
31+
* [#1902](https://github.com/stripe/stripe-ruby/pull/1902) Replace source hash with Telemetry UUID
32+
* [#1901](https://github.com/stripe/stripe-ruby/pull/1901) Make Error fields generated
33+
3034
## 19.3.0 - 2026-06-24
3135
This release changes the pinned API version to 2026-06-24.dahlia.
3236

Gemfile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ group :development do
2727
# The latest version of rubocop is only compatible with Ruby 2.7+
2828
gem "rubocop", "1.75.2" if RUBY_VERSION >= "2.7"
2929

30-
gem "sorbet"
31-
gem "tapioca"
30+
unless RUBY_PLATFORM =~ /mingw|mswin/
31+
gem "sorbet"
32+
gem "tapioca"
33+
end
3234

3335
platforms :mri do
3436
gem "byebug"

lib/stripe.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require "net/http"
99
require "openssl"
1010
require "rbconfig"
11+
require "fileutils"
1112
require "securerandom"
1213
require "set"
1314
require "socket"
@@ -36,6 +37,7 @@
3637
require "stripe/request_params"
3738
require "stripe/stripe_context"
3839
require "stripe/util"
40+
require "stripe/telemetry_id"
3941
require "stripe/connection_manager"
4042
require "stripe/multipart_encoder"
4143
require "stripe/api_requestor"

lib/stripe/api_requestor.rb

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
require "digest"
43
require "socket"
54
require "stripe/instrumentation"
65

@@ -1104,31 +1103,6 @@ def dup_from_response_headers(headers)
11041103
# in so that we can generate a rich user agent header to help debug
11051104
# integrations.
11061105
class SystemProfiler
1107-
UNAME_HASH = begin
1108-
parts = []
1109-
parts << if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
1110-
begin
1111-
`ver 2>NUL`.strip
1112-
rescue StandardError
1113-
""
1114-
end
1115-
else
1116-
begin
1117-
`uname -a 2>/dev/null`.strip
1118-
rescue StandardError
1119-
""
1120-
end
1121-
end
1122-
parts << begin
1123-
Socket.gethostname
1124-
rescue StandardError
1125-
""
1126-
end
1127-
Digest::MD5.hexdigest(parts.join(" "))
1128-
rescue StandardError
1129-
""
1130-
end
1131-
11321106
AI_AGENTS = [
11331107
# aiAgents: The beginning of the section generated from our OpenAPI spec
11341108
%w[ANTIGRAVITY_CLI_ALIAS antigravity],
@@ -1166,7 +1140,8 @@ def self.user_agent
11661140

11671141
if Stripe.enable_telemetry?
11681142
ua[:platform] = RUBY_PLATFORM
1169-
ua[:source] = UNAME_HASH unless UNAME_HASH.empty?
1143+
tid = TelemetryId.get
1144+
ua[:telemetry_id] = tid if tid
11701145
end
11711146

11721147
ai_agent = detect_ai_agent

lib/stripe/telemetry_id.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
module Stripe
4+
module TelemetryId
5+
@cached_id = nil
6+
@loaded = false
7+
8+
def self.config_dir
9+
if Gem.win_platform?
10+
appdata = ENV.fetch("APPDATA", nil)
11+
return nil unless appdata && !appdata.empty?
12+
13+
::File.join(appdata, "Stripe")
14+
else
15+
xdg = ENV.fetch("XDG_CONFIG_HOME", nil)
16+
if xdg && !xdg.empty?
17+
::File.join(xdg, "stripe")
18+
else
19+
::File.expand_path("~/.config/stripe")
20+
end
21+
end
22+
rescue ArgumentError
23+
nil
24+
end
25+
26+
def self.get
27+
return @cached_id if @loaded
28+
29+
dir = config_dir
30+
return nil unless dir
31+
32+
file_path = ::File.join(dir, "telemetry_id")
33+
34+
begin
35+
content = ::File.read(file_path).strip
36+
unless content.empty?
37+
@cached_id = content
38+
return @cached_id
39+
end
40+
rescue SystemCallError
41+
# File doesn't exist or can't be read
42+
end
43+
44+
new_id = SecureRandom.hex(16)
45+
46+
begin
47+
::FileUtils.mkdir_p(dir)
48+
::File.write(file_path, new_id)
49+
rescue SystemCallError
50+
return nil
51+
end
52+
53+
@cached_id = new_id
54+
@cached_id
55+
ensure
56+
@loaded = true
57+
end
58+
59+
# For testing only
60+
def self.reset!
61+
@cached_id = nil
62+
@loaded = false
63+
end
64+
end
65+
end

test/stripe/api_requestor_test.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -902,8 +902,8 @@ class RequestorTest < Test::Unit::TestCase
902902
client.send(request_method, :post, "/v1/charges", :api,
903903
&@read_body_chunk_block)
904904
end
905-
assert_equal "#{APIRequestor::ERROR_MESSAGE_CONNECTION % Stripe::DEFAULT_API_BASE} Request was retried 2 times.\n\n(Network error: Connection refused)",
906-
e.message
905+
assert_match(/Request was retried 2 times\.\n\n\(Network error: .*refused/i,
906+
e.message)
907907
end
908908

909909
should "handle error response with unknown value" do
@@ -1800,29 +1800,29 @@ class SystemProfilerTest < Test::Unit::TestCase
18001800
Stripe.enable_telemetry = false
18011801
end
18021802

1803-
should "omit source when UNAME_HASH is empty" do
1804-
original = APIRequestor::SystemProfiler::UNAME_HASH
1805-
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
1806-
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, "")
1803+
should "include telemetry_id when telemetry is enabled" do
18071804
Stripe.enable_telemetry = true
1805+
TelemetryId.stubs(:get).returns("abc123def456")
18081806
ua = APIRequestor::SystemProfiler.user_agent
1809-
refute ua.key?(:source)
1807+
assert_equal "abc123def456", ua[:telemetry_id]
18101808
ensure
1811-
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
1812-
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, original)
18131809
Stripe.enable_telemetry = false
18141810
end
18151811

1816-
should "include source when UNAME_HASH is non-empty" do
1817-
original = APIRequestor::SystemProfiler::UNAME_HASH
1818-
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
1819-
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, "abc123")
1812+
should "omit telemetry_id when telemetry is disabled" do
1813+
Stripe.enable_telemetry = false
1814+
ua = APIRequestor::SystemProfiler.user_agent
1815+
refute ua.key?(:telemetry_id)
1816+
ensure
1817+
Stripe.enable_telemetry = false
1818+
end
1819+
1820+
should "omit telemetry_id when TelemetryId.get returns nil" do
18201821
Stripe.enable_telemetry = true
1822+
TelemetryId.stubs(:get).returns(nil)
18211823
ua = APIRequestor::SystemProfiler.user_agent
1822-
assert_equal "abc123", ua[:source]
1824+
refute ua.key?(:telemetry_id)
18231825
ensure
1824-
APIRequestor::SystemProfiler.send(:remove_const, :UNAME_HASH)
1825-
APIRequestor::SystemProfiler.const_set(:UNAME_HASH, original)
18261826
Stripe.enable_telemetry = false
18271827
end
18281828
end

test/stripe/telemetry_id_test.rb

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# frozen_string_literal: true
2+
3+
require File.expand_path("../test_helper", __dir__)
4+
require "tmpdir"
5+
6+
module Stripe
7+
class TelemetryIdTest < Test::Unit::TestCase
8+
setup do
9+
TelemetryId.reset!
10+
end
11+
12+
teardown do
13+
TelemetryId.reset!
14+
end
15+
16+
context ".config_dir" do
17+
if RUBY_PLATFORM !~ /mingw|mswin/
18+
should "return XDG_CONFIG_HOME/stripe when XDG_CONFIG_HOME is set" do
19+
with_env("XDG_CONFIG_HOME" => "/tmp/xdg") do
20+
assert_equal "/tmp/xdg/stripe", TelemetryId.config_dir
21+
end
22+
end
23+
24+
should "return ~/.config/stripe when XDG_CONFIG_HOME is not set" do
25+
with_env("XDG_CONFIG_HOME" => nil) do
26+
assert_equal ::File.expand_path("~/.config/stripe"), TelemetryId.config_dir
27+
end
28+
end
29+
30+
should "fall back to ~/.config/stripe when XDG_CONFIG_HOME is empty" do
31+
with_env("XDG_CONFIG_HOME" => "") do
32+
assert_equal ::File.expand_path("~/.config/stripe"), TelemetryId.config_dir
33+
end
34+
end
35+
else
36+
should "return APPDATA/Stripe on Windows" do
37+
refute_nil TelemetryId.config_dir
38+
assert TelemetryId.config_dir.end_with?("Stripe")
39+
end
40+
end
41+
end
42+
43+
context ".get" do
44+
should "return nil when config_dir returns nil" do
45+
TelemetryId.stubs(:config_dir).returns(nil)
46+
assert_nil TelemetryId.get
47+
end
48+
49+
should "return cached id from file when it exists" do
50+
::Dir.mktmpdir do |dir|
51+
file_path = ::File.join(dir, "telemetry_id")
52+
::File.write(file_path, "stored_id_abc123\n")
53+
54+
TelemetryId.stubs(:config_dir).returns(dir)
55+
assert_equal "stored_id_abc123", TelemetryId.get
56+
end
57+
end
58+
59+
should "generate and persist a new id when file does not exist" do
60+
::Dir.mktmpdir do |dir|
61+
TelemetryId.stubs(:config_dir).returns(dir)
62+
id = TelemetryId.get
63+
refute_nil id
64+
assert_equal 32, id.length
65+
assert_equal id, ::File.read(::File.join(dir, "telemetry_id"))
66+
end
67+
end
68+
69+
should "create parent directory when it does not exist" do
70+
::Dir.mktmpdir do |base_dir|
71+
dir = ::File.join(base_dir, "nonexistent", "nested")
72+
TelemetryId.stubs(:config_dir).returns(dir)
73+
id = TelemetryId.get
74+
refute_nil id
75+
assert ::File.exist?(::File.join(dir, "telemetry_id"))
76+
end
77+
end
78+
79+
should "return nil when directory cannot be created" do
80+
TelemetryId.stubs(:config_dir).returns("/nonexistent/readonly/path")
81+
::FileUtils.stubs(:mkdir_p).raises(Errno::EACCES, "permission denied")
82+
assert_nil TelemetryId.get
83+
end
84+
85+
should "cache the result after first call" do
86+
::Dir.mktmpdir do |dir|
87+
TelemetryId.stubs(:config_dir).returns(dir)
88+
first_id = TelemetryId.get
89+
# Modify the file to confirm the second call reads from cache
90+
::File.write(::File.join(dir, "telemetry_id"), "different_value")
91+
second_id = TelemetryId.get
92+
assert_equal first_id, second_id
93+
end
94+
end
95+
96+
should "only call config_dir once even when it returns nil" do
97+
TelemetryId.stubs(:config_dir).returns(nil)
98+
TelemetryId.get
99+
# Second call should use cache; expects config_dir was only called once
100+
TelemetryId.expects(:config_dir).never
101+
TelemetryId.get
102+
end
103+
end
104+
105+
context ".reset!" do
106+
should "clear cached state so next call re-evaluates" do
107+
::Dir.mktmpdir do |dir|
108+
TelemetryId.stubs(:config_dir).returns(dir)
109+
first_id = TelemetryId.get
110+
refute_nil first_id
111+
112+
TelemetryId.reset!
113+
::File.write(::File.join(dir, "telemetry_id"), "new_persisted_id")
114+
115+
second_id = TelemetryId.get
116+
assert_equal "new_persisted_id", second_id
117+
end
118+
end
119+
end
120+
121+
private def with_env(vars)
122+
old_values = {}
123+
vars.each do |key, value|
124+
old_values[key] = ENV.fetch(key, nil)
125+
if value.nil?
126+
ENV.delete(key)
127+
else
128+
ENV[key] = value
129+
end
130+
end
131+
yield
132+
ensure
133+
old_values.each do |key, value|
134+
if value.nil?
135+
ENV.delete(key)
136+
else
137+
ENV[key] = value
138+
end
139+
end
140+
end
141+
end
142+
end

0 commit comments

Comments
 (0)