From 27bca127c1559c73a50a520bd65ed36603091dc6 Mon Sep 17 00:00:00 2001 From: tytv2 Date: Sat, 25 Jul 2026 22:25:20 +0700 Subject: [PATCH] feat(vks): send grn-vks-cli User-Agent on every VKS API request Tag each outbound VKS public-API request with a version-stamped User-Agent (grn-vks-cli/) so the backend can attribute and count traffic from the grn VKS CLI. The header is set on both the initial request and the post-401 retry; version is shared with cliVersion so release-please bumps it automatically. --- go/cmd/root.go | 5 +++++ go/internal/client/client.go | 8 +++++++ go/internal/client/client_test.go | 36 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/go/cmd/root.go b/go/cmd/root.go index 2054b49..7744c10 100644 --- a/go/cmd/root.go +++ b/go/cmd/root.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/vngcloud/greennode-cli/cmd/configure" "github.com/vngcloud/greennode-cli/internal/cli" + "github.com/vngcloud/greennode-cli/internal/client" "github.com/vngcloud/greennode-cli/internal/config" ) @@ -74,6 +75,10 @@ func init() { rootCmd.SetVersionTemplate("grn-cli/{{.Version}}\n") + // Tag every VKS API request with a version-stamped User-Agent so the backend + // can attribute traffic to the grn VKS CLI. Shares cliVersion with --version. + client.UserAgent = "grn-vks-cli/" + cliVersion + rootCmd.AddCommand(configure.ConfigureCmd) for _, svc := range cli.Services() { rootCmd.AddCommand(svc) diff --git a/go/internal/client/client.go b/go/internal/client/client.go index c15738b..b9f68a8 100644 --- a/go/internal/client/client.go +++ b/go/internal/client/client.go @@ -39,6 +39,12 @@ var retryableStatusCodes = map[int]bool{ 500: true, 502: true, 503: true, 504: true, } +// UserAgent is the User-Agent header sent on every VKS API request so the +// backend can attribute traffic to the grn VKS CLI. cmd overrides it at startup +// with the release version (e.g. "grn-vks-cli/1.7.3"); the version-less default +// keeps identification intact if that override is ever skipped (e.g. in tests). +var UserAgent = "grn-vks-cli" + // GreennodeClient is an HTTP client for Greennode APIs with retry and auto token refresh. type GreennodeClient struct { baseURL string @@ -205,6 +211,7 @@ func (c *GreennodeClient) requestRaw(method, path string, params map[string]stri req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", UserAgent) if c.debug { fmt.Fprintf(os.Stderr, "[debug] %s %s\n", method, fullURL) @@ -245,6 +252,7 @@ func (c *GreennodeClient) requestRaw(method, path string, params map[string]stri req2, _ := http.NewRequest(method, fullURL, retryBody) req2.Header.Set("Authorization", "Bearer "+token) req2.Header.Set("Content-Type", "application/json") + req2.Header.Set("User-Agent", UserAgent) resp2, err := c.httpClient.Do(req2) if err != nil { return "", err diff --git a/go/internal/client/client_test.go b/go/internal/client/client_test.go index 523888e..0dbcea4 100644 --- a/go/internal/client/client_test.go +++ b/go/internal/client/client_test.go @@ -61,6 +61,42 @@ func TestPatchSendsPatchMethodAndBody(t *testing.T) { } } +func TestRequestSetsUserAgentHeader(t *testing.T) { + // Every outbound VKS API request must carry a User-Agent so the backend can + // attribute traffic to the grn VKS CLI. + var gotUA string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotUA = r.Header.Get("User-Agent") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer srv.Close() + + // Pin a known value; the production default is set from cmd at startup. + prev := UserAgent + UserAgent = "grn-vks-cli/9.9.9" + defer func() { UserAgent = prev }() + + tm := auth.NewTokenManager("id", "secret") + tm.SetToken("test-token", time.Now().Add(1*time.Hour)) + + c := NewGreennodeClient(srv.URL, tm, 5*time.Second, 5*time.Second, false, false) + if _, err := c.Get("/v1/thing", nil); err != nil { + t.Fatalf("Get returned error: %v", err) + } + if gotUA != "grn-vks-cli/9.9.9" { + t.Errorf("User-Agent = %q, want %q", gotUA, "grn-vks-cli/9.9.9") + } +} + +func TestUserAgentDefaultsToVKSCLI(t *testing.T) { + // The package default identifies the VKS CLI even if cmd never overrides it. + if UserAgent != "grn-vks-cli" { + t.Errorf("default UserAgent = %q, want %q", UserAgent, "grn-vks-cli") + } +} + func TestFormatErrorSurfacesNestedErrorObject(t *testing.T) { // VKS returns errors as {"error": {"message": ...}} — a nested object, not a // string. The detail must still reach the user instead of being dropped.