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 cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import (
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/style"
"github.com/slackapi/slack-cli/internal/update"
"github.com/slackapi/slack-cli/internal/useragent"
"github.com/slackapi/slack-cli/internal/version"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -286,6 +287,7 @@ func InitConfig(ctx context.Context, clients *shared.ClientFactory, rootCmd *cob
rootCmd.SetContext(ctx)
// Debug logging
clients.IO.PrintDebug(ctx, "system_id: %s", clients.Config.SystemID)
clients.IO.PrintDebug(ctx, "user_agent: %s", useragent.BuildUserAgent(version.Raw()))
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: This is a new addition to our verbose logs. For a while, I've wanted to see our user-agent which is used for each API request. This may also be helpful when diagnosing user reported issues.

}

// Init Project ID, if current directory is a project
Expand Down
11 changes: 4 additions & 7 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"io"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
"time"
Expand All @@ -35,6 +34,7 @@ import (
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/internal/slackdeps"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/useragent"
"github.com/uber/jaeger-client-go"
)

Expand Down Expand Up @@ -128,10 +128,9 @@ func (c *Client) postForm(ctx context.Context, endpoint string, formValues url.V
if err != nil {
return nil, err
}
var userAgent = fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)

request.Header.Add("content-type", "application/x-www-form-urlencoded")
request.Header.Add("User-Agent", userAgent)
request.Header.Add("User-Agent", useragent.BuildUserAgent(cliVersion))
if jaegerSpanContext, ok := span.Context().(jaeger.SpanContext); ok {
request.Header.Add("x-b3-sampled", "0")
request.Header.Add("x-b3-spanid", jaegerSpanContext.SpanID().String())
Expand Down Expand Up @@ -179,8 +178,7 @@ func (c *Client) postJSON(ctx context.Context, endpoint, token string, cookie st
if err != nil {
return nil, err
}
var userAgent = fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)
request.Header.Add("User-Agent", userAgent)
request.Header.Add("User-Agent", useragent.BuildUserAgent(cliVersion))
if jaegerSpanContext, ok := span.Context().(jaeger.SpanContext); ok {
request.Header.Add("x-b3-sampled", "0")
request.Header.Add("x-b3-spanid", jaegerSpanContext.SpanID().String())
Expand Down Expand Up @@ -232,9 +230,8 @@ func (c *Client) get(ctx context.Context, endpoint, token string, cookie string)
if err != nil {
return nil, err
}
var userAgent = fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)

request.Header.Add("User-Agent", userAgent)
request.Header.Add("User-Agent", useragent.BuildUserAgent(cliVersion))
if jaegerSpanContext, ok := span.Context().(jaeger.SpanContext); ok {
request.Header.Add("x-b3-sampled", "0")
request.Header.Add("x-b3-spanid", jaegerSpanContext.SpanID().String())
Expand Down
4 changes: 2 additions & 2 deletions internal/api/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import (
"fmt"
"net/http"
"net/url"
"runtime"

"github.com/opentracing/opentracing-go"
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/internal/useragent"
)

var docsBaseURL = "https://docs.slack.dev"
Expand Down Expand Up @@ -59,7 +59,7 @@ func buildDocsSearchRequest(ctx context.Context, urlStr, cliVersion string) (*ht
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS))
req.Header.Add("User-Agent", useragent.BuildUserAgent(cliVersion))
return req, nil
}

Expand Down
5 changes: 2 additions & 3 deletions internal/api/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import (
"net/http"
"net/textproto"
"net/url"
"runtime"

"github.com/opentracing/opentracing-go"
"github.com/slackapi/slack-cli/internal/image"
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/useragent"
"github.com/spf13/afero"
)

Expand Down Expand Up @@ -132,8 +132,7 @@ func (c *Client) uploadIcon(ctx context.Context, fs afero.Fs, token, appID, icon
if err != nil {
return IconResult{}, err
}
var userAgent = fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)
request.Header.Add("User-Agent", userAgent)
request.Header.Add("User-Agent", useragent.BuildUserAgent(cliVersion))

c.io.PrintDebug(ctx, "HTTP Request: %v %v %v", request.Method, request.URL, request.Proto)
c.io.PrintDebug(ctx, "HTTP Request User-Agent: %s", request.Header.Get("User-Agent"))
Expand Down
4 changes: 2 additions & 2 deletions internal/api/raw_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import (
"io"
"net/http"
"net/url"
"runtime"
"time"

"github.com/opentracing/opentracing-go"
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/internal/useragent"
"github.com/uber/jaeger-client-go"
)

Expand Down Expand Up @@ -67,7 +67,7 @@ func (c *Client) RawRequest(ctx context.Context, httpMethod, endpoint, token str
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS))
request.Header.Set("User-Agent", useragent.BuildUserAgent(cliVersion))

