Skip to content
Merged
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
27 changes: 27 additions & 0 deletions stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"regexp"
Expand Down Expand Up @@ -1557,6 +1558,7 @@ func (nopReadCloser) Close() error { return nil }
// is serialized and sent in the `X-Stripe-Client-User-Agent` as additional
// debugging information.
type stripeClientUserAgent struct {
AIAgent string `json:"ai_agent,omitempty"`
Application *AppInfo `json:"application"`
BindingsVersion string `json:"bindings_version"`
Language string `json:"lang"`
Expand Down Expand Up @@ -1622,6 +1624,25 @@ func getUname() string {
return out.String()
}

var aiAgents = map[string]string{
"ANTIGRAVITY_CLI_ALIAS": "antigravity",
"CLAUDECODE": "claude_code",
"CLINE_ACTIVE": "cline",
"CODEX_SANDBOX": "codex_cli",
"CURSOR_AGENT": "cursor",
"GEMINI_CLI": "gemini_cli",
"OPENCODE": "open_code",
}

func detectAIAgent(lookupEnv func(string) (string, bool)) (string, bool) {
for k, name := range aiAgents {
if val, ok := lookupEnv(k); ok && val != "" {
return name, true
}
}
return "", false
}

func init() {
initUserAgent()
}
Expand All @@ -1631,6 +1652,9 @@ func initUserAgent() {
if appInfo != nil {
encodedUserAgent += " " + appInfo.formatUserAgent()
}
if agent, ok := detectAIAgent(os.LookupEnv); ok {
encodedUserAgent += " AIAgent/" + agent
}
encodedStripeUserAgentReady = &sync.Once{}
}

Expand All @@ -1644,6 +1668,9 @@ func getEncodedStripeUserAgent() string {
Publisher: "stripe",
Uname: getUname(),
}
if agent, ok := detectAIAgent(os.LookupEnv); ok {
stripeUserAgent.AIAgent = agent
}
marshaled, err := json.Marshal(stripeUserAgent)
// Encoding this struct should never be a problem, so we're okay to panic
// in case it is for some reason.
Expand Down
86 changes: 84 additions & 2 deletions stripe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,7 @@ func TestUserAgent(t *testing.T) {

// We keep out version constant private to the package, so use a regexp
// match instead.
expectedPattern := regexp.MustCompile(`^Stripe/v1 GoBindings/[.\-\w\d]+$`)
expectedPattern := regexp.MustCompile(`^Stripe/v1 GoBindings/[.\-\w\d]+( AIAgent/\w+)?$`)

match := expectedPattern.MatchString(req.Header.Get("User-Agent"))
assert.True(t, match)
Expand All @@ -1244,7 +1244,7 @@ func TestUserAgentWithAppInfo(t *testing.T) {

// We keep out version constant private to the package, so use a regexp
// match instead.
expectedPattern := regexp.MustCompile(`^Stripe/v1 GoBindings/[.\-\w\d]+ MyAwesomePlugin/1.2.34 \(https://myawesomeplugin.info\)$`)
expectedPattern := regexp.MustCompile(`^Stripe/v1 GoBindings/[.\-\w\d]+ MyAwesomePlugin/1.2.34 \(https://myawesomeplugin.info\)( AIAgent/\w+)?$`)

match := expectedPattern.MatchString(req.Header.Get("User-Agent"))
assert.True(t, match)
Expand Down Expand Up @@ -1321,6 +1321,88 @@ func TestStripeClientUserAgentWithAppInfo(t *testing.T) {
assert.Equal(t, appInfo.Version, decodedAppInfo["version"])
}

func TestDetectAIAgent(t *testing.T) {
// Test detection of a specific agent
t.Run("DetectsAgent", func(t *testing.T) {
getEnv := func(key string) (string, bool) {
if key == "CLAUDECODE" {
return "1", true
}
return "", false
}
agent, ok := detectAIAgent(getEnv)
assert.True(t, ok)
assert.Equal(t, "claude_code", agent)
})

// Test no agent detected
t.Run("NoAgent", func(t *testing.T) {
getEnv := func(key string) (string, bool) { return "", false }
agent, ok := detectAIAgent(getEnv)
assert.False(t, ok)
assert.Equal(t, "", agent)
})

// Test each agent individually
t.Run("AllAgents", func(t *testing.T) {
for k, expected := range aiAgents {
getEnv := func(key string) (string, bool) {
if key == k {
return "1", true
}
return "", false
}
agent, ok := detectAIAgent(getEnv)
assert.True(t, ok)
assert.Equal(t, expected, agent, "Expected %s for env var %s", expected, k)
}
})
}

func TestUserAgentWithAIAgent(t *testing.T) {
// Save and restore the original encodedUserAgent
originalEncodedUserAgent := encodedUserAgent
defer func() {
encodedUserAgent = originalEncodedUserAgent
}()

// Simulate AI agent detection by manually setting the user agent
encodedUserAgent = "Stripe/v1 GoBindings/" + clientversion + " AIAgent/claude_code"

c := GetBackend(APIBackend).(*BackendImplementation)
req, err := c.NewRequest("", "/v1/hello", "", "", nil)
assert.NoError(t, err)

assert.Contains(t, req.Header.Get("User-Agent"), "AIAgent/claude_code")
}

func TestStripeClientUserAgentWithAIAgent(t *testing.T) {
// Save and restore the original state
originalEncodedStripeUserAgent := encodedStripeUserAgent
originalReady := encodedStripeUserAgentReady
defer func() {
encodedStripeUserAgent = originalEncodedStripeUserAgent
encodedStripeUserAgentReady = originalReady
}()

// Reset so getEncodedStripeUserAgent re-computes
encodedStripeUserAgentReady = &sync.Once{}

encoded := getEncodedStripeUserAgent()
var userAgent map[string]interface{}
err := json.Unmarshal([]byte(encoded), &userAgent)
assert.NoError(t, err)

// The ai_agent field may or may not be present depending on the test
// environment. If no AI agent env var is set, the key should be absent
// (omitempty). We just verify the JSON is valid and the field type is
// correct if present.
if val, ok := userAgent["ai_agent"]; ok {
_, isString := val.(string)
assert.True(t, isString)
}
}

func TestResponseToError(t *testing.T) {
c := GetBackend(APIBackend).(*BackendImplementation)

Expand Down