From 29dff27e8cd1f273c88f5334e0e6d2f6dd7de462 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:26:44 +0000 Subject: [PATCH 1/2] Initial plan From 065a568784e90549a4a644d92d0a95f02d507c65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:35:31 +0000 Subject: [PATCH 2/2] Emit installed extension IDs and versions in telemetry for all commands Add new telemetry fields `extensions.installed.ids` and `extensions.installed.versions` to capture the list of installed extensions on every command span. This enables tracking which extensions users have installed, even for non-extension commands like `azd up` or `azd deploy`. Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/cmd/middleware/telemetry.go | 32 +++++++++++ cli/azd/cmd/middleware/telemetry_test.go | 70 +++++++++++++++++++++++ cli/azd/internal/tracing/fields/fields.go | 12 ++++ 3 files changed, 114 insertions(+) diff --git a/cli/azd/cmd/middleware/telemetry.go b/cli/azd/cmd/middleware/telemetry.go index d58ee722214..0a3dcf67348 100644 --- a/cli/azd/cmd/middleware/telemetry.go +++ b/cli/azd/cmd/middleware/telemetry.go @@ -7,6 +7,7 @@ import ( "context" "errors" "log" + "slices" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -97,6 +98,9 @@ func (m *TelemetryMiddleware) Run(ctx context.Context, next NextFn) (*actions.Ac span.SetAttributes(fields.PlatformTypeKey.String(string(platformConfig.Type))) } + // Emit installed extension IDs and versions for all commands + m.setInstalledExtensionsAttributes(span) + defer func() { // Include any usage attributes set span.SetAttributes(tracing.GetUsageAttributes()...) @@ -158,3 +162,31 @@ func (m *TelemetryMiddleware) extensionCmdInfo(extensionId string) (string, []st fullPath := strings.Join(append([]string{"azd", namespacePath}, commandPath...), " ") return events.GetCommandEventName(fullPath), commandFlags } + +// setInstalledExtensionsAttributes emits the list of installed extension IDs and versions as span attributes. +func (m *TelemetryMiddleware) setInstalledExtensionsAttributes(span tracing.Span) { + if m.extensionManager == nil { + return + } + + installed, err := m.extensionManager.ListInstalled() + if err != nil || len(installed) == 0 { + return + } + + ids := make([]string, 0, len(installed)) + for id := range installed { + ids = append(ids, id) + } + slices.Sort(ids) + + versions := make([]string, 0, len(installed)) + for _, id := range ids { + versions = append(versions, installed[id].Version) + } + + span.SetAttributes( + fields.ExtensionsInstalledIds.StringSlice(ids), + fields.ExtensionsInstalledVersions.StringSlice(versions), + ) +} diff --git a/cli/azd/cmd/middleware/telemetry_test.go b/cli/azd/cmd/middleware/telemetry_test.go index e0a04284bf0..1f66091b024 100644 --- a/cli/azd/cmd/middleware/telemetry_test.go +++ b/cli/azd/cmd/middleware/telemetry_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/azure/azure-dev/cli/azd/cmd/actions" + "github.com/azure/azure-dev/cli/azd/pkg/config" + "github.com/azure/azure-dev/cli/azd/pkg/extensions" "github.com/azure/azure-dev/cli/azd/pkg/lazy" "github.com/azure/azure-dev/cli/azd/pkg/platform" "github.com/azure/azure-dev/cli/azd/test/mocks" @@ -79,4 +81,72 @@ func Test_Telemetry_Run(t *testing.T) { "Context should be a different instance since telemetry creates a new context", ) }) + + t.Run("WithInstalledExtensions", func(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + // Set up installed extensions in config + userConfigManager := config.NewUserConfigManager(mockContext.ConfigManager) + userConfig, err := userConfigManager.Load() + require.NoError(t, err) + + installedExtensions := map[string]*extensions.Extension{ + "microsoft.azd.demo": { + Id: "microsoft.azd.demo", + Version: "0.5.0", + }, + "microsoft.azd.ai": { + Id: "microsoft.azd.ai", + Version: "1.2.0", + }, + } + err = userConfig.Set("extension.installed", installedExtensions) + require.NoError(t, err) + + lazyRunner := lazy.NewLazy(func() (*extensions.Runner, error) { + return nil, nil + }) + manager, err := extensions.NewManager(userConfigManager, nil, lazyRunner, mockContext.HttpClient) + require.NoError(t, err) + + options := &Options{ + CommandPath: "azd provision", + Name: "provision", + } + middleware := NewTelemetryMiddleware(options, lazyPlatformConfig, manager) + + ran := false + nextFn := func(ctx context.Context) (*actions.ActionResult, error) { + ran = true + return nil, nil + } + + _, _ = middleware.Run(*mockContext.Context, nextFn) + require.True(t, ran) + + // Verify that installed extensions were listed without error + installed, err := manager.ListInstalled() + require.NoError(t, err) + require.Equal(t, 2, len(installed)) + }) + + t.Run("WithNilExtensionManager", func(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + options := &Options{ + CommandPath: "azd provision", + Name: "provision", + } + middleware := NewTelemetryMiddleware(options, lazyPlatformConfig, nil) + + ran := false + nextFn := func(ctx context.Context) (*actions.ActionResult, error) { + ran = true + return nil, nil + } + + // Should not panic when extensionManager is nil + _, _ = middleware.Run(*mockContext.Context, nextFn) + require.True(t, ran) + }) } diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index f347c5788d6..db18481101f 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -580,4 +580,16 @@ var ( Classification: SystemMetadata, Purpose: FeatureInsight, } + // The list of installed extension identifiers. + ExtensionsInstalledIds = AttributeKey{ + Key: attribute.Key("extensions.installed.ids"), + Classification: SystemMetadata, + Purpose: FeatureInsight, + } + // The list of installed extension versions. + ExtensionsInstalledVersions = AttributeKey{ + Key: attribute.Key("extensions.installed.versions"), + Classification: SystemMetadata, + Purpose: FeatureInsight, + } )