Skip to content

Commit 8d5d19f

Browse files
authored
Merge pull request #2166 from stripe/jar/merge-java-private-preview
Merge to private-preview
2 parents 94d67ad + ee69e8a commit 8d5d19f

4 files changed

Lines changed: 139 additions & 6 deletions

File tree

.claude/CLAUDE.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# stripe-java
2+
3+
## Testing
4+
5+
- Run all tests: `just test`
6+
- Run a single test class: `just test-one com.stripe.net.HttpClientTest`
7+
- Pass extra Gradle args: `just test --tests com.stripe.SomeTest`
8+
9+
## Formatting
10+
11+
- Format: `just format` (uses Spotless via Gradle)
12+
- Format check: `just format-check`
13+
14+
## Key Locations
15+
16+
- HTTP client abstract base (retry logic): `src/main/java/com/stripe/net/HttpClient.java`
17+
- HTTP implementation: `src/main/java/com/stripe/net/HttpURLConnectionClient.java`
18+
- Header management: `src/main/java/com/stripe/net/HttpHeaders.java`
19+
- Request building: `src/main/java/com/stripe/net/StripeRequest.java`
20+
- Authentication: `src/main/java/com/stripe/net/Authenticator.java`, `BearerTokenAuthenticator.java`
21+
- Response getter (request dispatch): `src/main/java/com/stripe/net/LiveStripeResponseGetter.java`
22+
- Main config/version: `src/main/java/com/stripe/Stripe.java`
23+
- Client class: `src/main/java/com/stripe/StripeClient.java`
24+
25+
## Generated Code
26+
27+
- Files containing `File generated from our OpenAPI spec` at the top are generated; do not edit. Similarly, any code block starting with `The beginning of the section generated from our OpenAPI spec` is generated and should not be edited directly.
28+
- If something in a generated file/range needs to be updated, add a summary of the change to your report but don't attempt to edit it directly.
29+
- Resource model classes under `src/main/java/com/stripe/model/` are largely generated.
30+
- The `net/` package (HTTP client, headers, request/response) is NOT generated.
31+
32+
## Conventions
33+
34+
- Uses Java's built-in `HttpURLConnection` for HTTP
35+
- Requires JDK 17 to build
36+
- Gradle build system
37+
- Work is not complete until `just test` passes
38+
39+
### Comments
40+
41+
- Comments MUST only be used to:
42+
1. Document a function
43+
2. Explain the WHY of a piece of code
44+
3. Explain a particularly complicated piece of code
45+
- Comments NEVER should be used to:
46+
1. Say what used to be there. That's no longer relevant!
47+
2. Explain the WHAT of a piece of code (unless it's very non-obvious)
48+
49+
It's ok not to put comments on/in a function if their addition wouldn't meaningfully clarify anything.

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ jobs:
157157
((github.event_name == 'workflow_dispatch') || (github.event_name == 'push')) &&
158158
startsWith(github.ref, 'refs/tags/v') &&
159159
!contains(github.ref, 'beta') &&
160+
!contains(github.ref, 'alpha') &&
160161
endsWith(github.actor, '-stripe')
161162
needs: [build, test]
162163
runs-on: "ubuntu-24.04"

src/main/java/com/stripe/net/HttpClient.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Optional;
1414
import java.util.Properties;
1515
import java.util.concurrent.ThreadLocalRandom;
16+
import java.util.function.Function;
1617

1718
/** Base abstract class for HTTP clients used to send requests to Stripe's API. */
1819
public abstract class HttpClient {
@@ -137,12 +138,39 @@ public StripeResponseStream requestStreamWithRetries(StripeRequest request)
137138
return sendWithRetries(request, (r) -> this.requestStream(r));
138139
}
139140

141+
static String detectAIAgent() {
142+
return detectAIAgent(System::getenv);
143+
}
144+
145+
static String detectAIAgent(Function<String, String> getEnv) {
146+
String[][] agents = {
147+
{"ANTIGRAVITY_CLI_ALIAS", "antigravity"},
148+
{"CLAUDECODE", "claude_code"},
149+
{"CLINE_ACTIVE", "cline"},
150+
{"CODEX_SANDBOX", "codex_cli"},
151+
{"CURSOR_AGENT", "cursor"},
152+
{"GEMINI_CLI", "gemini_cli"},
153+
{"OPENCODE", "open_code"},
154+
};
155+
for (String[] agent : agents) {
156+
String val = getEnv.apply(agent[0]);
157+
if (val != null && !val.isEmpty()) {
158+
return agent[1];
159+
}
160+
}
161+
return "";
162+
}
163+
140164
/**
141165
* Builds the value of the {@code User-Agent} header.
142166
*
143167
* @return a string containing the value of the {@code User-Agent} header
144168
*/
145169
protected static String buildUserAgentString(StripeRequest request) {
170+
return buildUserAgentString(request, detectAIAgent());
171+
}
172+
173+
static String buildUserAgentString(StripeRequest request, String aiAgent) {
146174
String apiMode = request.apiMode() == ApiMode.V2 ? "v2" : "v1";
147175

148176
String userAgent = String.format("Stripe/%s JavaBindings/%s", apiMode, Stripe.VERSION);
@@ -151,6 +179,10 @@ protected static String buildUserAgentString(StripeRequest request) {
151179
userAgent += " " + formatAppInfo(Stripe.getAppInfo());
152180
}
153181

182+
if (!aiAgent.isEmpty()) {
183+
userAgent += " AIAgent/" + aiAgent;
184+
}
185+
154186
return userAgent;
155187
}
156188

@@ -160,6 +192,10 @@ protected static String buildUserAgentString(StripeRequest request) {
160192
* @return a string containing the value of the {@code X-Stripe-Client-User-Agent} header
161193
*/
162194
protected static String buildXStripeClientUserAgentString() {
195+
return buildXStripeClientUserAgentString(detectAIAgent());
196+
}
197+
198+
static String buildXStripeClientUserAgentString(String aiAgent) {
163199
String[] propertyNames = {
164200
"os.name",
165201
"os.version",
@@ -182,6 +218,10 @@ protected static String buildXStripeClientUserAgentString() {
182218
}
183219
getGsonVersion().ifPresent(ver -> propertyMap.put("gson.version", ver));
184220

221+
if (!aiAgent.isEmpty()) {
222+
propertyMap.put("ai_agent", aiAgent);
223+
}
224+
185225
return ApiResource.INTERNAL_GSON.toJson(propertyMap);
186226
}
187227

src/test/java/com/stripe/net/HttpClientTest.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,8 @@ public void testV1RequestSetsCorrectUserAgent() throws StripeException {
192192
RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(),
193193
ApiMode.V1);
194194

195-
assertEquals(
196-
HttpClient.buildUserAgentString(request),
197-
String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION));
195+
String userAgent = HttpClient.buildUserAgentString(request);
196+
assertTrue(userAgent.startsWith(String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION)));
198197
}
199198

