Skip to content

feat: add prebuilt workspaces telemetry #18084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 29, 2025
Merged
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
1 change: 1 addition & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
66 changes: 65 additions & 1 deletion coderd/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -683,6 +685,52 @@ 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)
}

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 {
Expand Down Expand Up @@ -1152,6 +1200,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.
Expand Down Expand Up @@ -1724,6 +1773,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) {}
Expand Down
107 changes: 107 additions & 0 deletions coderd/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,113 @@ 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, _ := dbtestutil.NewDB(t)

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}
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

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())
}

require.Equal(t, tc.expectedCreated, eventCounts[telemetry.PrebuiltWorkspaceEventTypeCreated])
require.Equal(t, tc.expectedFailed, eventCounts[telemetry.PrebuiltWorkspaceEventTypeFailed])
require.Equal(t, tc.expectedClaimed, eventCounts[telemetry.PrebuiltWorkspaceEventTypeClaimed])
})
}
}

type mockDB struct {
database.Store
}

func (*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
}

type emptyMockDB struct {
database.Store
}

func (*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 |
Expand Down
Loading