From ab1fc6c089ab3a801ed62365035d30f973342b19 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Thu, 28 May 2026 01:10:59 +0530 Subject: [PATCH] RTECO-1368 - Fix metrics collection for agent detection and interactive context fields --- go.mod | 2 +- go.sum | 4 +- metrics_visibility_test.go | 134 +++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3f3fcaea7..3e30cf33a 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64 github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260527043943-fdf755c4f4c2 - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260527100708-78f8364e88e1 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194 github.com/jfrog/jfrog-cli-evidence v0.9.4 github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260430094150-ce7d9b371c6f github.com/jfrog/jfrog-cli-security v1.29.2 diff --git a/go.sum b/go.sum index 7f657b6ae..5f4f08d77 100644 --- a/go.sum +++ b/go.sum @@ -414,8 +414,8 @@ github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64 h1:b github.com/jfrog/jfrog-cli-application v1.0.2-0.20260511133105-55a0ab56fd64/go.mod h1:cKqb/JgN+XuD4RhOxvSZnyGyXw3cJsTZfQT3rk9MCho= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260527043943-fdf755c4f4c2 h1:acwlLYjjglecqjXXgj2JoM1bUhH/dDMpLpJXrBfvyqU= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260527043943-fdf755c4f4c2/go.mod h1:T5HDtDxHlUZWF4LQnmF2kiyFyd8yLOf3K618BsG0CWk= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260527100708-78f8364e88e1 h1:GRKvXZ0aLrQbKuMnaWrks9xCJoiRaSfVEZ57k2rd+b0= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260527100708-78f8364e88e1/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194 h1:cwppCKLitT0XBqYGQimW00qyx1ej88sY+rIjXAWNvAU= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-cli-evidence v0.9.4 h1:RAqZYaH2RrzmhW+bGA7dx/yTqa4X1fZ4/5V7VVMSJtc= github.com/jfrog/jfrog-cli-evidence v0.9.4/go.mod h1:nLSLqLIhQz1Hi2n+KjHZTyK1mcmPLevv41LjItLswmE= github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260430094150-ce7d9b371c6f h1:M1cesbKYSznwPA76dNctjCELxGx34TSSjwoYnJm9/6Y= diff --git a/metrics_visibility_test.go b/metrics_visibility_test.go index 987038aaa..9a93feca0 100644 --- a/metrics_visibility_test.go +++ b/metrics_visibility_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + corecommands "github.com/jfrog/jfrog-cli-core/v2/common/commands" coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" ) @@ -86,6 +87,139 @@ func startVisMockServer(t *testing.T) (*httptest.Server, chan visReq) { return srv, ch } +// TestVisibility_AgentContext_E2E verifies that agent execution context +// (is_agent / agent / is_interactive) flows end-to-end from env vars through +// the metrics pipeline into the visibility wire payload. +// +// ExecutionContext memoizes via sync.Once for the process lifetime, and other +// tests in this binary trigger the metrics path first under `go test`, so we +// must reset the cache before re-evaluating env vars and reset again on +// cleanup so this test cannot leak agent state into anything that follows. +func TestVisibility_AgentContext_E2E(t *testing.T) { + corecommands.ResetExecutionContextForTest() + t.Cleanup(corecommands.ResetExecutionContextForTest) + t.Setenv("CURSOR_AGENT", "1") + + srv, ch := startMockServer(t) + defer srv.Close() + + home := t.TempDir() + t.Setenv("JFROG_CLI_HOME_DIR", home) + t.Setenv("JFROG_CLI_REPORT_USAGE", "true") + + jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials() + platformURL := srv.URL + "/" + artURL := srv.URL + "/artifactory/" + if err := jf.Exec("c", "add", "agent-mock", + "--url", platformURL, + "--artifactory-url", artURL, + "--access-token", "dummy", + "--interactive=false", + "--enc-password=false", + ); err != nil { + t.Fatalf("config add failed: %v", err) + } + if err := jf.Exec("c", "use", "agent-mock"); err != nil { + t.Fatalf("config use failed: %v", err) + } + + if err := jf.Exec("rt", "ping", "--server-id", "agent-mock"); err != nil { + t.Fatalf("jf exec failed: %v", err) + } + + select { + case req := <-ch: + if req.Path != "/jfconnect/api/v1/backoffice/metrics/log" { + t.Fatalf("unexpected path: %s", req.Path) + } + var payload struct { + Labels struct { + IsAgent string `json:"is_agent"` + Agent string `json:"agent"` + IsInteractive string `json:"is_interactive"` + } `json:"labels"` + } + if err := json.Unmarshal(req.Body, &payload); err != nil { + t.Fatalf("bad JSON: %v\nbody: %s", err, string(req.Body)) + } + if payload.Labels.IsAgent != "true" { + t.Errorf("is_agent: got %q want %q (body: %s)", payload.Labels.IsAgent, "true", string(req.Body)) + } + if payload.Labels.Agent != "cursor" { + t.Errorf("agent: got %q want %q (body: %s)", payload.Labels.Agent, "cursor", string(req.Body)) + } + // Stdout is not a TTY under `go test`, so is_interactive should be "false". + if payload.Labels.IsInteractive != "false" { + t.Errorf("is_interactive: got %q want %q (body: %s)", payload.Labels.IsInteractive, "false", string(req.Body)) + } + case <-time.After(5 * time.Second): + t.Fatalf("timeout waiting for metrics POST") + } +} + +// TestVisibility_NoAgent_E2E is the negative counterpart to +// TestVisibility_AgentContext_E2E: with no agent env var set, the wire payload +// must carry is_agent="false" and an empty agent name. ExecutionContext is +// reset because the positive test above memoizes IsAgent=true earlier in this +// binary. +func TestVisibility_NoAgent_E2E(t *testing.T) { + corecommands.ResetExecutionContextForTest() + t.Cleanup(corecommands.ResetExecutionContextForTest) + t.Setenv("CURSOR_AGENT", "") + t.Setenv("CLAUDECODE", "") + t.Setenv("AGENT", "") + + srv, ch := startMockServer(t) + defer srv.Close() + + home := t.TempDir() + t.Setenv("JFROG_CLI_HOME_DIR", home) + t.Setenv("JFROG_CLI_REPORT_USAGE", "true") + + jf := coreTests.NewJfrogCli(execMain, "jf", "").WithoutCredentials() + platformURL := srv.URL + "/" + artURL := srv.URL + "/artifactory/" + if err := jf.Exec("c", "add", "noagent-mock", + "--url", platformURL, + "--artifactory-url", artURL, + "--access-token", "dummy", + "--interactive=false", + "--enc-password=false", + ); err != nil { + t.Fatalf("config add failed: %v", err) + } + if err := jf.Exec("c", "use", "noagent-mock"); err != nil { + t.Fatalf("config use failed: %v", err) + } + if err := jf.Exec("rt", "ping", "--server-id", "noagent-mock"); err != nil { + t.Fatalf("jf exec failed: %v", err) + } + + select { + case req := <-ch: + if req.Path != "/jfconnect/api/v1/backoffice/metrics/log" { + t.Fatalf("unexpected path: %s", req.Path) + } + var payload struct { + Labels struct { + IsAgent string `json:"is_agent"` + Agent string `json:"agent"` + } `json:"labels"` + } + if err := json.Unmarshal(req.Body, &payload); err != nil { + t.Fatalf("bad JSON: %v\nbody: %s", err, string(req.Body)) + } + if payload.Labels.IsAgent != "false" { + t.Errorf("is_agent: got %q want %q (body: %s)", payload.Labels.IsAgent, "false", string(req.Body)) + } + if payload.Labels.Agent != "" { + t.Errorf("agent: got %q want %q (body: %s)", payload.Labels.Agent, "", string(req.Body)) + } + case <-time.After(5 * time.Second): + t.Fatalf("timeout waiting for metrics POST") + } +} + func TestVisibilitySendUsage_RtCurl_E2E(t *testing.T) { srv, ch := startMockServer(t) defer srv.Close()