200199
@Test
@@ -207,8 +206,52 @@ public void testV2RequestSetsCorrectUserAgent() throws StripeException {
207206
RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(),
208207
ApiMode.V2);
209208

210-
assertEquals(
211-
HttpClient.buildUserAgentString(request),
212-
String.format("Stripe/v2 JavaBindings/%s", Stripe.VERSION));
209+
String userAgent = HttpClient.buildUserAgentString(request);
210+
assertTrue(userAgent.startsWith(String.format("Stripe/v2 JavaBindings/%s", Stripe.VERSION)));
211+
}
212+
213+
@Test
214+
public void testDetectAIAgent() {
215+
String agent = HttpClient.detectAIAgent(key -> key.equals("CLAUDECODE") ? "1" : null);
216+
assertEquals("claude_code", agent);
217+
}
218+
219+
@Test
220+
public void testDetectAIAgentNoEnv() {
221+
String agent = HttpClient.detectAIAgent(key -> null);
222+
assertEquals("", agent);
223+
}
224+
225+
@Test
226+
public void testDetectAIAgentFirstMatchWins() {
227+
String agent =
228+
HttpClient.detectAIAgent(
229+
key -> {
230+
if (key.equals("CURSOR_AGENT") || key.equals("OPENCODE")) return "1";
231+
return null;
232+
});
233+
assertEquals("cursor", agent);
234+
}
235+
236+
@Test
237+
public void testBuildUserAgentStringWithAIAgent() throws StripeException {
238+
StripeRequest request =
239+
StripeRequest.create(
240+
ApiResource.RequestMethod.GET,
241+
"http://example.com/get",
242+
null,
243+
RequestOptions.builder().setApiKey("sk_test_123").build(),
244+
ApiMode.V1);
245+
246+
String userAgent = HttpClient.buildUserAgentString(request, "cursor");
247+
assertTrue(userAgent.contains("AIAgent/cursor"));
248+
}
249+
250+
@Test
251+
public void testBuildXStripeClientUserAgentStringWithAIAgent() {
252+
String json = HttpClient.buildXStripeClientUserAgentString("cursor");
253+
com.google.gson.JsonObject parsed =
254+
com.google.gson.JsonParser.parseString(json).getAsJsonObject();
255+
assertEquals("cursor", parsed.get("ai_agent").getAsString());
213256
}
214257
}

0 commit comments

Comments
 (0)