From bad6d48b2ef18846ed4cc9f6d7b7d8ef67169573 Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:48:10 -0700 Subject: [PATCH 1/3] add user-agent to api client --- internal/api/client.go | 8 ++++++++ internal/api/client_test.go | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/internal/api/client.go b/internal/api/client.go index 5a3d3a5..4fe8f5c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -36,6 +36,7 @@ type Client struct { apiKey string httpClient *http.Client debug bool + userAgent string } func NewClient(baseURL, apiKey string, debug bool) *Client { @@ -44,9 +45,15 @@ func NewClient(baseURL, apiKey string, debug bool) *Client { apiKey: apiKey, httpClient: &http.Client{Timeout: 5 * time.Second}, debug: debug, + userAgent: "loops-go/dev", } } +func (c *Client) WithUserAgent(ua string) *Client { + c.userAgent = ua + return c +} + func errorFromResponse(resp *http.Response) *APIError { var body struct { Error string `json:"error"` @@ -119,6 +126,7 @@ func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, return nil, err } req.Header.Set("Authorization", "Bearer "+c.apiKey) + req.Header.Set("User-Agent", c.userAgent) if body != nil { req.Header.Set("Content-Type", "application/json") } diff --git a/internal/api/client_test.go b/internal/api/client_test.go index 57dd481..9ec52e9 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -79,10 +79,25 @@ func TestNewRequest(t *testing.T) { if got := req.Header.Get("Authorization"); got != wantAuth { t.Errorf("Authorization = %q, want %q", got, wantAuth) } + + if got := req.Header.Get("User-Agent"); got != "loops-go/dev" { + t.Errorf("User-Agent = %q, want %q", got, "loops-go/dev") + } }) } } +func TestWithUserAgent(t *testing.T) { + client := NewClient("https://example.com/api/v1", "test-key", false).WithUserAgent("loops-cli/1.2.3") + req, err := client.newRequest(http.MethodGet, "/api-key", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := req.Header.Get("User-Agent"); got != "loops-cli/1.2.3" { + t.Errorf("User-Agent = %q, want %q", got, "loops-cli/1.2.3") + } +} + func TestNewRequest_InvalidURL(t *testing.T) { client := NewClient("://bad-url", "test-key", false) _, err := client.newRequest(http.MethodGet, "/path", nil) From 733fd5e5b2a5c44942ad5fa8c3fa21d18c96278f Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:48:31 -0700 Subject: [PATCH 2/3] add cli user agent --- cmd/api_key.go | 2 +- cmd/auth_login.go | 2 +- cmd/auth_status.go | 2 +- cmd/contact_properties.go | 4 ++-- cmd/contacts.go | 8 ++++---- cmd/events.go | 2 +- cmd/lists.go | 2 +- cmd/root.go | 6 ++++++ cmd/transactional.go | 4 ++-- 9 files changed, 19 insertions(+), 13 deletions(-) diff --git a/cmd/api_key.go b/cmd/api_key.go index 5be562e..114bc24 100644 --- a/cmd/api_key.go +++ b/cmd/api_key.go @@ -9,7 +9,7 @@ import ( ) func runAPIKey(cfg *config.Config) (*api.APIKeyResponse, error) { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).GetAPIKey() + return newAPIClient(cfg).GetAPIKey() } var apiKeyCmd = &cobra.Command{ diff --git a/cmd/auth_login.go b/cmd/auth_login.go index 306be7c..1c8a221 100644 --- a/cmd/auth_login.go +++ b/cmd/auth_login.go @@ -66,7 +66,7 @@ func runAuthLogin(apiKey, name string, skipVerify bool) (*api.APIKeyResponse, er } return nil, nil } - result, err := api.NewClient(config.EndpointURL(), apiKey, debugFlag).GetAPIKey() + result, err := api.NewClient(config.EndpointURL(), apiKey, debugFlag).WithUserAgent("loops-cli/"+version).GetAPIKey() if err != nil { return nil, fmt.Errorf("API key verification failed: %w", err) } diff --git a/cmd/auth_status.go b/cmd/auth_status.go index 0deba3c..5133146 100644 --- a/cmd/auth_status.go +++ b/cmd/auth_status.go @@ -47,7 +47,7 @@ func runAuthStatus() (*config.Config, *api.APIKeyResponse, *config.PersistentCon if err != nil { return nil, nil, nil, err } - keyResp, err := api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).GetAPIKey() + keyResp, err := newAPIClient(cfg).GetAPIKey() if err != nil { return nil, nil, nil, fmt.Errorf("API key verification failed: %w", err) } diff --git a/cmd/contact_properties.go b/cmd/contact_properties.go index 1a1950f..5b15ec6 100644 --- a/cmd/contact_properties.go +++ b/cmd/contact_properties.go @@ -9,11 +9,11 @@ import ( ) func runContactPropertiesList(cfg *config.Config, customOnly bool) ([]api.ContactProperty, error) { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).ListContactProperties(customOnly) + return newAPIClient(cfg).ListContactProperties(customOnly) } func runContactPropertiesCreate(cfg *config.Config, name, propType string) error { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).CreateContactProperty(name, propType) + return newAPIClient(cfg).CreateContactProperty(name, propType) } var contactPropertiesCmd = &cobra.Command{ diff --git a/cmd/contacts.go b/cmd/contacts.go index 8f61078..8296db1 100644 --- a/cmd/contacts.go +++ b/cmd/contacts.go @@ -77,7 +77,7 @@ func contactFieldParamsFromCmd(cmd *cobra.Command) (contactFieldParams, error) { // find func runContactsFind(cfg *config.Config, email, userID string) ([]api.Contact, error) { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).FindContacts(api.FindContactParams{ + return newAPIClient(cfg).FindContacts(api.FindContactParams{ Email: email, UserID: userID, }) @@ -149,7 +149,7 @@ type contactCreateResult struct { } func runContactsCreate(cfg *config.Config, req api.CreateContactRequest) (string, error) { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).CreateContact(req) + return newAPIClient(cfg).CreateContact(req) } var contactsCreateCmd = &cobra.Command{ @@ -196,7 +196,7 @@ var contactsCreateCmd = &cobra.Command{ // update func runContactsUpdate(cfg *config.Config, req api.UpdateContactRequest) error { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).UpdateContact(req) + return newAPIClient(cfg).UpdateContact(req) } var contactsUpdateCmd = &cobra.Command{ @@ -244,7 +244,7 @@ var contactsUpdateCmd = &cobra.Command{ // delete func runContactsDelete(cfg *config.Config, email, userID string) error { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).DeleteContact(email, userID) + return newAPIClient(cfg).DeleteContact(email, userID) } var contactsDeleteCmd = &cobra.Command{ diff --git a/cmd/events.go b/cmd/events.go index 33da593..ea1e7a0 100644 --- a/cmd/events.go +++ b/cmd/events.go @@ -35,7 +35,7 @@ func parseMailingLists(pairs []string) (map[string]bool, error) { } func runEventsSend(cfg *config.Config, req api.SendEventRequest) error { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).SendEvent(req) + return newAPIClient(cfg).SendEvent(req) } var eventsCmd = &cobra.Command{ diff --git a/cmd/lists.go b/cmd/lists.go index eaa39a7..a6a1e15 100644 --- a/cmd/lists.go +++ b/cmd/lists.go @@ -9,7 +9,7 @@ import ( ) func runListsList(cfg *config.Config) ([]api.MailingList, error) { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).ListMailingLists() + return newAPIClient(cfg).ListMailingLists() } var listsCmd = &cobra.Command{ diff --git a/cmd/root.go b/cmd/root.go index 5185218..76a998f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "fmt" "os" + "github.com/loops-so/cli/internal/api" "github.com/loops-so/cli/internal/config" "github.com/spf13/cobra" ) @@ -15,6 +16,11 @@ var outputFormat outputFlag = "text" var teamFlag string var debugFlag bool +func newAPIClient(cfg *config.Config) *api.Client { + return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug). + WithUserAgent("loops-cli/" + version) +} + func loadConfig() (*config.Config, error) { cfg, err := config.Load(teamFlag) if err != nil { diff --git a/cmd/transactional.go b/cmd/transactional.go index ee4bb1d..07fc32a 100644 --- a/cmd/transactional.go +++ b/cmd/transactional.go @@ -64,7 +64,7 @@ func attachmentFromPath(path string) (api.Attachment, error) { } func runTransactionalList(cfg *config.Config, params api.PaginationParams) ([]api.TransactionalEmail, error) { - client := api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug) + client := newAPIClient(cfg) if params.Cursor != "" { emails, _, err := client.ListTransactional(params) return emails, err @@ -78,7 +78,7 @@ func runTransactionalList(cfg *config.Config, params api.PaginationParams) ([]ap } func runTransactionalSend(cfg *config.Config, req api.SendTransactionalRequest) error { - return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).SendTransactional(req) + return newAPIClient(cfg).SendTransactional(req) } var transactionalCmd = &cobra.Command{ From bc1f3a5efa6ac2273b0b679d574d5e290a9199cd Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:00:36 -0700 Subject: [PATCH 3/3] dont repeat the user-agent string --- cmd/auth_login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/auth_login.go b/cmd/auth_login.go index 1c8a221..355c5c3 100644 --- a/cmd/auth_login.go +++ b/cmd/auth_login.go @@ -66,7 +66,7 @@ func runAuthLogin(apiKey, name string, skipVerify bool) (*api.APIKeyResponse, er } return nil, nil } - result, err := api.NewClient(config.EndpointURL(), apiKey, debugFlag).WithUserAgent("loops-cli/"+version).GetAPIKey() + result, err := newAPIClient(&config.Config{EndpointURL: config.EndpointURL(), APIKey: apiKey, Debug: debugFlag}).GetAPIKey() if err != nil { return nil, fmt.Errorf("API key verification failed: %w", err) }