feat(coderd/telemetry): track AI task usage (#19418)

Relates to https://github.com/coder/internal/issues/868
This commit is contained in:
Cian Johnston
2025-08-20 17:59:09 +01:00
committed by GitHub
parent 5b1e809862
commit 9ad124d489
2 changed files with 79 additions and 10 deletions
+10 -1
View File
@@ -768,7 +768,7 @@ func ConvertWorkspace(workspace database.Workspace) Workspace {
// ConvertWorkspaceBuild anonymizes a workspace build.
func ConvertWorkspaceBuild(build database.WorkspaceBuild) WorkspaceBuild {
return WorkspaceBuild{
wb := WorkspaceBuild{
ID: build.ID,
CreatedAt: build.CreatedAt,
WorkspaceID: build.WorkspaceID,
@@ -777,6 +777,10 @@ func ConvertWorkspaceBuild(build database.WorkspaceBuild) WorkspaceBuild {
// #nosec G115 - Safe conversion as build numbers are expected to be positive and within uint32 range
BuildNumber: uint32(build.BuildNumber),
}
if build.HasAITask.Valid {
wb.HasAITask = ptr.Ref(build.HasAITask.Bool)
}
return wb
}
// ConvertProvisionerJob anonymizes a provisioner job.
@@ -1105,6 +1109,9 @@ func ConvertTemplateVersion(version database.TemplateVersion) TemplateVersion {
if version.SourceExampleID.Valid {
snapVersion.SourceExampleID = &version.SourceExampleID.String
}
if version.HasAITask.Valid {
snapVersion.HasAITask = ptr.Ref(version.HasAITask.Bool)
}
return snapVersion
}
@@ -1357,6 +1364,7 @@ type WorkspaceBuild struct {
TemplateVersionID uuid.UUID `json:"template_version_id"`
JobID uuid.UUID `json:"job_id"`
BuildNumber uint32 `json:"build_number"`
HasAITask *bool `json:"has_ai_task"`
}
type Workspace struct {
@@ -1404,6 +1412,7 @@ type TemplateVersion struct {
OrganizationID uuid.UUID `json:"organization_id"`
JobID uuid.UUID `json:"job_id"`
SourceExampleID *string `json:"source_example_id,omitempty"`
HasAITask *bool `json:"has_ai_task"`
}
type ProvisionerJob struct {
+69 -9
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"slices"
"sort"
"testing"
"time"
@@ -105,6 +106,52 @@ func TestTelemetry(t *testing.T) {
OpenIn: database.WorkspaceAppOpenInSlimWindow,
AgentID: wsagent.ID,
})
taskJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
Provisioner: database.ProvisionerTypeTerraform,
StorageMethod: database.ProvisionerStorageMethodFile,
Type: database.ProvisionerJobTypeTemplateVersionDryRun,
OrganizationID: org.ID,
})
taskTpl := dbgen.Template(t, db, database.Template{
Provisioner: database.ProvisionerTypeTerraform,
OrganizationID: org.ID,
CreatedBy: user.ID,
})
taskTV := dbgen.TemplateVersion(t, db, database.TemplateVersion{
OrganizationID: org.ID,
TemplateID: uuid.NullUUID{UUID: taskTpl.ID, Valid: true},
CreatedBy: user.ID,
JobID: taskJob.ID,
HasAITask: sql.NullBool{Bool: true, Valid: true},
})
taskWs := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
OrganizationID: org.ID,
TemplateID: taskTpl.ID,
})
taskWsResource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: taskJob.ID,
})
taskWsAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: taskWsResource.ID,
})
taskWsApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{
SharingLevel: database.AppSharingLevelOwner,
Health: database.WorkspaceAppHealthDisabled,
OpenIn: database.WorkspaceAppOpenInSlimWindow,
AgentID: taskWsAgent.ID,
})
taskWB := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
Transition: database.WorkspaceTransitionStart,
Reason: database.BuildReasonAutostart,
WorkspaceID: taskWs.ID,
TemplateVersionID: tv.ID,
JobID: taskJob.ID,
HasAITask: sql.NullBool{Valid: true, Bool: true},
AITaskSidebarAppID: uuid.NullUUID{Valid: true, UUID: taskWsApp.ID},
})
group := dbgen.Group(t, db, database.Group{
OrganizationID: org.ID,
})
@@ -148,19 +195,19 @@ func TestTelemetry(t *testing.T) {
})
_, snapshot := collectSnapshot(ctx, t, db, nil)
require.Len(t, snapshot.ProvisionerJobs, 1)
require.Len(t, snapshot.ProvisionerJobs, 2)
require.Len(t, snapshot.Licenses, 1)
require.Len(t, snapshot.Templates, 1)
require.Len(t, snapshot.TemplateVersions, 2)
require.Len(t, snapshot.Templates, 2)
require.Len(t, snapshot.TemplateVersions, 3)
require.Len(t, snapshot.Users, 1)
require.Len(t, snapshot.Groups, 2)
// 1 member in the everyone group + 1 member in the custom group
require.Len(t, snapshot.GroupMembers, 2)
require.Len(t, snapshot.Workspaces, 1)
require.Len(t, snapshot.WorkspaceApps, 1)
require.Len(t, snapshot.WorkspaceAgents, 1)
require.Len(t, snapshot.WorkspaceBuilds, 1)
require.Len(t, snapshot.WorkspaceResources, 1)
require.Len(t, snapshot.Workspaces, 2)
require.Len(t, snapshot.WorkspaceApps, 2)
require.Len(t, snapshot.WorkspaceAgents, 2)
require.Len(t, snapshot.WorkspaceBuilds, 2)
require.Len(t, snapshot.WorkspaceResources, 2)
require.Len(t, snapshot.WorkspaceAgentStats, 1)
require.Len(t, snapshot.WorkspaceProxies, 1)
require.Len(t, snapshot.WorkspaceModules, 1)
@@ -169,11 +216,24 @@ func TestTelemetry(t *testing.T) {
require.Len(t, snapshot.TelemetryItems, 2)
require.Len(t, snapshot.WorkspaceAgentMemoryResourceMonitors, 1)
require.Len(t, snapshot.WorkspaceAgentVolumeResourceMonitors, 1)
wsa := snapshot.WorkspaceAgents[0]
wsa := snapshot.WorkspaceAgents[1]
require.Len(t, wsa.Subsystems, 2)
require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0])
require.Equal(t, string(database.WorkspaceAgentSubsystemExectrace), wsa.Subsystems[1])
require.True(t, slices.ContainsFunc(snapshot.TemplateVersions, func(ttv telemetry.TemplateVersion) bool {
if ttv.ID != taskTV.ID {
return false
}
return assert.NotNil(t, ttv.HasAITask) && assert.True(t, *ttv.HasAITask)
}))
require.True(t, slices.ContainsFunc(snapshot.WorkspaceBuilds, func(twb telemetry.WorkspaceBuild) bool {
if twb.ID != taskWB.ID {
return false
}
return assert.NotNil(t, twb.HasAITask) && assert.True(t, *twb.HasAITask)
}))
tvs := snapshot.TemplateVersions
sort.Slice(tvs, func(i, j int) bool {
// Sort by SourceExampleID presence (non-nil comes before nil)