diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index e00ff31b96..713cb53b6d 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -1561,7 +1561,7 @@ func AIBridgeToolUsage(t testing.TB, db database.Store, seed database.InsertAIBr return toolUsage } -func Task(t testing.TB, db database.Store, orig database.TaskTable) database.TaskTable { +func Task(t testing.TB, db database.Store, orig database.TaskTable) database.Task { t.Helper() parameters := orig.TemplateParameters @@ -1581,7 +1581,12 @@ func Task(t testing.TB, db database.Store, orig database.TaskTable) database.Tas }) require.NoError(t, err, "failed to insert task") - return task + // Return the Task from the view instead of the TaskTable + fetched, err := db.GetTaskByID(genCtx, task.ID) + require.NoError(t, err, "failed to fetch task") + require.Equal(t, task.ID, fetched.ID) + + return fetched } func TaskWorkspaceApp(t testing.TB, db database.Store, orig database.TaskWorkspaceApp) database.TaskWorkspaceApp { diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index aca6156904..5c894f98e9 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -6709,7 +6709,7 @@ func TestTasksWithStatusView(t *testing.T) { buildTransition database.WorkspaceTransition, agentState database.WorkspaceAgentLifecycleState, appHealths []database.WorkspaceAppHealth, - ) database.TaskTable { + ) database.Task { t.Helper() template := dbgen.Template(t, db, database.Template{ @@ -7455,7 +7455,7 @@ func TestListTasks(t *testing.T) { }) // Helper function to create a task - createTask := func(orgID, ownerID uuid.UUID) database.TaskTable { + createTask := func(orgID, ownerID uuid.UUID) database.Task { ws := dbgen.Workspace(t, db, database.WorkspaceTable{ OrganizationID: orgID, OwnerID: ownerID, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 0cfb83faa4..6cd490190b 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -3003,9 +3003,9 @@ func TestCompleteJob(t *testing.T) { OwnerID: user.ID, OrganizationID: pd.OrganizationID, }) - var taskTable database.TaskTable + var genTask database.Task if tc.isTask { - taskTable = dbgen.Task(t, db, database.TaskTable{ + genTask = dbgen.Task(t, db, database.TaskTable{ OwnerID: user.ID, OrganizationID: pd.OrganizationID, WorkspaceID: uuid.NullUUID{UUID: workspaceTable.ID, Valid: true}, @@ -3081,7 +3081,7 @@ func TestCompleteJob(t *testing.T) { require.Equal(t, tc.expectHasAiTask, build.HasAITask.Bool) if tc.isTask { - task, err := db.GetTaskByID(ctx, taskTable.ID) + task, err := db.GetTaskByID(ctx, genTask.ID) require.NoError(t, err) require.Equal(t, tc.expectTaskStatus, task.Status) } diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 75b432022b..37e41fcd70 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -730,6 +730,19 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + dbTasks, err := r.options.Database.ListTasks(ctx, database.ListTasksParams{ + OwnerID: uuid.Nil, + OrganizationID: uuid.Nil, + }) + if err != nil { + return err + } + for _, dbTask := range dbTasks { + snapshot.Tasks = append(snapshot.Tasks, ConvertTask(dbTask)) + } + return nil + }) err := eg.Wait() if err != nil { @@ -1205,6 +1218,7 @@ type Snapshot struct { Workspaces []Workspace `json:"workspaces"` NetworkEvents []NetworkEvent `json:"network_events"` Organizations []Organization `json:"organizations"` + Tasks []Task `json:"tasks"` TelemetryItems []TelemetryItem `json:"telemetry_items"` UserTailnetConnections []UserTailnetConnection `json:"user_tailnet_connections"` PrebuiltWorkspaces []PrebuiltWorkspace `json:"prebuilt_workspaces"` @@ -1753,6 +1767,52 @@ type Organization struct { CreatedAt time.Time `json:"created_at"` } +type Task struct { + ID string `json:"id"` + OrganizationID string `json:"organization_id"` + OwnerID string `json:"owner_id"` + Name string `json:"name"` + WorkspaceID *string `json:"workspace_id"` + WorkspaceBuildNumber *int64 `json:"workspace_build_number"` + WorkspaceAgentID *string `json:"workspace_agent_id"` + WorkspaceAppID *string `json:"workspace_app_id"` + TemplateVersionID string `json:"template_version_id"` + PromptHash string `json:"prompt_hash"` // Prompt is hashed for privacy. + CreatedAt time.Time `json:"created_at"` + Status string `json:"status"` +} + +// ConvertTask anonymizes a Task. +func ConvertTask(task database.Task) Task { + t := &Task{ + ID: task.ID.String(), + OrganizationID: task.OrganizationID.String(), + OwnerID: task.OwnerID.String(), + Name: task.Name, + WorkspaceID: nil, + WorkspaceBuildNumber: nil, + WorkspaceAgentID: nil, + WorkspaceAppID: nil, + TemplateVersionID: task.TemplateVersionID.String(), + PromptHash: fmt.Sprintf("%x", sha256.Sum256([]byte(task.Prompt))), + CreatedAt: task.CreatedAt, + Status: string(task.Status), + } + if task.WorkspaceID.Valid { + t.WorkspaceID = ptr.Ref(task.WorkspaceID.UUID.String()) + } + if task.WorkspaceBuildNumber.Valid { + t.WorkspaceBuildNumber = ptr.Ref(int64(task.WorkspaceBuildNumber.Int32)) + } + if task.WorkspaceAgentID.Valid { + t.WorkspaceAgentID = ptr.Ref(task.WorkspaceAgentID.UUID.String()) + } + if task.WorkspaceAppID.Valid { + t.WorkspaceAppID = ptr.Ref(task.WorkspaceAppID.UUID.String()) + } + return *t +} + type telemetryItemKey string // The comment below gets rid of the warning that the name "TelemetryItemKey" has diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 5508a7d881..a48bdee299 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -151,6 +151,20 @@ func TestTelemetry(t *testing.T) { HasAITask: sql.NullBool{Valid: true, Bool: true}, AITaskSidebarAppID: uuid.NullUUID{Valid: true, UUID: taskWsApp.ID}, }) + task := dbgen.Task(t, db, database.TaskTable{ + OwnerID: user.ID, + OrganizationID: org.ID, + WorkspaceID: uuid.NullUUID{Valid: true, UUID: taskWs.ID}, + TemplateVersionID: taskTV.ID, + Prompt: "example prompt", + TemplateParameters: json.RawMessage(`{"foo": "bar"}`), + }) + taskWA := dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{ + TaskID: task.ID, + WorkspaceAgentID: uuid.NullUUID{Valid: true, UUID: taskWsAgent.ID}, + WorkspaceAppID: uuid.NullUUID{Valid: true, UUID: taskWsApp.ID}, + WorkspaceBuildNumber: taskWB.BuildNumber, + }) group := dbgen.Group(t, db, database.Group{ OrganizationID: org.ID, @@ -220,6 +234,22 @@ func TestTelemetry(t *testing.T) { 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.Len(t, snapshot.Tasks, 1) + for _, snapTask := range snapshot.Tasks { + assert.Equal(t, task.ID.String(), snapTask.ID) + assert.Equal(t, task.OrganizationID.String(), snapTask.OrganizationID) + assert.Equal(t, task.OwnerID.String(), snapTask.OwnerID) + assert.Equal(t, task.Name, snapTask.Name) + if assert.True(t, task.WorkspaceID.Valid) { + assert.Equal(t, task.WorkspaceID.UUID.String(), *snapTask.WorkspaceID) + } + assert.EqualValues(t, taskWA.WorkspaceBuildNumber, *snapTask.WorkspaceBuildNumber) + assert.Equal(t, taskWA.WorkspaceAgentID.UUID.String(), *snapTask.WorkspaceAgentID) + assert.Equal(t, taskWA.WorkspaceAppID.UUID.String(), *snapTask.WorkspaceAppID) + assert.Equal(t, task.TemplateVersionID.String(), snapTask.TemplateVersionID) + assert.Equal(t, "e196fe22e61cfa32d8c38749e0ce348108bb4cae29e2c36cdcce7e77faa9eb5f", snapTask.PromptHash) + assert.Equal(t, task.CreatedAt.UTC(), snapTask.CreatedAt.UTC()) + } require.True(t, slices.ContainsFunc(snapshot.TemplateVersions, func(ttv telemetry.TemplateVersion) bool { if ttv.ID != taskTV.ID {