Skip to content

Commit df3091b

Browse files
authored
RTECO-1021 - Enhance metrics visibility by including package manager context (#1569)
1 parent edba164 commit df3091b

5 files changed

Lines changed: 232 additions & 4 deletions

File tree

common/commands/command.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ func Exec(command Command) error {
4343
return err
4444
}
4545

46+
// ExecWithPackageManager tags the command with the given package manager
47+
// name (e.g. "npm", "go", "docker") for telemetry, then runs Exec.
48+
// The package manager context is consumed by CollectMetrics inside Exec.
49+
func ExecWithPackageManager(command Command, packageManager string) error {
50+
SetPackageManagerContext(packageManager)
51+
return Exec(command)
52+
}
53+
4654
// ExecAndThenReportUsage runs the command and then triggers a usage report.
4755
// Used for commands which don't have the full server details before execution.
4856
// For example: oidc exchange command, which will get access token only after execution.

common/commands/metrics_collector.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ type MetricsData = metrics.MetricsData
1414

1515
// metricsCollector provides thread-safe collection and storage of command metrics
1616
type metricsCollector struct {
17-
mu sync.RWMutex
18-
metricsData map[string]*MetricsData
19-
packageAliasContext string
17+
mu sync.RWMutex
18+
metricsData map[string]*MetricsData
19+
packageAliasContext string
20+
packageManagerContext string
2021
}
2122

2223
var contextFlags []string
@@ -38,6 +39,16 @@ func CollectMetrics(commandName string, flags []string) {
3839

3940
pkgAliasTool := globalMetricsCollector.packageAliasContext
4041
globalMetricsCollector.packageAliasContext = ""
42+
pkgManagerTool := globalMetricsCollector.packageManagerContext
43+
globalMetricsCollector.packageManagerContext = ""
44+
45+
// Alias path wins: if the command was dispatched via a package alias, that
46+
// tool name is the source of truth for PackageManager. Otherwise the
47+
// explicit SetPackageManagerContext value (set by buildtool actions) is used.
48+
packageManager := pkgAliasTool
49+
if packageManager == "" {
50+
packageManager = pkgManagerTool
51+
}
4152

4253
globalMetricsCollector.metricsData[commandName] = &MetricsData{
4354
Flags: flags,
@@ -50,7 +61,7 @@ func CollectMetrics(commandName string, flags []string) {
5061
Agent: ec.Agent,
5162
IsInteractive: ec.IsInteractive,
5263
PackageAlias: pkgAliasTool != "",
53-
PackageManager: pkgAliasTool,
64+
PackageManager: packageManager,
5465
}
5566
}
5667

@@ -184,3 +195,13 @@ func SetPackageAliasContext(tool string) {
184195
defer globalMetricsCollector.mu.Unlock()
185196
globalMetricsCollector.packageAliasContext = tool
186197
}
198+
199+
// SetPackageManagerContext records the package manager associated with the
200+
// current CLI command (e.g. "npm", "go", "docker") for direct invocations
201+
// such as `jf npm install`. CollectMetrics reads and clears this value
202+
// automatically. SetPackageAliasContext takes precedence when both are set.
203+
func SetPackageManagerContext(tool string) {
204+
globalMetricsCollector.mu.Lock()
205+
defer globalMetricsCollector.mu.Unlock()
206+
globalMetricsCollector.packageManagerContext = tool
207+
}

common/commands/metrics_collector_test.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,7 @@ func TestSetPackageAliasContext(t *testing.T) {
889889
func TestCollectMetrics_WithoutPackageAlias(t *testing.T) {
890890
ClearAllMetrics()
891891
globalMetricsCollector.packageAliasContext = ""
892+
globalMetricsCollector.packageManagerContext = ""
892893

893894
commandName := "rt_upload"
894895
CollectMetrics(commandName, nil)
@@ -905,6 +906,62 @@ func TestCollectMetrics_WithoutPackageAlias(t *testing.T) {
905906
}
906907
}
907908

909+
func TestSetPackageManagerContext(t *testing.T) {
910+
ClearAllMetrics()
911+
globalMetricsCollector.packageAliasContext = ""
912+
globalMetricsCollector.packageManagerContext = ""
913+
914+
commandName := "npm_install"
915+
flags := []string{"save-dev"}
916+
917+
// Set the package manager context (simulates a direct `jf npm install` invocation).
918+
SetPackageManagerContext("npm")
919+
CollectMetrics(commandName, flags)
920+
921+
metrics := GetCollectedMetrics(commandName)
922+
if metrics == nil {
923+
t.Fatal("Expected metrics to be collected")
924+
}
925+
if metrics.PackageAlias {
926+
t.Error("Expected PackageAlias to be false for direct invocation")
927+
}
928+
if metrics.PackageManager != "npm" {
929+
t.Errorf("Expected PackageManager 'npm', got '%s'", metrics.PackageManager)
930+
}
931+
932+
// packageManagerContext must be cleared after CollectMetrics.
933+
if globalMetricsCollector.packageManagerContext != "" {
934+
t.Errorf("Expected packageManagerContext to be cleared, got '%s'", globalMetricsCollector.packageManagerContext)
935+
}
936+
}
937+
938+
func TestSetPackageManagerContext_AliasWinsWhenBothSet(t *testing.T) {
939+
ClearAllMetrics()
940+
globalMetricsCollector.packageAliasContext = ""
941+
globalMetricsCollector.packageManagerContext = ""
942+
943+
// Both contexts set: alias must take precedence to preserve existing
944+
// alias-dispatch semantics. PackageAlias is true and PackageManager uses
945+
// the alias tool name.
946+
SetPackageAliasContext("npm")
947+
SetPackageManagerContext("pip")
948+
CollectMetrics("npm_install", nil)
949+
950+
metrics := GetCollectedMetrics("npm_install")
951+
if metrics == nil {
952+
t.Fatal("Expected metrics to be collected")
953+
}
954+
if !metrics.PackageAlias {
955+
t.Error("Expected PackageAlias to be true when alias context set")
956+
}
957+
if metrics.PackageManager != "npm" {
958+
t.Errorf("Expected PackageManager 'npm' (alias wins), got '%s'", metrics.PackageManager)
959+
}
960+
if globalMetricsCollector.packageAliasContext != "" || globalMetricsCollector.packageManagerContext != "" {
961+
t.Error("Expected both context fields to be cleared after CollectMetrics")
962+
}
963+
}
964+
908965
func TestMetricsIntegrationFlow(t *testing.T) {
909966
// Clear any existing metrics
910967
globalMetricsCollector.mu.Lock()
@@ -1039,3 +1096,118 @@ func TestAgentContextEndToEnd(t *testing.T) {
10391096
t.Errorf("wire JSON missing is_interactive: %s", wire)
10401097
}
10411098
}
1099+
1100+
// TestExecWithPackageManager verifies that ExecWithPackageManager stamps the
1101+
// package_manager label in the collected metrics before the command runs.
1102+
func TestExecWithPackageManager(t *testing.T) {
1103+
ClearAllMetrics()
1104+
globalMetricsCollector.packageAliasContext = ""
1105+
globalMetricsCollector.packageManagerContext = ""
1106+
1107+
commandName := "rt_go"
1108+
cmd := &MockCommand{name: commandName}
1109+
1110+
if err := ExecWithPackageManager(cmd, "go"); err != nil {
1111+
t.Fatalf("ExecWithPackageManager returned unexpected error: %v", err)
1112+
}
1113+
1114+
m := GetCollectedMetrics(commandName)
1115+
if m == nil {
1116+
t.Fatal("expected metrics to be collected after ExecWithPackageManager")
1117+
}
1118+
if m.PackageManager != "go" {
1119+
t.Errorf("PackageManager: got %q want %q", m.PackageManager, "go")
1120+
}
1121+
if m.PackageAlias {
1122+
t.Errorf("PackageAlias should be false for direct ExecWithPackageManager call, got true")
1123+
}
1124+
}
1125+
1126+
// TestExecWithPackageManager_AliasWins verifies that when both alias context and
1127+
// package manager are set, alias wins (is_package_alias=true, package_manager=alias value).
1128+
func TestExecWithPackageManager_AliasWins(t *testing.T) {
1129+
ClearAllMetrics()
1130+
globalMetricsCollector.packageAliasContext = ""
1131+
globalMetricsCollector.packageManagerContext = ""
1132+
1133+
commandName := "rt_npm_install"
1134+
cmd := &MockCommand{name: commandName}
1135+
1136+
// Simulate alias path setting alias context first
1137+
SetPackageAliasContext("npm")
1138+
// Then ExecWithPackageManager also called (shouldn't override alias)
1139+
if err := ExecWithPackageManager(cmd, "npm"); err != nil {
1140+
t.Fatalf("ExecWithPackageManager returned unexpected error: %v", err)
1141+
}
1142+
1143+
m := GetCollectedMetrics(commandName)
1144+
if m == nil {
1145+
t.Fatal("expected metrics to be collected")
1146+
}
1147+
if !m.PackageAlias {
1148+
t.Error("PackageAlias should be true when alias context was set")
1149+
}
1150+
if m.PackageManager != "npm" {
1151+
t.Errorf("PackageManager: got %q want %q", m.PackageManager, "npm")
1152+
}
1153+
}
1154+
1155+
// TestExec_NonPMCommand_NoPackageManagerFields verifies that running a plain
1156+
// non-PM command via Exec (e.g. rt_ping, rt_upload) does NOT set package_manager
1157+
// or package_alias in the collected metrics.
1158+
func TestExec_NonPMCommand_NoPackageManagerFields(t *testing.T) {
1159+
ClearAllMetrics()
1160+
globalMetricsCollector.packageAliasContext = ""
1161+
globalMetricsCollector.packageManagerContext = ""
1162+
1163+
cmd := &MockCommand{name: "rt_ping"}
1164+
if err := Exec(cmd); err != nil {
1165+
t.Fatalf("Exec returned unexpected error: %v", err)
1166+
}
1167+
1168+
m := GetCollectedMetrics("rt_ping")
1169+
if m == nil {
1170+
t.Fatal("expected metrics to be collected")
1171+
}
1172+
if m.PackageAlias {
1173+
t.Errorf("PackageAlias should be false for non-PM command, got true")
1174+
}
1175+
if m.PackageManager != "" {
1176+
t.Errorf("PackageManager should be empty for non-PM command, got %q", m.PackageManager)
1177+
}
1178+
}
1179+
1180+
// TestExec_NonPMAfterPM_NoLeakage verifies that running a non-PM command after
1181+
// a PM command does NOT inherit the previous command's package_manager.
1182+
// This guards against context leakage between sequential command executions.
1183+
func TestExec_NonPMAfterPM_NoLeakage(t *testing.T) {
1184+
ClearAllMetrics()
1185+
globalMetricsCollector.packageAliasContext = ""
1186+
globalMetricsCollector.packageManagerContext = ""
1187+
1188+
// First: run a PM command
1189+
pmCmd := &MockCommand{name: "rt_go"}
1190+
if err := ExecWithPackageManager(pmCmd, "go"); err != nil {
1191+
t.Fatalf("ExecWithPackageManager: %v", err)
1192+
}
1193+
pmMetrics := GetCollectedMetrics("rt_go")
1194+
if pmMetrics == nil || pmMetrics.PackageManager != "go" {
1195+
t.Fatalf("PM command should have PackageManager=go, got: %+v", pmMetrics)
1196+
}
1197+
1198+
// Second: run a non-PM command immediately after
1199+
nonPMCmd := &MockCommand{name: "rt_ping"}
1200+
if err := Exec(nonPMCmd); err != nil {
1201+
t.Fatalf("Exec: %v", err)
1202+
}
1203+
nonPMMetrics := GetCollectedMetrics("rt_ping")
1204+
if nonPMMetrics == nil {
1205+
t.Fatal("expected metrics to be collected for non-PM command")
1206+
}
1207+
if nonPMMetrics.PackageManager != "" {
1208+
t.Errorf("non-PM command leaked package_manager=%q from previous PM command", nonPMMetrics.PackageManager)
1209+
}
1210+
if nonPMMetrics.PackageAlias {
1211+
t.Errorf("non-PM command leaked package_alias=true from previous PM command")
1212+
}
1213+
}

utils/usage/visibility/commands_count_metric.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ func NewCommandsCountMetricWithEnhancedData(commandName string, metricsData *Met
9595
}
9696
if metricsData.PackageAlias {
9797
labels.PackageAlias = "true"
98+
}
99+
if metricsData.PackageManager != "" {
98100
labels.PackageManager = metricsData.PackageManager
99101
}
100102
}

utils/usage/visibility/commands_count_metric_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,28 @@ func TestNewCommandsCountMetricWithoutPackageAlias(t *testing.T) {
308308
assert.NotContains(t, string(metricJSON), `"package_alias"`)
309309
assert.NotContains(t, string(metricJSON), `"package_manager"`)
310310
}
311+
312+
// TestNewCommandsCountMetric_PackageManagerWithoutAlias verifies that
313+
// package_manager is emitted for direct (non-alias) buildtool invocations
314+
// while package_alias remains absent from the wire payload.
315+
func TestNewCommandsCountMetric_PackageManagerWithoutAlias(t *testing.T) {
316+
commandName := "npm_install"
317+
318+
metricsData := &MetricsData{
319+
Flags: []string{"save-dev"},
320+
PackageAlias: false,
321+
PackageManager: "npm",
322+
}
323+
324+
metric := NewCommandsCountMetricWithEnhancedData(commandName, metricsData)
325+
326+
labels, ok := metric.Labels.(*commandsCountLabels)
327+
assert.True(t, ok, "Expected labels to be of type commandsCountLabels")
328+
assert.Empty(t, labels.PackageAlias)
329+
assert.Equal(t, "npm", labels.PackageManager)
330+
331+
metricJSON, err := json.Marshal(metric)
332+
assert.NoError(t, err)
333+
assert.NotContains(t, string(metricJSON), `"package_alias"`)
334+
assert.Contains(t, string(metricJSON), `"package_manager":"npm"`)
335+
}

0 commit comments

Comments
 (0)