From 9eef6ddc5c5f4fec92c13d1a282efbe1b6a3b031 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 28 May 2025 17:13:04 +0200 Subject: [PATCH 1/5] feat: add prebuilt workspaces telemetry Signed-off-by: Danny Kopping --- coderd/telemetry/telemetry.go | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 2d6789054856c..f62549bc44cac 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -683,6 +683,48 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + metrics, err := r.options.Database.GetPrebuildMetrics(ctx) + if err != nil { + return xerrors.Errorf("get prebuild metrics: %w", err) + } + + var totalCreated, totalFailed, totalClaimed int64 + for _, metric := range metrics { + totalCreated += metric.CreatedCount + totalFailed += metric.FailedCount + totalClaimed += metric.ClaimedCount + } + + snapshot.PrebuiltWorkspaces = make([]PrebuiltWorkspace, 0, 3) + now := dbtime.Now() + + if totalCreated > 0 { + snapshot.PrebuiltWorkspaces = append(snapshot.PrebuiltWorkspaces, PrebuiltWorkspace{ + ID: uuid.New(), + CreatedAt: now, + EventType: PrebuiltWorkspaceEventTypeCreated, + Count: int(totalCreated), + }) + } + if totalFailed > 0 { + snapshot.PrebuiltWorkspaces = append(snapshot.PrebuiltWorkspaces, PrebuiltWorkspace{ + ID: uuid.New(), + CreatedAt: now, + EventType: PrebuiltWorkspaceEventTypeFailed, + Count: int(totalFailed), + }) + } + if totalClaimed > 0 { + snapshot.PrebuiltWorkspaces = append(snapshot.PrebuiltWorkspaces, PrebuiltWorkspace{ + ID: uuid.New(), + CreatedAt: now, + EventType: PrebuiltWorkspaceEventTypeClaimed, + Count: int(totalClaimed), + }) + } + return nil + }) err := eg.Wait() if err != nil { @@ -1152,6 +1194,7 @@ type Snapshot struct { Organizations []Organization `json:"organizations"` TelemetryItems []TelemetryItem `json:"telemetry_items"` UserTailnetConnections []UserTailnetConnection `json:"user_tailnet_connections"` + PrebuiltWorkspaces []PrebuiltWorkspace `json:"prebuilt_workspaces"` } // Deployment contains information about the host running Coder. @@ -1724,6 +1767,21 @@ type UserTailnetConnection struct { CoderDesktopVersion *string `json:"coder_desktop_version"` } +type PrebuiltWorkspaceEventType string + +const ( + PrebuiltWorkspaceEventTypeCreated PrebuiltWorkspaceEventType = "created" + PrebuiltWorkspaceEventTypeFailed PrebuiltWorkspaceEventType = "failed" + PrebuiltWorkspaceEventTypeClaimed PrebuiltWorkspaceEventType = "claimed" +) + +type PrebuiltWorkspace struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + EventType PrebuiltWorkspaceEventType `json:"event_type"` + Count int `json:"count"` +} + type noopReporter struct{} func (*noopReporter) Report(_ *Snapshot) {} From 68d9a129eb3570fb312b66bd87b1b090cc4a4332 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 28 May 2025 18:20:38 +0200 Subject: [PATCH 2/5] chore: add tests Signed-off-by: Danny Kopping --- cli/server.go | 1 + coderd/telemetry/telemetry.go | 8 ++- coderd/telemetry/telemetry_test.go | 79 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/cli/server.go b/cli/server.go index 1794044bce48f..62b430cf22781 100644 --- a/cli/server.go +++ b/cli/server.go @@ -864,6 +864,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. BuiltinPostgres: builtinPostgres, DeploymentID: deploymentID, Database: options.Database, + Experiments: coderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value()), Logger: logger.Named("telemetry"), URL: vals.Telemetry.URL.Value(), Tunnel: tunnel != nil, diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index f62549bc44cac..5fa5bb3fbbd04 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -28,6 +28,7 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" "cdr.dev/slog" + "github.com/coder/coder/v2/buildinfo" clitelemetry "github.com/coder/coder/v2/cli/telemetry" "github.com/coder/coder/v2/coderd/database" @@ -47,7 +48,8 @@ type Options struct { Database database.Store Logger slog.Logger // URL is an endpoint to direct telemetry towards! - URL *url.URL + URL *url.URL + Experiments codersdk.Experiments DeploymentID string DeploymentConfig *codersdk.DeploymentValues @@ -684,6 +686,10 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { return nil }) eg.Go(func() error { + if !r.options.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds) { + return nil + } + metrics, err := r.options.Database.GetPrebuildMetrics(ctx) if err != nil { return xerrors.Errorf("get prebuild metrics: %w", err) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 7de4c98e07fa8..049d4977f109e 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -370,6 +370,85 @@ func TestTelemetryItem(t *testing.T) { require.Equal(t, item.Value, "new_value") } +func TestPrebuiltWorkspacesTelemetry(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db := dbmem.New() + + deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { + opts.Database = &mockDB{Store: opts.Database} + opts.Experiments = codersdk.Experiments{ + codersdk.ExperimentWorkspacePrebuilds, + } + return opts + }) + + require.NotNil(t, deployment) + require.NotNil(t, snapshot) + + require.Len(t, snapshot.PrebuiltWorkspaces, 3) + + eventCounts := make(map[telemetry.PrebuiltWorkspaceEventType]int) + for _, event := range snapshot.PrebuiltWorkspaces { + eventCounts[event.EventType] = event.Count + require.NotEqual(t, uuid.Nil, event.ID) + require.False(t, event.CreatedAt.IsZero()) + } + + require.Equal(t, 5, eventCounts[telemetry.PrebuiltWorkspaceEventTypeCreated]) + require.Equal(t, 2, eventCounts[telemetry.PrebuiltWorkspaceEventTypeFailed]) + require.Equal(t, 3, eventCounts[telemetry.PrebuiltWorkspaceEventTypeClaimed]) +} + +type mockDB struct { + database.Store +} + +func (m *mockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) { + return []database.GetPrebuildMetricsRow{ + { + TemplateName: "template1", + PresetName: "preset1", + OrganizationName: "org1", + CreatedCount: 3, + FailedCount: 1, + ClaimedCount: 2, + }, + { + TemplateName: "template2", + PresetName: "preset2", + OrganizationName: "org1", + CreatedCount: 2, + FailedCount: 1, + ClaimedCount: 1, + }, + }, nil +} + +func TestPrebuiltWorkspacesTelemetryEmpty(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + db := dbmem.New() + + deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { + opts.Database = &emptyMockDB{Store: opts.Database} + return opts + }) + + require.NotNil(t, deployment) + require.NotNil(t, snapshot) + + require.Len(t, snapshot.PrebuiltWorkspaces, 0) +} + +type emptyMockDB struct { + database.Store +} + +func (m *emptyMockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) { + return []database.GetPrebuildMetricsRow{}, nil +} + func TestShouldReportTelemetryDisabled(t *testing.T) { t.Parallel() // Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled | From 1bc61c0f5dd9843d548cc46478224d89f631f0b3 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 28 May 2025 18:38:40 +0200 Subject: [PATCH 3/5] chore: unify into table test Signed-off-by: Danny Kopping --- coderd/telemetry/telemetry_test.go | 98 +++++++++++++++++++----------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 049d4977f109e..0a48c7943cbf3 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -375,29 +375,73 @@ func TestPrebuiltWorkspacesTelemetry(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) db := dbmem.New() - deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { - opts.Database = &mockDB{Store: opts.Database} - opts.Experiments = codersdk.Experiments{ - codersdk.ExperimentWorkspacePrebuilds, - } - return opts - }) + cases := []struct { + name string + experimentEnabled bool + storeFn func(store database.Store) database.Store + expectedSnapshotEntries int + expectedCreated int + expectedFailed int + expectedClaimed int + }{ + { + name: "experiment enabled", + experimentEnabled: true, + storeFn: func(store database.Store) database.Store { + return &mockDB{Store: store} + }, + expectedSnapshotEntries: 3, + expectedCreated: 5, + expectedFailed: 2, + expectedClaimed: 3, + }, + { + name: "experiment enabled, prebuilds not used", + experimentEnabled: true, + storeFn: func(store database.Store) database.Store { + return &emptyMockDB{Store: store} + }, + }, + { + name: "experiment disabled", + experimentEnabled: false, + storeFn: func(store database.Store) database.Store { + return &mockDB{Store: store} + }, + }, + } - require.NotNil(t, deployment) - require.NotNil(t, snapshot) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - require.Len(t, snapshot.PrebuiltWorkspaces, 3) + deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { + opts.Database = tc.storeFn(db) + if tc.experimentEnabled { + opts.Experiments = codersdk.Experiments{ + codersdk.ExperimentWorkspacePrebuilds, + } + } + return opts + }) + + require.NotNil(t, deployment) + require.NotNil(t, snapshot) + + require.Len(t, snapshot.PrebuiltWorkspaces, tc.expectedSnapshotEntries) + + eventCounts := make(map[telemetry.PrebuiltWorkspaceEventType]int) + for _, event := range snapshot.PrebuiltWorkspaces { + eventCounts[event.EventType] = event.Count + require.NotEqual(t, uuid.Nil, event.ID) + require.False(t, event.CreatedAt.IsZero()) + } - eventCounts := make(map[telemetry.PrebuiltWorkspaceEventType]int) - for _, event := range snapshot.PrebuiltWorkspaces { - eventCounts[event.EventType] = event.Count - require.NotEqual(t, uuid.Nil, event.ID) - require.False(t, event.CreatedAt.IsZero()) + require.Equal(t, tc.expectedCreated, eventCounts[telemetry.PrebuiltWorkspaceEventTypeCreated]) + require.Equal(t, tc.expectedFailed, eventCounts[telemetry.PrebuiltWorkspaceEventTypeFailed]) + require.Equal(t, tc.expectedClaimed, eventCounts[telemetry.PrebuiltWorkspaceEventTypeClaimed]) + }) } - - require.Equal(t, 5, eventCounts[telemetry.PrebuiltWorkspaceEventTypeCreated]) - require.Equal(t, 2, eventCounts[telemetry.PrebuiltWorkspaceEventTypeFailed]) - require.Equal(t, 3, eventCounts[telemetry.PrebuiltWorkspaceEventTypeClaimed]) } type mockDB struct { @@ -425,22 +469,6 @@ func (m *mockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetr }, nil } -func TestPrebuiltWorkspacesTelemetryEmpty(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitMedium) - db := dbmem.New() - - deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { - opts.Database = &emptyMockDB{Store: opts.Database} - return opts - }) - - require.NotNil(t, deployment) - require.NotNil(t, snapshot) - - require.Len(t, snapshot.PrebuiltWorkspaces, 0) -} - type emptyMockDB struct { database.Store } From 588390f992c395c60ae101742b9587341c15a0d7 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 28 May 2025 18:56:17 +0200 Subject: [PATCH 4/5] chore: appeasing linter Signed-off-by: Danny Kopping --- coderd/telemetry/telemetry_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 0a48c7943cbf3..a74d12d38f1c2 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -448,7 +448,7 @@ type mockDB struct { database.Store } -func (m *mockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) { +func (*mockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) { return []database.GetPrebuildMetricsRow{ { TemplateName: "template1", @@ -473,7 +473,7 @@ type emptyMockDB struct { database.Store } -func (m *emptyMockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) { +func (*emptyMockDB) GetPrebuildMetrics(context.Context) ([]database.GetPrebuildMetricsRow, error) { return []database.GetPrebuildMetricsRow{}, nil } From 545adaead1775a35dd7406a81e3e8cd335597cd4 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 29 May 2025 10:18:48 +0000 Subject: [PATCH 5/5] chore: dbmem gtfo Signed-off-by: Danny Kopping --- coderd/telemetry/telemetry_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index a74d12d38f1c2..ab9d2a75e9cf2 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -373,7 +373,7 @@ func TestTelemetryItem(t *testing.T) { func TestPrebuiltWorkspacesTelemetry(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) - db := dbmem.New() + db, _ := dbtestutil.NewDB(t) cases := []struct { name string