mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add prebuilt workspaces telemetry (#18084)
Adds telemetry for a _global_ account of prebuilt workspaces created, failed to build, and claimed. Partitioning this data by template/preset tuple is not currently in scope. --------- Signed-off-by: Danny Kopping <dannykopping@gmail.com>
This commit is contained in:
@ -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,
|
||||
|
@ -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
|
||||
@ -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 {
|
||||
@ -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.
|
||||
@ -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) {}
|
||||
|
@ -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 |
|
||||
|
Reference in New Issue
Block a user