From ab95ae827d129562df600bb6f8e46870024a6549 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 19 Mar 2024 11:05:29 +0200 Subject: [PATCH] feat(coderd): add enabled experiments to telemetry (#12656) --- cli/cliui/provisionerjob_test.go | 3 ++- cli/server.go | 1 + coderd/telemetry/telemetry.go | 20 ++++++++++++++++++++ coderd/telemetry/telemetry_test.go | 28 ++++++++++++++++++++++------ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/cli/cliui/provisionerjob_test.go b/cli/cliui/provisionerjob_test.go index f3661ca8d1..f75a8bc53f 100644 --- a/cli/cliui/provisionerjob_test.go +++ b/cli/cliui/provisionerjob_test.go @@ -11,9 +11,10 @@ import ( "testing" "time" - "github.com/coder/coder/v2/testutil" "github.com/stretchr/testify/assert" + "github.com/coder/coder/v2/testutil" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/codersdk" diff --git a/cli/server.go b/cli/server.go index f57d761da7..f00a6ec968 100644 --- a/cli/server.go +++ b/cli/server.go @@ -819,6 +819,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. Prometheus: vals.Prometheus.Enable.Value(), STUN: len(vals.DERP.Server.STUNAddresses) != 0, Tunnel: tunnel != nil, + Experiments: vals.Experiments.Value(), ParseLicenseJWT: func(lic *telemetry.License) error { // This will be nil when running in AGPL-only mode. if options.ParseLicenseClaims == nil { diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index a70c0c7d26..8a43c40070 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -54,6 +54,7 @@ type Options struct { SnapshotFrequency time.Duration Tunnel bool ParseLicenseJWT func(lic *License) error + Experiments []string } // New constructs a reporter for telemetry data. @@ -480,6 +481,10 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) { } return nil }) + eg.Go(func() error { + snapshot.Experiments = ConvertExperiments(r.options.Experiments) + return nil + }) err := eg.Wait() if err != nil { @@ -741,6 +746,16 @@ func ConvertExternalProvisioner(id uuid.UUID, tags map[string]string, provisione } } +func ConvertExperiments(experiments []string) []Experiment { + var out []Experiment + + for _, exp := range experiments { + out = append(out, Experiment{Name: exp}) + } + + return out +} + // Snapshot represents a point-in-time anonymized database dump. // Data is aggregated by latest on the server-side, so partial data // can be sent without issue. @@ -763,6 +778,7 @@ type Snapshot struct { WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"` WorkspaceResources []WorkspaceResource `json:"workspace_resources"` Workspaces []Workspace `json:"workspaces"` + Experiments []Experiment `json:"experiments"` } // Deployment contains information about the host running Coder. @@ -971,6 +987,10 @@ type ExternalProvisioner struct { ShutdownAt *time.Time `json:"shutdown_at"` } +type Experiment struct { + Name string `json:"name"` +} + type noopReporter struct{} func (*noopReporter) Report(_ *Snapshot) {} diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 5df6be77f7..4661a4f8f2 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -85,7 +85,7 @@ func TestTelemetry(t *testing.T) { assert.NoError(t, err) _, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{}) - _, snapshot := collectSnapshot(t, db) + _, snapshot := collectSnapshot(t, db, nil) require.Len(t, snapshot.ProvisionerJobs, 1) require.Len(t, snapshot.Licenses, 1) require.Len(t, snapshot.Templates, 1) @@ -110,21 +110,32 @@ func TestTelemetry(t *testing.T) { _ = dbgen.User(t, db, database.User{ Email: "kyle@coder.com", }) - _, snapshot := collectSnapshot(t, db) + _, snapshot := collectSnapshot(t, db, nil) require.Len(t, snapshot.Users, 1) require.Equal(t, snapshot.Users[0].EmailHashed, "bb44bf07cf9a2db0554bba63a03d822c927deae77df101874496df5a6a3e896d@coder.com") }) + t.Run("Experiments", func(t *testing.T) { + t.Parallel() + + const expName = "my-experiment" + exps := []string{expName} + _, snapshot := collectSnapshot(t, dbmem.New(), func(opts telemetry.Options) telemetry.Options { + opts.Experiments = exps + return opts + }) + require.Equal(t, []telemetry.Experiment{{Name: expName}}, snapshot.Experiments) + }) } // nolint:paralleltest func TestTelemetryInstallSource(t *testing.T) { t.Setenv("CODER_TELEMETRY_INSTALL_SOURCE", "aws_marketplace") db := dbmem.New() - deployment, _ := collectSnapshot(t, db) + deployment, _ := collectSnapshot(t, db, nil) require.Equal(t, "aws_marketplace", deployment.InstallSource) } -func collectSnapshot(t *testing.T, db database.Store) (*telemetry.Deployment, *telemetry.Snapshot) { +func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { t.Helper() deployment := make(chan *telemetry.Deployment, 64) snapshot := make(chan *telemetry.Snapshot, 64) @@ -149,12 +160,17 @@ func collectSnapshot(t *testing.T, db database.Store) (*telemetry.Deployment, *t t.Cleanup(server.Close) serverURL, err := url.Parse(server.URL) require.NoError(t, err) - reporter, err := telemetry.New(telemetry.Options{ + options := telemetry.Options{ Database: db, Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), URL: serverURL, DeploymentID: uuid.NewString(), - }) + } + if addOptionsFn != nil { + options = addOptionsFn(options) + } + + reporter, err := telemetry.New(options) require.NoError(t, err) t.Cleanup(reporter.Close) return <-deployment, <-snapshot