From e894f31973a0dc7a7f9c37bb11a0324859460f64 Mon Sep 17 00:00:00 2001 From: Kanishk Date: Thu, 28 May 2026 00:40:04 +0530 Subject: [PATCH] RTECO-1368 - Fix metrics collection for agent detection and interactive context fields --- .gitignore | 1 + common/commands/command.go | 3 + common/commands/execution_context.go | 13 ++++ common/commands/metrics_collector_test.go | 55 +++++++++++++++ .../usage/visibility/commands_count_metric.go | 14 ++++ .../visibility/commands_count_metric_test.go | 70 +++++++++++++++++-- 6 files changed, 150 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 3b7951668..6a0bd0c18 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ testdata/go/project1/go.sum testdata/go/tmp tmp out +.gradle # Vim *~ diff --git a/common/commands/command.go b/common/commands/command.go index bf8a8ad5b..9a0efa7f9 100644 --- a/common/commands/command.go +++ b/common/commands/command.go @@ -140,6 +140,9 @@ func reportUsageToVisibilitySystem(commandName string, serverDetails *config.Ser IsCI: metricsData.IsCI, CISystem: metricsData.CISystem, IsContainer: metricsData.IsContainer, + IsAgent: metricsData.IsAgent, + Agent: metricsData.Agent, + IsInteractive: metricsData.IsInteractive, PackageAlias: metricsData.PackageAlias, PackageManager: metricsData.PackageManager, } diff --git a/common/commands/execution_context.go b/common/commands/execution_context.go index b12b8bfea..37411812d 100644 --- a/common/commands/execution_context.go +++ b/common/commands/execution_context.go @@ -55,6 +55,19 @@ var ( cachedExecutionContext ExecutionContext ) +// ResetExecutionContextForTest clears the memoized ExecutionContext so the +// next DetectExecutionContext call re-evaluates env vars. Exported for tests +// in downstream modules (e.g. jfrog-cli) that need to assert agent-detection +// behaviour against in-process command invocations after other tests in the +// same binary have already triggered memoization. +// +// Production code MUST NOT call this. Calling it concurrently with +// DetectExecutionContext is unsafe. +func ResetExecutionContextForTest() { + executionContextOnce = sync.Once{} + cachedExecutionContext = ExecutionContext{} +} + func computeExecutionContext() ExecutionContext { ec := ExecutionContext{ IsInteractive: clientlog.IsStdOutTerminal(), diff --git a/common/commands/metrics_collector_test.go b/common/commands/metrics_collector_test.go index 795901315..fa9d335db 100644 --- a/common/commands/metrics_collector_test.go +++ b/common/commands/metrics_collector_test.go @@ -984,3 +984,58 @@ func TestMetricsIntegrationFlow(t *testing.T) { t.Error("Expected metrics to be cleared after retrieval") } } + +// TestAgentContextEndToEnd verifies that agent/is_agent/is_interactive +// signals survive the full chain: env -> ExecutionContext -> CollectMetrics -> +// GetCollectedMetrics -> visibility.MetricsData -> commandsCountLabels -> wire JSON. +// This guards against any field being dropped at the boundaries between layers. +func TestAgentContextEndToEnd(t *testing.T) { + ClearAllMetrics() + clearAgentEnvVars(t) + t.Setenv("CURSOR_AGENT", "1") + resetExecutionContextForTest(t) + + commandName := "rt_download" + flags := []string{"recursive"} + + CollectMetrics(commandName, flags) + + collected := GetCollectedMetrics(commandName) + if collected == nil { + t.Fatal("Expected metrics to be collected") + } + if !collected.IsAgent || collected.Agent != "cursor" { + t.Errorf("collected IsAgent/Agent wrong: IsAgent=%v Agent=%q", collected.IsAgent, collected.Agent) + } + + visibilityData := &visibility.MetricsData{ + Flags: collected.Flags, + Platform: collected.Platform, + Architecture: collected.Architecture, + IsCI: collected.IsCI, + CISystem: collected.CISystem, + IsContainer: collected.IsContainer, + IsAgent: collected.IsAgent, + Agent: collected.Agent, + IsInteractive: collected.IsInteractive, + PackageAlias: collected.PackageAlias, + PackageManager: collected.PackageManager, + } + + metric := visibility.NewCommandsCountMetricWithEnhancedData(commandName, visibilityData) + metricJSON, err := json.Marshal(metric) + if err != nil { + t.Fatalf("json marshal failed: %v", err) + } + + wire := string(metricJSON) + if !strings.Contains(wire, `"is_agent":"true"`) { + t.Errorf("wire JSON missing is_agent=true: %s", wire) + } + if !strings.Contains(wire, `"agent":"cursor"`) { + t.Errorf("wire JSON missing agent=cursor: %s", wire) + } + if !strings.Contains(wire, `"is_interactive":`) { + t.Errorf("wire JSON missing is_interactive: %s", wire) + } +} diff --git a/utils/usage/visibility/commands_count_metric.go b/utils/usage/visibility/commands_count_metric.go index cdd6d7c47..93a2db6c7 100644 --- a/utils/usage/visibility/commands_count_metric.go +++ b/utils/usage/visibility/commands_count_metric.go @@ -28,6 +28,9 @@ type commandsCountLabels struct { IsCI string `json:"is_ci"` CISystem string `json:"ci_system,omitempty"` IsContainer string `json:"is_container"` + IsAgent string `json:"is_agent,omitempty"` + Agent string `json:"agent,omitempty"` + IsInteractive string `json:"is_interactive,omitempty"` PackageAlias string `json:"package_alias,omitempty"` PackageManager string `json:"package_manager,omitempty"` } @@ -79,6 +82,17 @@ func NewCommandsCountMetricWithEnhancedData(commandName string, metricsData *Met } else { labels.IsContainer = "false" } + if metricsData.IsAgent { + labels.IsAgent = "true" + labels.Agent = metricsData.Agent + } else { + labels.IsAgent = "false" + } + if metricsData.IsInteractive { + labels.IsInteractive = "true" + } else { + labels.IsInteractive = "false" + } if metricsData.PackageAlias { labels.PackageAlias = "true" labels.PackageManager = metricsData.PackageManager diff --git a/utils/usage/visibility/commands_count_metric_test.go b/utils/usage/visibility/commands_count_metric_test.go index 40edcc409..3a8f33190 100644 --- a/utils/usage/visibility/commands_count_metric_test.go +++ b/utils/usage/visibility/commands_count_metric_test.go @@ -63,12 +63,15 @@ func TestNewCommandsCountMetricWithEnhancedData(t *testing.T) { commandName := "enhanced-test-command" metricsData := &MetricsData{ - Flags: []string{"verbose", "recursive", "threads"}, - Platform: "linux", - Architecture: "amd64", - IsCI: true, - CISystem: "github_actions", - IsContainer: false, + Flags: []string{"verbose", "recursive", "threads"}, + Platform: "linux", + Architecture: "amd64", + IsCI: true, + CISystem: "github_actions", + IsContainer: false, + IsAgent: true, + Agent: "cursor", + IsInteractive: false, } metric := NewCommandsCountMetricWithEnhancedData(commandName, metricsData) @@ -88,6 +91,9 @@ func TestNewCommandsCountMetricWithEnhancedData(t *testing.T) { assert.Equal(t, "true", labels.IsCI) assert.Equal(t, "github_actions", labels.CISystem) assert.Equal(t, "false", labels.IsContainer) + assert.Equal(t, "true", labels.IsAgent) + assert.Equal(t, "cursor", labels.Agent) + assert.Equal(t, "false", labels.IsInteractive) } func TestNewCommandsCountMetricWithNilEnhancedData(t *testing.T) { @@ -113,6 +119,58 @@ func TestNewCommandsCountMetricWithNilEnhancedData(t *testing.T) { assert.Empty(t, labels.IsCI) assert.Empty(t, labels.CISystem) assert.Empty(t, labels.IsContainer) + assert.Empty(t, labels.IsAgent) + assert.Empty(t, labels.Agent) + assert.Empty(t, labels.IsInteractive) +} + +func TestNewCommandsCountMetricWithAgentContext(t *testing.T) { + commandName := "rt_download" + + metricsData := &MetricsData{ + IsAgent: true, + Agent: "cursor", + IsInteractive: true, + } + + metric := NewCommandsCountMetricWithEnhancedData(commandName, metricsData) + + labels, ok := metric.Labels.(*commandsCountLabels) + assert.True(t, ok, "Expected labels to be of type commandsCountLabels") + + assert.Equal(t, "true", labels.IsAgent) + assert.Equal(t, "cursor", labels.Agent) + assert.Equal(t, "true", labels.IsInteractive) + + metricJSON, err := json.Marshal(metric) + assert.NoError(t, err) + assert.Contains(t, string(metricJSON), `"is_agent":"true"`) + assert.Contains(t, string(metricJSON), `"agent":"cursor"`) + assert.Contains(t, string(metricJSON), `"is_interactive":"true"`) +} + +func TestNewCommandsCountMetricWithoutAgentContext(t *testing.T) { + commandName := "rt_upload" + + metricsData := &MetricsData{ + IsAgent: false, + IsInteractive: false, + } + + metric := NewCommandsCountMetricWithEnhancedData(commandName, metricsData) + + labels, ok := metric.Labels.(*commandsCountLabels) + assert.True(t, ok, "Expected labels to be of type commandsCountLabels") + + assert.Equal(t, "false", labels.IsAgent) + assert.Empty(t, labels.Agent) + assert.Equal(t, "false", labels.IsInteractive) + + metricJSON, err := json.Marshal(metric) + assert.NoError(t, err) + assert.Contains(t, string(metricJSON), `"is_agent":"false"`) + assert.Contains(t, string(metricJSON), `"is_interactive":"false"`) + assert.NotContains(t, string(metricJSON), `"agent":`) } func TestMetricsDataStructure(t *testing.T) {