if jaegerSpanContext, ok := span.Context().(jaeger.SpanContext); ok {
request.Header.Set("x-b3-sampled", "0")
Expand Down
5 changes: 2 additions & 3 deletions internal/api/s3_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import (
"mime/multipart"
"net/http"
"net/textproto"
"runtime"

"github.com/opentracing/opentracing-go"
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/useragent"
"github.com/spf13/afero"
)

Expand Down Expand Up @@ -100,8 +100,7 @@ func (c *Client) UploadPackageToS3(ctx context.Context, fs afero.Fs, appID strin
if err != nil {
return fileName, err
}
var userAgent = fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)
request.Header.Add("User-Agent", userAgent)
request.Header.Add("User-Agent", useragent.BuildUserAgent(cliVersion))

var s3span = opentracing.StartSpan("apiclient.UploadPackageToS3.FileUpload", opentracing.ChildOf(span.Context()))
s3span.SetTag("app", appID)
Expand Down
1 change: 1 addition & 0 deletions internal/tracking/logstash_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type EventData struct {

// EventContext contains information / metadata about the CLI session
type EventContext struct {
AIAgent string `json:"ai_agent,omitempty"`
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I chose ai_agent instead of agent because it's less ambiguous and more specific that it's an AI "agent".

Arch string `json:"arch"`
Binary string `json:"bin"`
CLIVersion string `json:"cli_version"`
Expand Down
2 changes: 2 additions & 0 deletions internal/tracking/tracking.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/slackapi/slack-cli/internal/ioutils"
"github.com/slackapi/slack-cli/internal/slackcontext"
"github.com/slackapi/slack-cli/internal/style"
"github.com/slackapi/slack-cli/internal/useragent"
)

// TrackingManager is an interface for tracking metrics and events related to CLI activity
Expand Down Expand Up @@ -194,6 +195,7 @@ func (e *EventTracker) FlushToLogstash(ctx context.Context, cfg *config.Config,
Timestamp: time.Now().UnixMilli(),
Data: eventData,
Context: EventContext{
AIAgent: useragent.DetectName(),
CLIVersion: versionString,
Host: ioutils.GetHostname(),
OS: runtime.GOOS,
Expand Down
77 changes: 77 additions & 0 deletions internal/useragent/useragent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package useragent

import (
"fmt"
"os"
"runtime"
"strings"
)

// AIAgent represents a detected AI coding agent that invoked the CLI.
type AIAgent struct {
Name string
Entry string
}

// Detect checks environment variables to determine if the CLI is being run by
// an AI coding agent. Returns nil if no agent is detected. Detection priority:
// CLAUDECODE > CODEX_CI > GEMINI_CLI > CLINE_ACTIVE > CURSOR_AGENT > AGENT.
func Detect() *AIAgent {
switch {
case os.Getenv("CLAUDECODE") == "1":
return &AIAgent{
Name: "claude-code",
Entry: os.Getenv("CLAUDE_CODE_ENTRYPOINT"),
}
case os.Getenv("CODEX_CI") == "1":
return &AIAgent{Name: "codex"}
case os.Getenv("GEMINI_CLI") == "1":
return &AIAgent{Name: "gemini-cli"}
case os.Getenv("CLINE_ACTIVE") == "true":
return &AIAgent{Name: "cline"}
case os.Getenv("CURSOR_AGENT") == "1":
return &AIAgent{Name: "cursor"}
case os.Getenv("AGENT") != "":
return &AIAgent{Name: os.Getenv("AGENT")}
default:
return nil
}
}

// DetectName returns the normalized name of the detected AI agent, or an empty
// string if no agent is detected.
func DetectName() string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func DetectName() string {
func DetectHarness() string {

🪬 question: Forgive these words but this is an unusual export for the value found I think but I also admit to not being so familiar with practices encouraged here!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌃 ramble: It does seem solid if we're framing this as the... actual user agent though? I'm understanding more I hope!

if agent := Detect(); agent != nil {
return agent.Name
}
return ""
}

// BuildUserAgent constructs the HTTP User-Agent header value for the CLI. If an
// AI agent is detected, an "AI-Agent (name=..., entry=...)" suffix is appended.
Comment on lines +64 to +65
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📚 quibble: It'd be helpful to have a full example here but no blocker!

func BuildUserAgent(cliVersion string) string {
ua := fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)
if agent := Detect(); agent != nil {
var parts []string
parts = append(parts, "name="+agent.Name)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parts = append(parts, "name="+agent.Name)
parts = append(parts, "name: "+agent.Name)

☎️ question: A ":" separator might match the os adjacent but I understand can make queries more difficult. If this was intentional please ignore but I did want to ask?

if agent.Entry != "" {
parts = append(parts, "entry="+agent.Entry)
}
ua += " AI-Agent (" + strings.Join(parts, ", ") + ")"
}
return ua
}
Loading
Loading