Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cli/azd/cmd/middleware/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"log"
"slices"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
Expand Down Expand Up @@ -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()...)
Expand Down Expand Up @@ -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),
)
}
70 changes: 70 additions & 0 deletions cli/azd/cmd/middleware/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
12 changes: 12 additions & 0 deletions cli/azd/internal/tracing/fields/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
)