From d5932f4a137d0ea7dda00ee3e29d5eac46d97715 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 13 Aug 2026 16:44:27 +0300 Subject: [PATCH 1/4] Add ai_client, ai_model, and ai_trigger Visibility labels. Rename client/model JSON keys for AI clarity and record skill vs hook path attribution from JFROG_CLI_USER_AGENT so the Data Lake can census jfrog-skills/plugin invocations for all reporting customers. --- common/commands/command.go | 1 + common/commands/metrics_collector.go | 24 +++++++++++++ common/commands/metrics_collector_test.go | 36 ++++++++++++++++--- utils/metrics/metrics.go | 7 ++-- .../usage/visibility/commands_count_metric.go | 7 ++-- .../visibility/commands_count_metric_test.go | 24 +++++++++++++ 6 files changed, 90 insertions(+), 9 deletions(-) diff --git a/common/commands/command.go b/common/commands/command.go index 85d5036e2..81ff08db9 100644 --- a/common/commands/command.go +++ b/common/commands/command.go @@ -152,6 +152,7 @@ func reportUsageToVisibilitySystem(commandName string, serverDetails *config.Ser Agent: metricsData.Agent, Client: metricsData.Client, Model: metricsData.Model, + Trigger: metricsData.Trigger, IsInteractive: metricsData.IsInteractive, PackageAlias: metricsData.PackageAlias, PackageManager: metricsData.PackageManager, diff --git a/common/commands/metrics_collector.go b/common/commands/metrics_collector.go index 54e333f38..aa343b167 100644 --- a/common/commands/metrics_collector.go +++ b/common/commands/metrics_collector.go @@ -2,6 +2,7 @@ package commands import ( "os" + "regexp" "runtime" "strings" "sync" @@ -9,6 +10,13 @@ import ( metrics "github.com/jfrog/jfrog-cli-core/v2/utils/metrics" ) +// EnvUserAgent is the process env that skills/hooks set for wrapper identity. +// Duplicated as a string (not imported from jfrog-cli) to avoid an import cycle. +const EnvUserAgent = "JFROG_CLI_USER_AGENT" + +// aiTriggerAllowlist is the only values accepted for Visibility ai_trigger. +var aiTriggerFromUA = regexp.MustCompile(`(?:^|[;(]\s*)trigger=(skill|hook)(?:\s*[;)]|$)`) + // MetricsData is shared from utils/metrics to avoid import cycles. type MetricsData = metrics.MetricsData @@ -35,6 +43,7 @@ func CollectMetrics(commandName string, flags []string) { ec := DetectExecutionContext() ciSystem := detectCISystem() isContainer := isRunningInContainer() + trigger := detectAiTrigger(os.Getenv(EnvUserAgent)) globalMetricsCollector.mu.Lock() defer globalMetricsCollector.mu.Unlock() @@ -63,6 +72,7 @@ func CollectMetrics(commandName string, flags []string) { Agent: ec.Agent, Client: ec.Client, Model: ec.Model, + Trigger: trigger, IsInteractive: ec.IsInteractive, PackageAlias: pkgAliasTool != "", PackageManager: packageManager, @@ -91,12 +101,26 @@ func GetCollectedMetrics(commandName string) *MetricsData { Agent: metrics.Agent, Client: metrics.Client, Model: metrics.Model, + Trigger: metrics.Trigger, IsInteractive: metrics.IsInteractive, PackageAlias: metrics.PackageAlias, PackageManager: metrics.PackageManager, } } +// detectAiTrigger reads trigger=skill|hook from JFROG_CLI_USER_AGENT parens +// (jfrog-skills / plugin path). Unrecognized values are ignored (cardinality). +func detectAiTrigger(userAgent string) string { + if userAgent == "" { + return "" + } + m := aiTriggerFromUA.FindStringSubmatch(userAgent) + if len(m) < 2 { + return "" + } + return m[1] +} + // detectCISystem identifies the CI environment and returns the system name func detectCISystem() string { ciEnvVars := map[string]string{ diff --git a/common/commands/metrics_collector_test.go b/common/commands/metrics_collector_test.go index 52382ad82..b90335adc 100644 --- a/common/commands/metrics_collector_test.go +++ b/common/commands/metrics_collector_test.go @@ -1052,6 +1052,7 @@ func TestAgentContextEndToEnd(t *testing.T) { t.Setenv("CURSOR_AGENT", "1") t.Setenv("TERM_PROGRAM", "vscode") t.Setenv("JFROG_CLI_AI_MODEL", "opus-4.7") + t.Setenv(EnvUserAgent, "jfrog-skills/0.22.0 (trigger=skill; tool=cursor; client=vscode; model=opus-4.7) jfrog-cli-go/2.120.0") resetExecutionContextForTest(t) commandName := "rt_download" @@ -1069,6 +1070,9 @@ func TestAgentContextEndToEnd(t *testing.T) { if collected.Client != "vscode" || collected.Model != "opus-4.7" { t.Errorf("collected Client/Model wrong: Client=%q Model=%q", collected.Client, collected.Model) } + if collected.Trigger != "skill" { + t.Errorf("collected Trigger wrong: %q", collected.Trigger) + } visibilityData := &visibility.MetricsData{ Flags: collected.Flags, @@ -1081,6 +1085,7 @@ func TestAgentContextEndToEnd(t *testing.T) { Agent: collected.Agent, Client: collected.Client, Model: collected.Model, + Trigger: collected.Trigger, IsInteractive: collected.IsInteractive, PackageAlias: collected.PackageAlias, PackageManager: collected.PackageManager, @@ -1099,17 +1104,40 @@ func TestAgentContextEndToEnd(t *testing.T) { if !strings.Contains(wire, `"agent":"cursor"`) { t.Errorf("wire JSON missing agent=cursor: %s", wire) } - if !strings.Contains(wire, `"client":"vscode"`) { - t.Errorf("wire JSON missing client=vscode: %s", wire) + if !strings.Contains(wire, `"ai_client":"vscode"`) { + t.Errorf("wire JSON missing ai_client=vscode: %s", wire) + } + if !strings.Contains(wire, `"ai_model":"opus-4.7"`) { + t.Errorf("wire JSON missing ai_model=opus-4.7: %s", wire) } - if !strings.Contains(wire, `"model":"opus-4.7"`) { - t.Errorf("wire JSON missing model=opus-4.7: %s", wire) + if !strings.Contains(wire, `"ai_trigger":"skill"`) { + t.Errorf("wire JSON missing ai_trigger=skill: %s", wire) } if !strings.Contains(wire, `"is_interactive":`) { t.Errorf("wire JSON missing is_interactive: %s", wire) } } +func TestDetectAiTrigger(t *testing.T) { + cases := []struct { + ua string + want string + }{ + {"jfrog-skills/0.22.0 (trigger=skill; tool=cursor) jfrog-cli-go/2.120.0", "skill"}, + {"jfrog-skills/0.1.0 (trigger=hook) jfrog-cli-go/2.119.0", "hook"}, + {"jfrog-skills/0.9.0 (trigger=skill) jfrog-cli-go/2.120.0 ai-agent/cursor", "skill"}, + {"setup-jfrog-cli-github-action/5.1.0", ""}, + {"jfrog-cli-go/2.119.0 ai-agent/claude", ""}, + {"jfrog-skills/0.22.0 (trigger=evil) jfrog-cli-go/2.120.0", ""}, + {"", ""}, + } + for _, tc := range cases { + if got := detectAiTrigger(tc.ua); got != tc.want { + t.Errorf("detectAiTrigger(%q)=%q want %q", tc.ua, got, tc.want) + } + } +} + // TestExecWithPackageManager verifies that ExecWithPackageManager stamps the // package_manager label in the collected metrics before the command runs. func TestExecWithPackageManager(t *testing.T) { diff --git a/utils/metrics/metrics.go b/utils/metrics/metrics.go index 83a2c8f94..ad50019ca 100644 --- a/utils/metrics/metrics.go +++ b/utils/metrics/metrics.go @@ -10,9 +10,10 @@ type MetricsData struct { CISystem string `json:"ci_system,omitempty"` IsContainer bool `json:"is_container,omitempty"` IsAgent bool `json:"is_agent,omitempty"` - Agent string `json:"agent,omitempty"` // "cursor", "claude"; empty when not an agent - Client string `json:"client,omitempty"` // "vscode", "zed" - Model string `json:"model,omitempty"` // "opus-4.7" + Agent string `json:"agent,omitempty"` // "cursor", "claude"; empty when not an agent + Client string `json:"ai_client,omitempty"` // host app (TERM_PROGRAM): "vscode", "zed" + Model string `json:"ai_model,omitempty"` // model slug: "opus-4.7" + Trigger string `json:"ai_trigger,omitempty"` // "skill" | "hook" from JFROG_CLI_USER_AGENT parens IsInteractive bool `json:"is_interactive,omitempty"` PackageAlias bool `json:"package_alias,omitempty"` PackageManager string `json:"package_manager,omitempty"` diff --git a/utils/usage/visibility/commands_count_metric.go b/utils/usage/visibility/commands_count_metric.go index 707646899..7a4e05a46 100644 --- a/utils/usage/visibility/commands_count_metric.go +++ b/utils/usage/visibility/commands_count_metric.go @@ -30,8 +30,9 @@ type commandsCountLabels struct { IsContainer string `json:"is_container"` IsAgent string `json:"is_agent,omitempty"` Agent string `json:"agent,omitempty"` - Client string `json:"client,omitempty"` - Model string `json:"model,omitempty"` + Client string `json:"ai_client,omitempty"` + Model string `json:"ai_model,omitempty"` + Trigger string `json:"ai_trigger,omitempty"` IsInteractive string `json:"is_interactive,omitempty"` PackageAlias string `json:"package_alias,omitempty"` PackageManager string `json:"package_manager,omitempty"` @@ -92,6 +93,8 @@ func NewCommandsCountMetricWithEnhancedData(commandName string, metricsData *Met } else { labels.IsAgent = "false" } + // Path attribution (jfrog-skills/plugin): independent of IsAgent. + labels.Trigger = metricsData.Trigger if metricsData.IsInteractive { labels.IsInteractive = "true" } else { diff --git a/utils/usage/visibility/commands_count_metric_test.go b/utils/usage/visibility/commands_count_metric_test.go index 3a2d67731..95728b0b7 100644 --- a/utils/usage/visibility/commands_count_metric_test.go +++ b/utils/usage/visibility/commands_count_metric_test.go @@ -97,7 +97,31 @@ func TestNewCommandsCountMetricWithEnhancedData(t *testing.T) { assert.Equal(t, "cursor", labels.Agent) assert.Equal(t, "vscode", labels.Client) assert.Equal(t, "opus-4.7", labels.Model) + assert.Empty(t, labels.Trigger) assert.Equal(t, "false", labels.IsInteractive) + + metricJSON, err := json.Marshal(metric) + assert.NoError(t, err) + wire := string(metricJSON) + assert.Contains(t, wire, `"ai_client":"vscode"`) + assert.Contains(t, wire, `"ai_model":"opus-4.7"`) + assert.NotContains(t, wire, `"client":"`) +} + +func TestNewCommandsCountMetricWithAiTrigger(t *testing.T) { + metricsData := &MetricsData{ + IsAgent: true, + Agent: "cursor", + Trigger: "skill", + } + metric := NewCommandsCountMetricWithEnhancedData("rt_ping", metricsData) + labels, ok := metric.Labels.(*commandsCountLabels) + assert.True(t, ok) + assert.Equal(t, "skill", labels.Trigger) + + wire, err := json.Marshal(metric) + assert.NoError(t, err) + assert.Contains(t, string(wire), `"ai_trigger":"skill"`) } func TestNewCommandsCountMetricWithNilEnhancedData(t *testing.T) { From a21473acc43065ca8602895e13f4d09a7769ad3b Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 13 Aug 2026 16:45:56 +0300 Subject: [PATCH 2/4] Clarify ai_trigger regex comment --- common/commands/metrics_collector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/commands/metrics_collector.go b/common/commands/metrics_collector.go index aa343b167..5702ffb74 100644 --- a/common/commands/metrics_collector.go +++ b/common/commands/metrics_collector.go @@ -14,7 +14,7 @@ import ( // Duplicated as a string (not imported from jfrog-cli) to avoid an import cycle. const EnvUserAgent = "JFROG_CLI_USER_AGENT" -// aiTriggerAllowlist is the only values accepted for Visibility ai_trigger. +// aiTriggerFromUA extracts allowlisted trigger=skill|hook from UA parens. var aiTriggerFromUA = regexp.MustCompile(`(?:^|[;(]\s*)trigger=(skill|hook)(?:\s*[;)]|$)`) // MetricsData is shared from utils/metrics to avoid import cycles. From f05cbcaa4c04698a1ceec5f403ba9886333198ca Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 13 Aug 2026 16:54:41 +0300 Subject: [PATCH 3/4] Detect ai_trigger in ExecutionContext alongside client and model Keeps every AI identity axis on one memoized, agent-gated detection path instead of reading the user-agent env separately at metrics collection. --- common/commands/execution_context.go | 17 ++++++++++++++++ common/commands/execution_context_test.go | 20 +++++++++++++++++++ common/commands/metrics_collector.go | 24 +---------------------- common/commands/metrics_collector_test.go | 11 ++++++----- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/common/commands/execution_context.go b/common/commands/execution_context.go index 4fbcf98db..8ee33d24e 100644 --- a/common/commands/execution_context.go +++ b/common/commands/execution_context.go @@ -2,6 +2,7 @@ package commands import ( "os" + "regexp" "strings" "sync" @@ -28,8 +29,15 @@ type ExecutionContext struct { // Model: model slug (JFROG_CLI_AI_MODEL) — "opus-4.7". Model string + + // Trigger: wrapper invocation type from JFROG_CLI_USER_AGENT — "skill" or "hook". + Trigger string } +const envUserAgent = "JFROG_CLI_USER_AGENT" + +var aiTriggerFromUA = regexp.MustCompile(`(?:^|[;(]\s*)trigger=(skill|hook)(?:\s*[;)]|$)`) + // agentDetector maps an agent name to env signals that prove the agent // invoked the CLI. Envs match on any non-empty value; EnvEquals requires an // exact value (used when a var is shared with non-agent hosts). @@ -183,6 +191,7 @@ func computeExecutionContext() ExecutionContext { if ec.IsAgent { ec.Client = detectClient() ec.Model = detectModel() + ec.Trigger = detectTrigger() } return ec } @@ -195,6 +204,14 @@ func detectClient() string { return sanitizeToken(os.Getenv("TERM_PROGRAM")) } +func detectTrigger() string { + match := aiTriggerFromUA.FindStringSubmatch(os.Getenv(envUserAgent)) + if len(match) < 2 { + return "" + } + return match[1] +} + // maxTokenLen caps sanitized identity tokens so a pathological env value cannot // inflate User-Agent / metrics payloads. Excess is truncated after filtering. const maxTokenLen = 64 diff --git a/common/commands/execution_context_test.go b/common/commands/execution_context_test.go index 821d3fed9..12a121d2c 100644 --- a/common/commands/execution_context_test.go +++ b/common/commands/execution_context_test.go @@ -223,6 +223,7 @@ func clearAgentEnvVars(t *testing.T) { t.Setenv("ROO_CODE_IPC_SOCKET_PATH", "") t.Setenv("TERM_PROGRAM", "") t.Setenv("JFROG_CLI_AI_MODEL", "") + t.Setenv(envUserAgent, "") } func TestDetectExecutionContext_ModelAgentOnly(t *testing.T) { @@ -276,3 +277,22 @@ func TestDetectExecutionContext_ClientSkippedForHuman(t *testing.T) { assert.False(t, ec.IsAgent) assert.Equal(t, "", ec.Client) } + +func TestDetectExecutionContext_TriggerAgentOnly(t *testing.T) { + resetExecutionContextForTest(t) + clearAgentEnvVars(t) + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "1") + t.Setenv(envUserAgent, "jfrog-skills/0.22.0 (trigger=hook) jfrog-cli-go/2.120.0") + + assert.Equal(t, "hook", DetectExecutionContext().Trigger) +} + +func TestDetectExecutionContext_TriggerSkippedForHuman(t *testing.T) { + resetExecutionContextForTest(t) + clearAgentEnvVars(t) + t.Setenv(envUserAgent, "jfrog-skills/0.22.0 (trigger=skill) jfrog-cli-go/2.120.0") + + ec := DetectExecutionContext() + assert.False(t, ec.IsAgent) + assert.Empty(t, ec.Trigger) +} diff --git a/common/commands/metrics_collector.go b/common/commands/metrics_collector.go index 5702ffb74..6561f9f66 100644 --- a/common/commands/metrics_collector.go +++ b/common/commands/metrics_collector.go @@ -2,7 +2,6 @@ package commands import ( "os" - "regexp" "runtime" "strings" "sync" @@ -10,13 +9,6 @@ import ( metrics "github.com/jfrog/jfrog-cli-core/v2/utils/metrics" ) -// EnvUserAgent is the process env that skills/hooks set for wrapper identity. -// Duplicated as a string (not imported from jfrog-cli) to avoid an import cycle. -const EnvUserAgent = "JFROG_CLI_USER_AGENT" - -// aiTriggerFromUA extracts allowlisted trigger=skill|hook from UA parens. -var aiTriggerFromUA = regexp.MustCompile(`(?:^|[;(]\s*)trigger=(skill|hook)(?:\s*[;)]|$)`) - // MetricsData is shared from utils/metrics to avoid import cycles. type MetricsData = metrics.MetricsData @@ -43,7 +35,6 @@ func CollectMetrics(commandName string, flags []string) { ec := DetectExecutionContext() ciSystem := detectCISystem() isContainer := isRunningInContainer() - trigger := detectAiTrigger(os.Getenv(EnvUserAgent)) globalMetricsCollector.mu.Lock() defer globalMetricsCollector.mu.Unlock() @@ -72,7 +63,7 @@ func CollectMetrics(commandName string, flags []string) { Agent: ec.Agent, Client: ec.Client, Model: ec.Model, - Trigger: trigger, + Trigger: ec.Trigger, IsInteractive: ec.IsInteractive, PackageAlias: pkgAliasTool != "", PackageManager: packageManager, @@ -108,19 +99,6 @@ func GetCollectedMetrics(commandName string) *MetricsData { } } -// detectAiTrigger reads trigger=skill|hook from JFROG_CLI_USER_AGENT parens -// (jfrog-skills / plugin path). Unrecognized values are ignored (cardinality). -func detectAiTrigger(userAgent string) string { - if userAgent == "" { - return "" - } - m := aiTriggerFromUA.FindStringSubmatch(userAgent) - if len(m) < 2 { - return "" - } - return m[1] -} - // detectCISystem identifies the CI environment and returns the system name func detectCISystem() string { ciEnvVars := map[string]string{ diff --git a/common/commands/metrics_collector_test.go b/common/commands/metrics_collector_test.go index b90335adc..5b447f4f9 100644 --- a/common/commands/metrics_collector_test.go +++ b/common/commands/metrics_collector_test.go @@ -1042,7 +1042,7 @@ func TestMetricsIntegrationFlow(t *testing.T) { } } -// TestAgentContextEndToEnd verifies that agent/is_agent/client/model/ +// TestAgentContextEndToEnd verifies that agent/is_agent/client/model/trigger/ // 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. @@ -1052,7 +1052,7 @@ func TestAgentContextEndToEnd(t *testing.T) { t.Setenv("CURSOR_AGENT", "1") t.Setenv("TERM_PROGRAM", "vscode") t.Setenv("JFROG_CLI_AI_MODEL", "opus-4.7") - t.Setenv(EnvUserAgent, "jfrog-skills/0.22.0 (trigger=skill; tool=cursor; client=vscode; model=opus-4.7) jfrog-cli-go/2.120.0") + t.Setenv(envUserAgent, "jfrog-skills/0.22.0 (trigger=skill; tool=cursor; client=vscode; model=opus-4.7) jfrog-cli-go/2.120.0") resetExecutionContextForTest(t) commandName := "rt_download" @@ -1118,7 +1118,7 @@ func TestAgentContextEndToEnd(t *testing.T) { } } -func TestDetectAiTrigger(t *testing.T) { +func TestDetectTrigger(t *testing.T) { cases := []struct { ua string want string @@ -1132,8 +1132,9 @@ func TestDetectAiTrigger(t *testing.T) { {"", ""}, } for _, tc := range cases { - if got := detectAiTrigger(tc.ua); got != tc.want { - t.Errorf("detectAiTrigger(%q)=%q want %q", tc.ua, got, tc.want) + t.Setenv(envUserAgent, tc.ua) + if got := detectTrigger(); got != tc.want { + t.Errorf("detectTrigger() with %q=%q want %q", tc.ua, got, tc.want) } } } From 9d9d7cf3bf2d4a9b6e3ef3bb595d12cfa361080a Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 13 Aug 2026 17:20:21 +0300 Subject: [PATCH 4/4] Drop ai_trigger; skill/hook path stays in product_version Path under the jfrog-skills plugin is encoded by producers as -skill / -hook on the first UA product token. --- common/commands/command.go | 1 - common/commands/execution_context.go | 17 ---------- common/commands/execution_context_test.go | 20 ------------ common/commands/metrics_collector.go | 2 -- common/commands/metrics_collector_test.go | 32 +------------------ utils/metrics/metrics.go | 7 ++-- .../usage/visibility/commands_count_metric.go | 3 -- .../visibility/commands_count_metric_test.go | 17 ---------- 8 files changed, 4 insertions(+), 95 deletions(-) diff --git a/common/commands/command.go b/common/commands/command.go index 81ff08db9..85d5036e2 100644 --- a/common/commands/command.go +++ b/common/commands/command.go @@ -152,7 +152,6 @@ func reportUsageToVisibilitySystem(commandName string, serverDetails *config.Ser Agent: metricsData.Agent, Client: metricsData.Client, Model: metricsData.Model, - Trigger: metricsData.Trigger, IsInteractive: metricsData.IsInteractive, PackageAlias: metricsData.PackageAlias, PackageManager: metricsData.PackageManager, diff --git a/common/commands/execution_context.go b/common/commands/execution_context.go index 8ee33d24e..4fbcf98db 100644 --- a/common/commands/execution_context.go +++ b/common/commands/execution_context.go @@ -2,7 +2,6 @@ package commands import ( "os" - "regexp" "strings" "sync" @@ -29,15 +28,8 @@ type ExecutionContext struct { // Model: model slug (JFROG_CLI_AI_MODEL) — "opus-4.7". Model string - - // Trigger: wrapper invocation type from JFROG_CLI_USER_AGENT — "skill" or "hook". - Trigger string } -const envUserAgent = "JFROG_CLI_USER_AGENT" - -var aiTriggerFromUA = regexp.MustCompile(`(?:^|[;(]\s*)trigger=(skill|hook)(?:\s*[;)]|$)`) - // agentDetector maps an agent name to env signals that prove the agent // invoked the CLI. Envs match on any non-empty value; EnvEquals requires an // exact value (used when a var is shared with non-agent hosts). @@ -191,7 +183,6 @@ func computeExecutionContext() ExecutionContext { if ec.IsAgent { ec.Client = detectClient() ec.Model = detectModel() - ec.Trigger = detectTrigger() } return ec } @@ -204,14 +195,6 @@ func detectClient() string { return sanitizeToken(os.Getenv("TERM_PROGRAM")) } -func detectTrigger() string { - match := aiTriggerFromUA.FindStringSubmatch(os.Getenv(envUserAgent)) - if len(match) < 2 { - return "" - } - return match[1] -} - // maxTokenLen caps sanitized identity tokens so a pathological env value cannot // inflate User-Agent / metrics payloads. Excess is truncated after filtering. const maxTokenLen = 64 diff --git a/common/commands/execution_context_test.go b/common/commands/execution_context_test.go index 12a121d2c..821d3fed9 100644 --- a/common/commands/execution_context_test.go +++ b/common/commands/execution_context_test.go @@ -223,7 +223,6 @@ func clearAgentEnvVars(t *testing.T) { t.Setenv("ROO_CODE_IPC_SOCKET_PATH", "") t.Setenv("TERM_PROGRAM", "") t.Setenv("JFROG_CLI_AI_MODEL", "") - t.Setenv(envUserAgent, "") } func TestDetectExecutionContext_ModelAgentOnly(t *testing.T) { @@ -277,22 +276,3 @@ func TestDetectExecutionContext_ClientSkippedForHuman(t *testing.T) { assert.False(t, ec.IsAgent) assert.Equal(t, "", ec.Client) } - -func TestDetectExecutionContext_TriggerAgentOnly(t *testing.T) { - resetExecutionContextForTest(t) - clearAgentEnvVars(t) - t.Setenv("CLAUDE_CODE_CHILD_SESSION", "1") - t.Setenv(envUserAgent, "jfrog-skills/0.22.0 (trigger=hook) jfrog-cli-go/2.120.0") - - assert.Equal(t, "hook", DetectExecutionContext().Trigger) -} - -func TestDetectExecutionContext_TriggerSkippedForHuman(t *testing.T) { - resetExecutionContextForTest(t) - clearAgentEnvVars(t) - t.Setenv(envUserAgent, "jfrog-skills/0.22.0 (trigger=skill) jfrog-cli-go/2.120.0") - - ec := DetectExecutionContext() - assert.False(t, ec.IsAgent) - assert.Empty(t, ec.Trigger) -} diff --git a/common/commands/metrics_collector.go b/common/commands/metrics_collector.go index 6561f9f66..54e333f38 100644 --- a/common/commands/metrics_collector.go +++ b/common/commands/metrics_collector.go @@ -63,7 +63,6 @@ func CollectMetrics(commandName string, flags []string) { Agent: ec.Agent, Client: ec.Client, Model: ec.Model, - Trigger: ec.Trigger, IsInteractive: ec.IsInteractive, PackageAlias: pkgAliasTool != "", PackageManager: packageManager, @@ -92,7 +91,6 @@ func GetCollectedMetrics(commandName string) *MetricsData { Agent: metrics.Agent, Client: metrics.Client, Model: metrics.Model, - Trigger: metrics.Trigger, IsInteractive: metrics.IsInteractive, PackageAlias: metrics.PackageAlias, PackageManager: metrics.PackageManager, diff --git a/common/commands/metrics_collector_test.go b/common/commands/metrics_collector_test.go index 5b447f4f9..8a3728d40 100644 --- a/common/commands/metrics_collector_test.go +++ b/common/commands/metrics_collector_test.go @@ -1042,7 +1042,7 @@ func TestMetricsIntegrationFlow(t *testing.T) { } } -// TestAgentContextEndToEnd verifies that agent/is_agent/client/model/trigger/ +// TestAgentContextEndToEnd verifies that agent/is_agent/client/model/ // 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. @@ -1052,7 +1052,6 @@ func TestAgentContextEndToEnd(t *testing.T) { t.Setenv("CURSOR_AGENT", "1") t.Setenv("TERM_PROGRAM", "vscode") t.Setenv("JFROG_CLI_AI_MODEL", "opus-4.7") - t.Setenv(envUserAgent, "jfrog-skills/0.22.0 (trigger=skill; tool=cursor; client=vscode; model=opus-4.7) jfrog-cli-go/2.120.0") resetExecutionContextForTest(t) commandName := "rt_download" @@ -1070,10 +1069,6 @@ func TestAgentContextEndToEnd(t *testing.T) { if collected.Client != "vscode" || collected.Model != "opus-4.7" { t.Errorf("collected Client/Model wrong: Client=%q Model=%q", collected.Client, collected.Model) } - if collected.Trigger != "skill" { - t.Errorf("collected Trigger wrong: %q", collected.Trigger) - } - visibilityData := &visibility.MetricsData{ Flags: collected.Flags, Platform: collected.Platform, @@ -1085,7 +1080,6 @@ func TestAgentContextEndToEnd(t *testing.T) { Agent: collected.Agent, Client: collected.Client, Model: collected.Model, - Trigger: collected.Trigger, IsInteractive: collected.IsInteractive, PackageAlias: collected.PackageAlias, PackageManager: collected.PackageManager, @@ -1110,35 +1104,11 @@ func TestAgentContextEndToEnd(t *testing.T) { if !strings.Contains(wire, `"ai_model":"opus-4.7"`) { t.Errorf("wire JSON missing ai_model=opus-4.7: %s", wire) } - if !strings.Contains(wire, `"ai_trigger":"skill"`) { - t.Errorf("wire JSON missing ai_trigger=skill: %s", wire) - } if !strings.Contains(wire, `"is_interactive":`) { t.Errorf("wire JSON missing is_interactive: %s", wire) } } -func TestDetectTrigger(t *testing.T) { - cases := []struct { - ua string - want string - }{ - {"jfrog-skills/0.22.0 (trigger=skill; tool=cursor) jfrog-cli-go/2.120.0", "skill"}, - {"jfrog-skills/0.1.0 (trigger=hook) jfrog-cli-go/2.119.0", "hook"}, - {"jfrog-skills/0.9.0 (trigger=skill) jfrog-cli-go/2.120.0 ai-agent/cursor", "skill"}, - {"setup-jfrog-cli-github-action/5.1.0", ""}, - {"jfrog-cli-go/2.119.0 ai-agent/claude", ""}, - {"jfrog-skills/0.22.0 (trigger=evil) jfrog-cli-go/2.120.0", ""}, - {"", ""}, - } - for _, tc := range cases { - t.Setenv(envUserAgent, tc.ua) - if got := detectTrigger(); got != tc.want { - t.Errorf("detectTrigger() with %q=%q want %q", tc.ua, got, tc.want) - } - } -} - // TestExecWithPackageManager verifies that ExecWithPackageManager stamps the // package_manager label in the collected metrics before the command runs. func TestExecWithPackageManager(t *testing.T) { diff --git a/utils/metrics/metrics.go b/utils/metrics/metrics.go index ad50019ca..96a2497e0 100644 --- a/utils/metrics/metrics.go +++ b/utils/metrics/metrics.go @@ -10,10 +10,9 @@ type MetricsData struct { CISystem string `json:"ci_system,omitempty"` IsContainer bool `json:"is_container,omitempty"` IsAgent bool `json:"is_agent,omitempty"` - Agent string `json:"agent,omitempty"` // "cursor", "claude"; empty when not an agent - Client string `json:"ai_client,omitempty"` // host app (TERM_PROGRAM): "vscode", "zed" - Model string `json:"ai_model,omitempty"` // model slug: "opus-4.7" - Trigger string `json:"ai_trigger,omitempty"` // "skill" | "hook" from JFROG_CLI_USER_AGENT parens + Agent string `json:"agent,omitempty"` // "cursor", "claude"; empty when not an agent + Client string `json:"ai_client,omitempty"` // host app (TERM_PROGRAM): "vscode", "zed" + Model string `json:"ai_model,omitempty"` // model slug: "opus-4.7" IsInteractive bool `json:"is_interactive,omitempty"` PackageAlias bool `json:"package_alias,omitempty"` PackageManager string `json:"package_manager,omitempty"` diff --git a/utils/usage/visibility/commands_count_metric.go b/utils/usage/visibility/commands_count_metric.go index 7a4e05a46..53246a17e 100644 --- a/utils/usage/visibility/commands_count_metric.go +++ b/utils/usage/visibility/commands_count_metric.go @@ -32,7 +32,6 @@ type commandsCountLabels struct { Agent string `json:"agent,omitempty"` Client string `json:"ai_client,omitempty"` Model string `json:"ai_model,omitempty"` - Trigger string `json:"ai_trigger,omitempty"` IsInteractive string `json:"is_interactive,omitempty"` PackageAlias string `json:"package_alias,omitempty"` PackageManager string `json:"package_manager,omitempty"` @@ -93,8 +92,6 @@ func NewCommandsCountMetricWithEnhancedData(commandName string, metricsData *Met } else { labels.IsAgent = "false" } - // Path attribution (jfrog-skills/plugin): independent of IsAgent. - labels.Trigger = metricsData.Trigger if metricsData.IsInteractive { labels.IsInteractive = "true" } else { diff --git a/utils/usage/visibility/commands_count_metric_test.go b/utils/usage/visibility/commands_count_metric_test.go index 95728b0b7..8608a0ccd 100644 --- a/utils/usage/visibility/commands_count_metric_test.go +++ b/utils/usage/visibility/commands_count_metric_test.go @@ -97,7 +97,6 @@ func TestNewCommandsCountMetricWithEnhancedData(t *testing.T) { assert.Equal(t, "cursor", labels.Agent) assert.Equal(t, "vscode", labels.Client) assert.Equal(t, "opus-4.7", labels.Model) - assert.Empty(t, labels.Trigger) assert.Equal(t, "false", labels.IsInteractive) metricJSON, err := json.Marshal(metric) @@ -108,22 +107,6 @@ func TestNewCommandsCountMetricWithEnhancedData(t *testing.T) { assert.NotContains(t, wire, `"client":"`) } -func TestNewCommandsCountMetricWithAiTrigger(t *testing.T) { - metricsData := &MetricsData{ - IsAgent: true, - Agent: "cursor", - Trigger: "skill", - } - metric := NewCommandsCountMetricWithEnhancedData("rt_ping", metricsData) - labels, ok := metric.Labels.(*commandsCountLabels) - assert.True(t, ok) - assert.Equal(t, "skill", labels.Trigger) - - wire, err := json.Marshal(metric) - assert.NoError(t, err) - assert.Contains(t, string(wire), `"ai_trigger":"skill"`) -} - func TestNewCommandsCountMetricWithNilEnhancedData(t *testing.T) { commandName := "nil-enhanced-test-command"