From b44071ae3704adfeb0f6b8dff1cacca307b9832e Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 30 Jan 2025 16:05:29 +0000 Subject: [PATCH 01/22] ReportDisabledIfNeeded --- cli/server.go | 20 +++++++++ coderd/telemetry/telemetry.go | 69 +++++++++++++++++++++++++++--- coderd/telemetry/telemetry_test.go | 1 + 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/cli/server.go b/cli/server.go index 03dcc698c1f05..459d2c089e60c 100644 --- a/cli/server.go +++ b/cli/server.go @@ -814,11 +814,31 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. if err != nil { return xerrors.Errorf("create telemetry reporter: %w", err) } + go options.Telemetry.RunSnapshotter() defer options.Telemetry.Close() } else { logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/setup/telemetry`, vals.DocsURL.String())) } + if !vals.Telemetry.Enable.Value() { + go func() { + reporter, err := telemetry.New(telemetry.Options{ + DeploymentID: deploymentID, + Database: options.Database, + Logger: logger.Named("telemetry"), + URL: vals.Telemetry.URL.Value(), + }) + if err != nil { + logger.Debug(ctx, "create telemetry reporter (disabled)", slog.Error(err)) + return + } + defer reporter.Close() + if err := reporter.ReportDisabledIfNeeded(); err != nil { + logger.Debug(ctx, "failed to report disabled telemetry", slog.Error(err)) + } + }() + } + // This prevents the pprof import from being accidentally deleted. _ = pprof.Handler if vals.Pprof.Enable { diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 3b4bcb7d15ae6..6f301c5ce1491 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -59,6 +59,9 @@ type Options struct { // New constructs a reporter for telemetry data. // Duplicate data will be sent, it's on the server-side to index by UUID. // Data is anonymized prior to being sent! +// +// The returned Reporter should be started with RunSnapshotter() to begin +// reporting. func New(options Options) (Reporter, error) { if options.SnapshotFrequency == 0 { // Report once every 30mins by default! @@ -83,7 +86,6 @@ func New(options Options) (Reporter, error) { snapshotURL: snapshotURL, startedAt: dbtime.Now(), } - go reporter.runSnapshotter() return reporter, nil } @@ -101,6 +103,12 @@ type Reporter interface { Report(snapshot *Snapshot) Enabled() bool Close() + // RunSnapshotter runs reporting in a loop. It should be called in a + // goroutine to avoid blocking the caller. + RunSnapshotter() + // ReportDisabledIfNeeded reports disabled telemetry if there was at least one report sent + // before the telemetry was disabled, and we haven't sent a report since the telemetry was disabled. + ReportDisabledIfNeeded() error } type remoteReporter struct { @@ -149,6 +157,12 @@ func (r *remoteReporter) reportSync(snapshot *Snapshot) { r.options.Logger.Debug(r.ctx, "bad response from telemetry server", slog.F("status", resp.StatusCode)) return } + if err := r.options.Database.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ + Key: string(TelemetryItemKeyLastTelemetryUpdate), + Value: dbtime.Now().Format(time.RFC3339), + }); err != nil { + r.options.Logger.Debug(r.ctx, "upsert last telemetry update", slog.Error(err)) + } r.options.Logger.Debug(r.ctx, "submitted snapshot") } @@ -177,7 +191,7 @@ func (r *remoteReporter) isClosed() bool { } } -func (r *remoteReporter) runSnapshotter() { +func (r *remoteReporter) RunSnapshotter() { first := true ticker := time.NewTicker(r.options.SnapshotFrequency) defer ticker.Stop() @@ -330,6 +344,45 @@ func checkIDPOrgSync(ctx context.Context, db database.Store, values *codersdk.De return syncConfig.Field != "", nil } +func (r *remoteReporter) ReportDisabledIfNeeded() error { + db := r.options.Database + lastTelemetryUpdate, telemetryUpdateErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyLastTelemetryUpdate)) + if telemetryUpdateErr != nil { + r.options.Logger.Debug(r.ctx, "get last telemetry update at", slog.Error(telemetryUpdateErr)) + } + telemetryDisabled, telemetryDisabledErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) + if telemetryDisabledErr != nil { + r.options.Logger.Debug(r.ctx, "get telemetry disabled", slog.Error(telemetryDisabledErr)) + } + shouldReportDisabledTelemetry := + telemetryUpdateErr == nil && + ((telemetryDisabledErr == nil && lastTelemetryUpdate.UpdatedAt.Before(telemetryDisabled.UpdatedAt)) || + errors.Is(telemetryDisabledErr, sql.ErrNoRows)) + if !shouldReportDisabledTelemetry { + return nil + } + + if err := db.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ + Key: string(TelemetryItemKeyTelemetryDisabled), + Value: time.Now().Format(time.RFC3339), + }); err != nil { + return xerrors.Errorf("upsert telemetry disabled: %w", err) + } + item, err := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) + if err != nil { + return xerrors.Errorf("get telemetry disabled: %w", err) + } + + r.reportSync( + &Snapshot{ + TelemetryItems: []TelemetryItem{ + ConvertTelemetryItem(item), + }, + }, + ) + return nil +} + // createSnapshot collects a full snapshot from the database. func (r *remoteReporter) createSnapshot() (*Snapshot, error) { var ( @@ -1566,7 +1619,9 @@ type telemetryItemKey string // //revive:disable:exported const ( - TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" + TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" + TelemetryItemKeyLastTelemetryUpdate telemetryItemKey = "last_telemetry_update" + TelemetryItemKeyTelemetryDisabled telemetryItemKey = "telemetry_disabled" ) type TelemetryItem struct { @@ -1578,6 +1633,8 @@ type TelemetryItem struct { type noopReporter struct{} -func (*noopReporter) Report(_ *Snapshot) {} -func (*noopReporter) Enabled() bool { return false } -func (*noopReporter) Close() {} +func (*noopReporter) Report(_ *Snapshot) {} +func (*noopReporter) Enabled() bool { return false } +func (*noopReporter) Close() {} +func (*noopReporter) RunSnapshotter() {} +func (*noopReporter) ReportDisabledIfNeeded() error { return nil } diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 647dcd834c6c9..cfb1f5bc089ff 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -398,6 +398,7 @@ func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts tel reporter, err := telemetry.New(options) require.NoError(t, err) + go reporter.RunSnapshotter() t.Cleanup(reporter.Close) return <-deployment, <-snapshot } From bb08f9de674a71c7232a5c3a13c1b1c805b419e8 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 30 Jan 2025 17:54:37 +0000 Subject: [PATCH 02/22] add a test for TestReportDisabledIfNeeded --- coderd/telemetry/telemetry.go | 4 +- coderd/telemetry/telemetry_test.go | 78 ++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 6f301c5ce1491..4f3c6d4cf16e5 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -347,11 +347,11 @@ func checkIDPOrgSync(ctx context.Context, db database.Store, values *codersdk.De func (r *remoteReporter) ReportDisabledIfNeeded() error { db := r.options.Database lastTelemetryUpdate, telemetryUpdateErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyLastTelemetryUpdate)) - if telemetryUpdateErr != nil { + if telemetryUpdateErr != nil && !errors.Is(telemetryUpdateErr, sql.ErrNoRows) { r.options.Logger.Debug(r.ctx, "get last telemetry update at", slog.Error(telemetryUpdateErr)) } telemetryDisabled, telemetryDisabledErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) - if telemetryDisabledErr != nil { + if telemetryDisabledErr != nil && !errors.Is(telemetryDisabledErr, sql.ErrNoRows) { r.options.Logger.Debug(r.ctx, "get telemetry disabled", slog.Error(telemetryDisabledErr)) } shouldReportDisabledTelemetry := diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index cfb1f5bc089ff..c512150353980 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -361,31 +361,103 @@ func TestTelemetryItem(t *testing.T) { require.Equal(t, item.Value, "new_value") } -func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { +func TestReportDisabledIfNeeded(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitMedium) + serverURL, _, snapshotChan := setupTelemetryServer(t) + + options := telemetry.Options{ + Database: db, + Logger: testutil.Logger(t), + URL: serverURL, + DeploymentID: uuid.NewString(), + } + + reporter, err := telemetry.New(options) + require.NoError(t, err) + t.Cleanup(reporter.Close) + + // No telemetry updated item, so no report should be sent + require.NoError(t, reporter.ReportDisabledIfNeeded()) + require.Empty(t, snapshotChan) + + // Telemetry disabled item not present, and a telemetry update item present + // Report should be sent + _ = dbgen.TelemetryItem(t, db, database.TelemetryItem{ + Key: string(telemetry.TelemetryItemKeyLastTelemetryUpdate), + Value: time.Now().Format(time.RFC3339), + }) + require.NoError(t, reporter.ReportDisabledIfNeeded()) + select { + case snapshot := <-snapshotChan: + require.Len(t, snapshot.TelemetryItems, 1) + require.Equal(t, snapshot.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryDisabled)) + case <-time.After(testutil.WaitShort / 2): + t.Fatal("timeout waiting for snapshot") + } + + // Telemetry disabled item present, and a telemetry update item present + // with an updated at time equal to or after the telemetry disabled item + // Report should not be sent + require.NoError(t, reporter.ReportDisabledIfNeeded()) + require.Empty(t, snapshotChan) + + // Telemetry disabled item present, and a telemetry update item present + // with an updated at time before the telemetry disabled item + // Report should be sent + // Wait a bit to ensure UpdatedAt is bigger when we upsert the telemetry disabled item + time.Sleep(100 * time.Millisecond) + require.NoError(t, db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: string(telemetry.TelemetryItemKeyTelemetryDisabled), + Value: time.Now().Format(time.RFC3339), + })) + require.NoError(t, reporter.ReportDisabledIfNeeded()) + select { + case snapshot := <-snapshotChan: + require.Len(t, snapshot.TelemetryItems, 1) + require.Equal(t, snapshot.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryDisabled)) + case <-time.After(testutil.WaitShort / 2): + t.Fatal("timeout waiting for snapshot") + } +} + +func setupTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { t.Helper() deployment := make(chan *telemetry.Deployment, 64) snapshot := make(chan *telemetry.Snapshot, 64) r := chi.NewRouter() r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) - w.WriteHeader(http.StatusAccepted) dd := &telemetry.Deployment{} err := json.NewDecoder(r.Body).Decode(dd) require.NoError(t, err) deployment <- dd + // Ensure the header is sent only after deployment is sent + w.WriteHeader(http.StatusAccepted) }) r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) - w.WriteHeader(http.StatusAccepted) ss := &telemetry.Snapshot{} err := json.NewDecoder(r.Body).Decode(ss) require.NoError(t, err) snapshot <- ss + // Ensure the header is sent only after snapshot is sent + w.WriteHeader(http.StatusAccepted) }) server := httptest.NewServer(r) t.Cleanup(server.Close) serverURL, err := url.Parse(server.URL) require.NoError(t, err) + + return serverURL, deployment, snapshot +} + +func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { + t.Helper() + + serverURL, deployment, snapshot := setupTelemetryServer(t) + options := telemetry.Options{ Database: db, Logger: testutil.Logger(t), From db99f0179e83a7daa95ad32295d0eb6ac3c44d19 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 30 Jan 2025 17:58:53 +0000 Subject: [PATCH 03/22] remove unused values --- coderd/telemetry/telemetry.go | 4 ++-- coderd/telemetry/telemetry_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 4f3c6d4cf16e5..575e5e30001ce 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -159,7 +159,7 @@ func (r *remoteReporter) reportSync(snapshot *Snapshot) { } if err := r.options.Database.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ Key: string(TelemetryItemKeyLastTelemetryUpdate), - Value: dbtime.Now().Format(time.RFC3339), + Value: "", }); err != nil { r.options.Logger.Debug(r.ctx, "upsert last telemetry update", slog.Error(err)) } @@ -364,7 +364,7 @@ func (r *remoteReporter) ReportDisabledIfNeeded() error { if err := db.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ Key: string(TelemetryItemKeyTelemetryDisabled), - Value: time.Now().Format(time.RFC3339), + Value: "", }); err != nil { return xerrors.Errorf("upsert telemetry disabled: %w", err) } diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index c512150353980..d9813fd0e8f01 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -386,7 +386,7 @@ func TestReportDisabledIfNeeded(t *testing.T) { // Report should be sent _ = dbgen.TelemetryItem(t, db, database.TelemetryItem{ Key: string(telemetry.TelemetryItemKeyLastTelemetryUpdate), - Value: time.Now().Format(time.RFC3339), + Value: "", }) require.NoError(t, reporter.ReportDisabledIfNeeded()) select { @@ -410,7 +410,7 @@ func TestReportDisabledIfNeeded(t *testing.T) { time.Sleep(100 * time.Millisecond) require.NoError(t, db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ Key: string(telemetry.TelemetryItemKeyTelemetryDisabled), - Value: time.Now().Format(time.RFC3339), + Value: "", })) require.NoError(t, reporter.ReportDisabledIfNeeded()) select { From 7293db51aa2784f8f272073b8c811895cdf51b1c Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 30 Jan 2025 18:27:15 +0000 Subject: [PATCH 04/22] fix data race --- cli/server.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cli/server.go b/cli/server.go index 459d2c089e60c..19f6558dd7ef5 100644 --- a/cli/server.go +++ b/cli/server.go @@ -821,22 +821,22 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } if !vals.Telemetry.Enable.Value() { - go func() { - reporter, err := telemetry.New(telemetry.Options{ - DeploymentID: deploymentID, - Database: options.Database, - Logger: logger.Named("telemetry"), - URL: vals.Telemetry.URL.Value(), - }) - if err != nil { - logger.Debug(ctx, "create telemetry reporter (disabled)", slog.Error(err)) - return - } - defer reporter.Close() - if err := reporter.ReportDisabledIfNeeded(); err != nil { - logger.Debug(ctx, "failed to report disabled telemetry", slog.Error(err)) - } - }() + reporter, err := telemetry.New(telemetry.Options{ + DeploymentID: deploymentID, + Database: options.Database, + Logger: logger.Named("telemetry"), + URL: vals.Telemetry.URL.Value(), + }) + if err != nil { + logger.Debug(ctx, "create telemetry reporter (disabled)", slog.Error(err)) + } else { + go func() { + defer reporter.Close() + if err := reporter.ReportDisabledIfNeeded(); err != nil { + logger.Debug(ctx, "failed to report disabled telemetry", slog.Error(err)) + } + }() + } } // This prevents the pprof import from being accidentally deleted. From 2ceaf0d49e9057327ecde5e51aaab7631c0c4056 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 30 Jan 2025 18:31:42 +0000 Subject: [PATCH 05/22] disable report on close when reporting telemetry disabled --- cli/server.go | 9 +++++---- coderd/telemetry/telemetry.go | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cli/server.go b/cli/server.go index 19f6558dd7ef5..9d73cdf7e6a5a 100644 --- a/cli/server.go +++ b/cli/server.go @@ -822,10 +822,11 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. if !vals.Telemetry.Enable.Value() { reporter, err := telemetry.New(telemetry.Options{ - DeploymentID: deploymentID, - Database: options.Database, - Logger: logger.Named("telemetry"), - URL: vals.Telemetry.URL.Value(), + DeploymentID: deploymentID, + Database: options.Database, + Logger: logger.Named("telemetry"), + URL: vals.Telemetry.URL.Value(), + DisableReportOnClose: true, }) if err != nil { logger.Debug(ctx, "create telemetry reporter (disabled)", slog.Error(err)) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 575e5e30001ce..05b979bfa3c1f 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -47,10 +47,11 @@ type Options struct { // URL is an endpoint to direct telemetry towards! URL *url.URL - DeploymentID string - DeploymentConfig *codersdk.DeploymentValues - BuiltinPostgres bool - Tunnel bool + DeploymentID string + DeploymentConfig *codersdk.DeploymentValues + BuiltinPostgres bool + Tunnel bool + DisableReportOnClose bool SnapshotFrequency time.Duration ParseLicenseJWT func(lic *License) error @@ -175,10 +176,12 @@ func (r *remoteReporter) Close() { close(r.closed) now := dbtime.Now() r.shutdownAt = &now - // Report a final collection of telemetry prior to close! - // This could indicate final actions a user has taken, and - // the time the deployment was shutdown. - r.reportWithDeployment() + if !r.options.DisableReportOnClose { + // Report a final collection of telemetry prior to close! + // This could indicate final actions a user has taken, and + // the time the deployment was shutdown. + r.reportWithDeployment() + } r.closeFunc() } From 9cccf4d893101ea06cf9ae17c4004f26798f9dea Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 30 Jan 2025 18:34:50 +0000 Subject: [PATCH 06/22] fmt --- coderd/telemetry/telemetry.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 05b979bfa3c1f..70e0da13e3628 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -357,10 +357,9 @@ func (r *remoteReporter) ReportDisabledIfNeeded() error { if telemetryDisabledErr != nil && !errors.Is(telemetryDisabledErr, sql.ErrNoRows) { r.options.Logger.Debug(r.ctx, "get telemetry disabled", slog.Error(telemetryDisabledErr)) } - shouldReportDisabledTelemetry := - telemetryUpdateErr == nil && - ((telemetryDisabledErr == nil && lastTelemetryUpdate.UpdatedAt.Before(telemetryDisabled.UpdatedAt)) || - errors.Is(telemetryDisabledErr, sql.ErrNoRows)) + shouldReportDisabledTelemetry := telemetryUpdateErr == nil && + ((telemetryDisabledErr == nil && lastTelemetryUpdate.UpdatedAt.Before(telemetryDisabled.UpdatedAt)) || + errors.Is(telemetryDisabledErr, sql.ErrNoRows)) if !shouldReportDisabledTelemetry { return nil } From ce2b563d5560b7ed6770dd5cf7163e6a0014152f Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 13:54:44 +0000 Subject: [PATCH 07/22] simplify the telemetry disabled reporting logic --- coderd/telemetry/telemetry.go | 44 ++++++++++++++++++++---------- coderd/telemetry/telemetry_test.go | 31 ++++++++++----------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 70e0da13e3628..1f132242375b8 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -158,12 +158,6 @@ func (r *remoteReporter) reportSync(snapshot *Snapshot) { r.options.Logger.Debug(r.ctx, "bad response from telemetry server", slog.F("status", resp.StatusCode)) return } - if err := r.options.Database.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ - Key: string(TelemetryItemKeyLastTelemetryUpdate), - Value: "", - }); err != nil { - r.options.Logger.Debug(r.ctx, "upsert last telemetry update", slog.Error(err)) - } r.options.Logger.Debug(r.ctx, "submitted snapshot") } @@ -194,7 +188,18 @@ func (r *remoteReporter) isClosed() bool { } } +func (r *remoteReporter) recordTelemetryEnabled() { + if err := r.options.Database.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ + Key: string(TelemetryItemKeyTelemetryEnabled), + Value: "", + }); err != nil { + r.options.Logger.Debug(r.ctx, "upsert last telemetry report", slog.Error(err)) + } +} + func (r *remoteReporter) RunSnapshotter() { + r.recordTelemetryEnabled() + first := true ticker := time.NewTicker(r.options.SnapshotFrequency) defer ticker.Stop() @@ -349,17 +354,26 @@ func checkIDPOrgSync(ctx context.Context, db database.Store, values *codersdk.De func (r *remoteReporter) ReportDisabledIfNeeded() error { db := r.options.Database - lastTelemetryUpdate, telemetryUpdateErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyLastTelemetryUpdate)) - if telemetryUpdateErr != nil && !errors.Is(telemetryUpdateErr, sql.ErrNoRows) { - r.options.Logger.Debug(r.ctx, "get last telemetry update at", slog.Error(telemetryUpdateErr)) + telemetryEnabled, telemetryEnabledErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryEnabled)) + if telemetryEnabledErr != nil && !errors.Is(telemetryEnabledErr, sql.ErrNoRows) { + r.options.Logger.Debug(r.ctx, "get telemetry enabled", slog.Error(telemetryEnabledErr)) } telemetryDisabled, telemetryDisabledErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) if telemetryDisabledErr != nil && !errors.Is(telemetryDisabledErr, sql.ErrNoRows) { r.options.Logger.Debug(r.ctx, "get telemetry disabled", slog.Error(telemetryDisabledErr)) } - shouldReportDisabledTelemetry := telemetryUpdateErr == nil && - ((telemetryDisabledErr == nil && lastTelemetryUpdate.UpdatedAt.Before(telemetryDisabled.UpdatedAt)) || - errors.Is(telemetryDisabledErr, sql.ErrNoRows)) + // There are 2 scenarios in which we want to report the disabled telemetry: + // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. + // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry + // was enabled again, and then disabled again. + // + // - In both cases, the TelemetryEnabled item will be present. + // - In case 1. the TelemetryDisabled item will not be present. + // - In case 2. the TelemetryDisabled item will be present, and the TelemetryEnabled item will + // be more recent than the TelemetryDisabled item. + shouldReportDisabledTelemetry := telemetryEnabledErr == nil && + (errors.Is(telemetryDisabledErr, sql.ErrNoRows) || + (telemetryDisabledErr == nil && telemetryEnabled.UpdatedAt.After(telemetryDisabled.UpdatedAt))) if !shouldReportDisabledTelemetry { return nil } @@ -1621,9 +1635,9 @@ type telemetryItemKey string // //revive:disable:exported const ( - TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" - TelemetryItemKeyLastTelemetryUpdate telemetryItemKey = "last_telemetry_update" - TelemetryItemKeyTelemetryDisabled telemetryItemKey = "telemetry_disabled" + TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" + TelemetryItemKeyTelemetryEnabled telemetryItemKey = "telemetry_enabled" + TelemetryItemKeyTelemetryDisabled telemetryItemKey = "telemetry_disabled" ) type TelemetryItem struct { diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index d9813fd0e8f01..8a09989b32f7a 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -365,27 +365,28 @@ func TestReportDisabledIfNeeded(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitMedium) - serverURL, _, snapshotChan := setupTelemetryServer(t) + serverURL, _, snapshotChan := mockTelemetryServer(t) options := telemetry.Options{ - Database: db, - Logger: testutil.Logger(t), - URL: serverURL, - DeploymentID: uuid.NewString(), + Database: db, + Logger: testutil.Logger(t), + URL: serverURL, + DeploymentID: uuid.NewString(), + DisableReportOnClose: true, } reporter, err := telemetry.New(options) require.NoError(t, err) t.Cleanup(reporter.Close) - // No telemetry updated item, so no report should be sent + // No telemetry enabled item, so no report should be sent require.NoError(t, reporter.ReportDisabledIfNeeded()) require.Empty(t, snapshotChan) - // Telemetry disabled item not present, and a telemetry update item present + // Telemetry enabled item present, and a telemetry disabled item not present // Report should be sent _ = dbgen.TelemetryItem(t, db, database.TelemetryItem{ - Key: string(telemetry.TelemetryItemKeyLastTelemetryUpdate), + Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), Value: "", }) require.NoError(t, reporter.ReportDisabledIfNeeded()) @@ -397,19 +398,17 @@ func TestReportDisabledIfNeeded(t *testing.T) { t.Fatal("timeout waiting for snapshot") } - // Telemetry disabled item present, and a telemetry update item present - // with an updated at time equal to or after the telemetry disabled item + // Telemetry enabled item present, and a more recent telemetry disabled item present // Report should not be sent require.NoError(t, reporter.ReportDisabledIfNeeded()) require.Empty(t, snapshotChan) - // Telemetry disabled item present, and a telemetry update item present - // with an updated at time before the telemetry disabled item + // Telemetry enabled item present, and a less recent telemetry disabled item present // Report should be sent - // Wait a bit to ensure UpdatedAt is bigger when we upsert the telemetry disabled item + // Wait a bit to ensure UpdatedAt is bigger when we upsert the telemetry enabled item time.Sleep(100 * time.Millisecond) require.NoError(t, db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ - Key: string(telemetry.TelemetryItemKeyTelemetryDisabled), + Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), Value: "", })) require.NoError(t, reporter.ReportDisabledIfNeeded()) @@ -422,7 +421,7 @@ func TestReportDisabledIfNeeded(t *testing.T) { } } -func setupTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { +func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { t.Helper() deployment := make(chan *telemetry.Deployment, 64) snapshot := make(chan *telemetry.Snapshot, 64) @@ -456,7 +455,7 @@ func setupTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, c func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) { t.Helper() - serverURL, deployment, snapshot := setupTelemetryServer(t) + serverURL, deployment, snapshot := mockTelemetryServer(t) options := telemetry.Options{ Database: db, From 7085aa0160e7c6364374e580c8a268f9bf500407 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 14:01:26 +0000 Subject: [PATCH 08/22] more accurate comments --- coderd/telemetry/telemetry.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 1f132242375b8..f71ae56ecf359 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -107,8 +107,10 @@ type Reporter interface { // RunSnapshotter runs reporting in a loop. It should be called in a // goroutine to avoid blocking the caller. RunSnapshotter() - // ReportDisabledIfNeeded reports disabled telemetry if there was at least one report sent - // before the telemetry was disabled, and we haven't sent a report since the telemetry was disabled. + // ReportDisabledIfNeeded reports telemetry in the following scenarios: + // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. + // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry + // was enabled again, then disabled again, and we haven't reported it yet. ReportDisabledIfNeeded() error } @@ -365,7 +367,7 @@ func (r *remoteReporter) ReportDisabledIfNeeded() error { // There are 2 scenarios in which we want to report the disabled telemetry: // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry - // was enabled again, and then disabled again. + // was enabled again, then disabled again, and we haven't reported it yet. // // - In both cases, the TelemetryEnabled item will be present. // - In case 1. the TelemetryDisabled item will not be present. From 16ba74008cafec36969c7bd008fbede97332d0fe Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 14:24:24 +0000 Subject: [PATCH 09/22] clearer comment --- coderd/telemetry/telemetry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index f71ae56ecf359..39d1f46b90c1d 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -107,7 +107,7 @@ type Reporter interface { // RunSnapshotter runs reporting in a loop. It should be called in a // goroutine to avoid blocking the caller. RunSnapshotter() - // ReportDisabledIfNeeded reports telemetry in the following scenarios: + // ReportDisabledIfNeeded reports that the telemetry was disabled in the following scenarios: // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry // was enabled again, then disabled again, and we haven't reported it yet. From 3b62bf9a545b41c8d73a451011d847ee25813f4a Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 15:25:33 +0000 Subject: [PATCH 10/22] fix test --- coderd/telemetry/telemetry_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 8a09989b32f7a..5c65c12ce363a 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -131,7 +131,8 @@ func TestTelemetry(t *testing.T) { require.Len(t, snapshot.WorkspaceProxies, 1) require.Len(t, snapshot.WorkspaceModules, 1) require.Len(t, snapshot.Organizations, 1) - require.Len(t, snapshot.TelemetryItems, 1) + // We create one item manually above. The other is TelemetryEnabled, created by the snapshotter. + require.Len(t, snapshot.TelemetryItems, 2) wsa := snapshot.WorkspaceAgents[0] require.Len(t, wsa.Subsystems, 2) require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0]) From 43b7e6b723da4b513f734c291aa2d7242401d307 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 15:32:26 +0000 Subject: [PATCH 11/22] clarify comment --- coderd/telemetry/telemetry.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 39d1f46b90c1d..c428fda56ed02 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -111,6 +111,8 @@ type Reporter interface { // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry // was enabled again, then disabled again, and we haven't reported it yet. + // + // In particular, it does nothing if the telemetry was never enabled. ReportDisabledIfNeeded() error } From aa56a6e5e202d53a6bf7cd83dd30b0207809ddc0 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 15:39:36 +0000 Subject: [PATCH 12/22] nit --- 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 5c65c12ce363a..aea68ab59cf93 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -406,7 +406,7 @@ func TestReportDisabledIfNeeded(t *testing.T) { // Telemetry enabled item present, and a less recent telemetry disabled item present // Report should be sent - // Wait a bit to ensure UpdatedAt is bigger when we upsert the telemetry enabled item + // Wait a bit to ensure UpdatedAt is greater when we upsert the telemetry enabled item time.Sleep(100 * time.Millisecond) require.NoError(t, db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), From a29b7f23e840d983e1260688bbef5db1df610231 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 15:55:02 +0000 Subject: [PATCH 13/22] add another comment --- coderd/telemetry/telemetry.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index c428fda56ed02..f2f2333139547 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -388,6 +388,11 @@ func (r *remoteReporter) ReportDisabledIfNeeded() error { }); err != nil { return xerrors.Errorf("upsert telemetry disabled: %w", err) } + // If any of the following calls fail, we will never report the disabled telemetry. + // Subsequent ReportDisabledIfNeeded calls will see the TelemetryDisabled item + // and quit early. This is okay. We only want to ping the telemetry server once + // at the time when the Coder server is first started with telemetry disabled, + // and never again. If that attempt fails, so be it. item, err := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) if err != nil { return xerrors.Errorf("get telemetry disabled: %w", err) From f3cb1b83f20cc247c401be786b8c851248adf3a6 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 15:59:22 +0000 Subject: [PATCH 14/22] shorten comment --- coderd/telemetry/telemetry.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index f2f2333139547..38fc604d11a28 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -390,8 +390,7 @@ func (r *remoteReporter) ReportDisabledIfNeeded() error { } // If any of the following calls fail, we will never report the disabled telemetry. // Subsequent ReportDisabledIfNeeded calls will see the TelemetryDisabled item - // and quit early. This is okay. We only want to ping the telemetry server once - // at the time when the Coder server is first started with telemetry disabled, + // and quit early. This is okay. We only want to ping the telemetry server once, // and never again. If that attempt fails, so be it. item, err := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) if err != nil { From 75d231f2fd5d20a7370dff10295633ceb66e437c Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 16:40:41 +0000 Subject: [PATCH 15/22] fix log message --- coderd/telemetry/telemetry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 38fc604d11a28..3d6d0348299ba 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -197,7 +197,7 @@ func (r *remoteReporter) recordTelemetryEnabled() { Key: string(TelemetryItemKeyTelemetryEnabled), Value: "", }); err != nil { - r.options.Logger.Debug(r.ctx, "upsert last telemetry report", slog.Error(err)) + r.options.Logger.Debug(r.ctx, "upsert telemetry enabled", slog.Error(err)) } } From a1c12cf958d1057e33db211a6496bae819c6b8b0 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 18:20:01 +0000 Subject: [PATCH 16/22] add an extra integration test --- cli/server.go | 1 + cli/server_test.go | 165 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 17 deletions(-) diff --git a/cli/server.go b/cli/server.go index 9d73cdf7e6a5a..8983c10ec403c 100644 --- a/cli/server.go +++ b/cli/server.go @@ -836,6 +836,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. if err := reporter.ReportDisabledIfNeeded(); err != nil { logger.Debug(ctx, "failed to report disabled telemetry", slog.Error(err)) } + logger.Debug(ctx, "finished disabled telemetry check") }() } } diff --git a/cli/server_test.go b/cli/server_test.go index 8ed4d89ceb970..39c44b786a4d2 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -39,6 +39,7 @@ import ( "tailscale.com/types/key" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/config" @@ -947,22 +948,7 @@ func TestServer(t *testing.T) { t.Run("Telemetry", func(t *testing.T) { t.Parallel() - deployment := make(chan struct{}, 64) - snapshot := make(chan *telemetry.Snapshot, 64) - r := chi.NewRouter() - r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - deployment <- struct{}{} - }) - r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - ss := &telemetry.Snapshot{} - err := json.NewDecoder(r.Body).Decode(ss) - require.NoError(t, err) - snapshot <- ss - }) - server := httptest.NewServer(r) - defer server.Close() + telemetryServerURL, deployment, snapshot := mockTelemetryServer(t) inv, cfg := clitest.New(t, "server", @@ -970,7 +956,7 @@ func TestServer(t *testing.T) { "--http-address", ":0", "--access-url", "http://example.com", "--telemetry", - "--telemetry-url", server.URL, + "--telemetry-url", telemetryServerURL.String(), "--cache-dir", t.TempDir(), ) clitest.Start(t, inv) @@ -2009,3 +1995,148 @@ func TestServer_DisabledDERP(t *testing.T) { err = c.Connect(ctx) require.Error(t, err) } + +type runServerOpts struct { + waitForSnapshot bool + telemetryDisabled bool + waitForTelemetryDisabledCheck bool +} + +func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("this test requires postgres") + } + + telemetryServerURL, deployment, snapshot := mockTelemetryServer(t) + dbConnURL, err := dbtestutil.Open(t) + require.NoError(t, err) + + cacheDir := t.TempDir() + runServer := func(t *testing.T, opts runServerOpts) (chan error, context.CancelFunc) { + ctx, cancelFunc := context.WithCancel(context.Background()) + inv, _ := clitest.New(t, + "server", + "--postgres-url", dbConnURL, + "--http-address", ":0", + "--access-url", "http://example.com", + "--telemetry="+strconv.FormatBool(!opts.telemetryDisabled), + "--telemetry-url", telemetryServerURL.String(), + "--cache-dir", cacheDir, + "--log-filter", ".*", + ) + finished := make(chan bool, 2) + errChan := make(chan error, 1) + pty := ptytest.New(t).Attach(inv) + go func() { + errChan <- inv.WithContext(ctx).Run() + finished <- true + }() + go func() { + defer func() { + finished <- true + }() + if opts.waitForSnapshot { + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "submitted snapshot") + } + if opts.waitForTelemetryDisabledCheck { + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "finished disabled telemetry check") + } + }() + <-finished + return errChan, cancelFunc + } + waitForShutdown := func(t *testing.T, errChan chan error) error { + t.Helper() + select { + case err := <-errChan: + return err + case <-time.After(testutil.WaitMedium): + t.Fatalf("timed out waiting for server to shutdown") + } + return nil + } + + errChan, cancelFunc := runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + + // Since telemetry was disabled, we expect no deployments or snapshots. + require.Empty(t, deployment) + require.Empty(t, snapshot) + + errChan, cancelFunc = runServer(t, runServerOpts{waitForSnapshot: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + // we expect to see a deployment and a snapshot twice: + // 1. the first pair is sent when the server starts + // 2. the second pair is sent when the server shuts down + for i := 0; i < 2; i++ { + select { + case ss := <-snapshot: + t.Logf("got this fun snapshot: %+v", ss) + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for snapshot") + } + select { + case <-deployment: + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for deployment") + } + } + + errChan, cancelFunc = runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + + // Since telemetry is disabled, we expect no deployment. We expect a snapshot + // with the telemetry disabled item. + require.Empty(t, deployment) + select { + case ss := <-snapshot: + require.Len(t, ss.TelemetryItems, 1) + require.Equal(t, string(telemetry.TelemetryItemKeyTelemetryDisabled), ss.TelemetryItems[0].Key) + case <-time.After(testutil.WaitShort / 2): + t.Fatalf("timed out waiting for snapshot") + } + + errChan, cancelFunc = runServer(t, runServerOpts{telemetryDisabled: true, waitForTelemetryDisabledCheck: true}) + cancelFunc() + require.NoError(t, waitForShutdown(t, errChan)) + // Since telemetry is disabled and we've already sent a snapshot, we expect no + // new deployments or snapshots. + require.Empty(t, deployment) + require.Empty(t, snapshot) +} + +func mockTelemetryServer(t *testing.T) (*url.URL, chan *telemetry.Deployment, chan *telemetry.Snapshot) { + t.Helper() + deployment := make(chan *telemetry.Deployment, 64) + snapshot := make(chan *telemetry.Snapshot, 64) + r := chi.NewRouter() + r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) + dd := &telemetry.Deployment{} + err := json.NewDecoder(r.Body).Decode(dd) + require.NoError(t, err) + deployment <- dd + // Ensure the header is sent only after deployment is sent + w.WriteHeader(http.StatusAccepted) + }) + r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, buildinfo.Version(), r.Header.Get(telemetry.VersionHeader)) + ss := &telemetry.Snapshot{} + err := json.NewDecoder(r.Body).Decode(ss) + require.NoError(t, err) + snapshot <- ss + // Ensure the header is sent only after snapshot is sent + w.WriteHeader(http.StatusAccepted) + }) + server := httptest.NewServer(r) + t.Cleanup(server.Close) + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + return serverURL, deployment, snapshot +} From a0fce8d53202f5d1777b0ff69413fbad1c5ae53e Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 21:38:30 +0000 Subject: [PATCH 17/22] use only a single telemetry item to track telemetry enabled status --- cli/server.go | 90 +++++++---------- cli/server_test.go | 8 +- coderd/telemetry/telemetry.go | 149 ++++++++++++++--------------- coderd/telemetry/telemetry_test.go | 116 +++++++++++----------- 4 files changed, 170 insertions(+), 193 deletions(-) diff --git a/cli/server.go b/cli/server.go index 8983c10ec403c..ebcc74156ef86 100644 --- a/cli/server.go +++ b/cli/server.go @@ -781,66 +781,46 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. // This should be output before the logs start streaming. cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):") - if vals.Telemetry.Enable { - vals, err := vals.WithoutSecrets() - if err != nil { - return xerrors.Errorf("remove secrets from deployment values: %w", err) - } - options.Telemetry, err = telemetry.New(telemetry.Options{ - BuiltinPostgres: builtinPostgres, - DeploymentID: deploymentID, - Database: options.Database, - Logger: logger.Named("telemetry"), - URL: vals.Telemetry.URL.Value(), - Tunnel: tunnel != nil, - DeploymentConfig: vals, - ParseLicenseJWT: func(lic *telemetry.License) error { - // This will be nil when running in AGPL-only mode. - if options.ParseLicenseClaims == nil { - return nil - } - - email, trial, err := options.ParseLicenseClaims(lic.JWT) - if err != nil { - return err - } - if email != "" { - lic.Email = &email - } - lic.Trial = &trial + deploymentConfigWithoutSecrets, err := vals.WithoutSecrets() + if err != nil { + return xerrors.Errorf("remove secrets from deployment values: %w", err) + } + telemetryReporter, err := telemetry.New(telemetry.Options{ + Enabled: vals.Telemetry.Enable.Value(), + BuiltinPostgres: builtinPostgres, + DeploymentID: deploymentID, + Database: options.Database, + Logger: logger.Named("telemetry"), + URL: vals.Telemetry.URL.Value(), + Tunnel: tunnel != nil, + DeploymentConfig: deploymentConfigWithoutSecrets, + ParseLicenseJWT: func(lic *telemetry.License) error { + // This will be nil when running in AGPL-only mode. + if options.ParseLicenseClaims == nil { return nil - }, - }) - if err != nil { - return xerrors.Errorf("create telemetry reporter: %w", err) - } - go options.Telemetry.RunSnapshotter() - defer options.Telemetry.Close() + } + + email, trial, err := options.ParseLicenseClaims(lic.JWT) + if err != nil { + return err + } + if email != "" { + lic.Email = &email + } + lic.Trial = &trial + return nil + }, + }) + if err != nil { + return xerrors.Errorf("create telemetry reporter: %w", err) + } + defer telemetryReporter.Close() + if vals.Telemetry.Enable.Value() { + options.Telemetry = telemetryReporter } else { logger.Warn(ctx, fmt.Sprintf(`telemetry disabled, unable to notify of security issues. Read more: %s/admin/setup/telemetry`, vals.DocsURL.String())) } - if !vals.Telemetry.Enable.Value() { - reporter, err := telemetry.New(telemetry.Options{ - DeploymentID: deploymentID, - Database: options.Database, - Logger: logger.Named("telemetry"), - URL: vals.Telemetry.URL.Value(), - DisableReportOnClose: true, - }) - if err != nil { - logger.Debug(ctx, "create telemetry reporter (disabled)", slog.Error(err)) - } else { - go func() { - defer reporter.Close() - if err := reporter.ReportDisabledIfNeeded(); err != nil { - logger.Debug(ctx, "failed to report disabled telemetry", slog.Error(err)) - } - logger.Debug(ctx, "finished disabled telemetry check") - }() - } - } - // This prevents the pprof import from being accidentally deleted. _ = pprof.Handler if vals.Pprof.Enable { diff --git a/cli/server_test.go b/cli/server_test.go index 39c44b786a4d2..952247fca3bc4 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -2041,7 +2041,7 @@ func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "submitted snapshot") } if opts.waitForTelemetryDisabledCheck { - pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "finished disabled telemetry check") + pty.ExpectMatchContext(testutil.Context(t, testutil.WaitLong), "finished telemetry status check") } }() <-finished @@ -2074,8 +2074,7 @@ func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { // 2. the second pair is sent when the server shuts down for i := 0; i < 2; i++ { select { - case ss := <-snapshot: - t.Logf("got this fun snapshot: %+v", ss) + case <-snapshot: case <-time.After(testutil.WaitShort / 2): t.Fatalf("timed out waiting for snapshot") } @@ -2096,7 +2095,8 @@ func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { select { case ss := <-snapshot: require.Len(t, ss.TelemetryItems, 1) - require.Equal(t, string(telemetry.TelemetryItemKeyTelemetryDisabled), ss.TelemetryItems[0].Key) + require.Equal(t, string(telemetry.TelemetryItemKeyTelemetryEnabled), ss.TelemetryItems[0].Key) + require.Equal(t, "0", ss.TelemetryItems[0].Value) case <-time.After(testutil.WaitShort / 2): t.Fatalf("timed out waiting for snapshot") } diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 3d6d0348299ba..ee7827bdf61eb 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -42,16 +42,16 @@ const ( ) type Options struct { + Enabled bool Database database.Store Logger slog.Logger // URL is an endpoint to direct telemetry towards! URL *url.URL - DeploymentID string - DeploymentConfig *codersdk.DeploymentValues - BuiltinPostgres bool - Tunnel bool - DisableReportOnClose bool + DeploymentID string + DeploymentConfig *codersdk.DeploymentValues + BuiltinPostgres bool + Tunnel bool SnapshotFrequency time.Duration ParseLicenseJWT func(lic *License) error @@ -60,9 +60,6 @@ type Options struct { // New constructs a reporter for telemetry data. // Duplicate data will be sent, it's on the server-side to index by UUID. // Data is anonymized prior to being sent! -// -// The returned Reporter should be started with RunSnapshotter() to begin -// reporting. func New(options Options) (Reporter, error) { if options.SnapshotFrequency == 0 { // Report once every 30mins by default! @@ -87,6 +84,7 @@ func New(options Options) (Reporter, error) { snapshotURL: snapshotURL, startedAt: dbtime.Now(), } + go reporter.runSnapshotter() return reporter, nil } @@ -104,16 +102,6 @@ type Reporter interface { Report(snapshot *Snapshot) Enabled() bool Close() - // RunSnapshotter runs reporting in a loop. It should be called in a - // goroutine to avoid blocking the caller. - RunSnapshotter() - // ReportDisabledIfNeeded reports that the telemetry was disabled in the following scenarios: - // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. - // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry - // was enabled again, then disabled again, and we haven't reported it yet. - // - // In particular, it does nothing if the telemetry was never enabled. - ReportDisabledIfNeeded() error } type remoteReporter struct { @@ -129,8 +117,8 @@ type remoteReporter struct { shutdownAt *time.Time } -func (*remoteReporter) Enabled() bool { - return true +func (r *remoteReporter) Enabled() bool { + return r.options.Enabled } func (r *remoteReporter) Report(snapshot *Snapshot) { @@ -174,7 +162,7 @@ func (r *remoteReporter) Close() { close(r.closed) now := dbtime.Now() r.shutdownAt = &now - if !r.options.DisableReportOnClose { + if r.Enabled() { // Report a final collection of telemetry prior to close! // This could indicate final actions a user has taken, and // the time the deployment was shutdown. @@ -192,17 +180,72 @@ func (r *remoteReporter) isClosed() bool { } } -func (r *remoteReporter) recordTelemetryEnabled() { - if err := r.options.Database.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ +// See the corresponding test in telemetry_test.go for a truth table. +func ShouldReportTelemetryDisabled(recordedTelemetryEnabled *bool, telemetryEnabled bool) bool { + return recordedTelemetryEnabled != nil && *recordedTelemetryEnabled && !telemetryEnabled +} + +// RecordTelemetryStatusChange records the telemetry status change in the database. +// If the change should be reported, meaning that the status changed from enabled to disabled, +// returns a snapshot to be sent to the telemetry server. +func RecordTelemetryStatusChange( //nolint:revive + ctx context.Context, + db database.Store, + telemetryEnabled bool, +) (*Snapshot, error) { + item, err := db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get telemetry enabled: %w", err) + } + var recordedTelemetryEnabled *bool + if !errors.Is(err, sql.ErrNoRows) { + value := item.Value == "1" + recordedTelemetryEnabled = &value + } + + newValue := "0" + if telemetryEnabled { + newValue = "1" + } + + if err := db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ Key: string(TelemetryItemKeyTelemetryEnabled), - Value: "", + Value: newValue, }); err != nil { - r.options.Logger.Debug(r.ctx, "upsert telemetry enabled", slog.Error(err)) + return nil, xerrors.Errorf("upsert telemetry enabled: %w", err) + } + + shouldReport := ShouldReportTelemetryDisabled(recordedTelemetryEnabled, telemetryEnabled) + if !shouldReport { + return nil, nil //nolint:nilnil } + // If any of the following calls fail, we will never report the disabled telemetry. + // Subsequent function calls will see the TelemetryDisabled item + // and quit early. This is okay. We only want to ping the telemetry server once, + // and never again. If that attempt fails, so be it. + item, err = db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) + if err != nil { + return nil, xerrors.Errorf("get telemetry enabled after upsert: %w", err) + } + return &Snapshot{ + TelemetryItems: []TelemetryItem{ + ConvertTelemetryItem(item), + }, + }, nil } -func (r *remoteReporter) RunSnapshotter() { - r.recordTelemetryEnabled() +func (r *remoteReporter) runSnapshotter() { + telemetryDisabledSnapshot, err := RecordTelemetryStatusChange(r.ctx, r.options.Database, r.Enabled()) + if err != nil { + r.options.Logger.Debug(r.ctx, "record and maybe report telemetry status", slog.Error(err)) + } + if telemetryDisabledSnapshot != nil { + r.reportSync(telemetryDisabledSnapshot) + } + r.options.Logger.Debug(r.ctx, "finished telemetry status check") + if !r.Enabled() { + return + } first := true ticker := time.NewTicker(r.options.SnapshotFrequency) @@ -356,57 +399,6 @@ func checkIDPOrgSync(ctx context.Context, db database.Store, values *codersdk.De return syncConfig.Field != "", nil } -func (r *remoteReporter) ReportDisabledIfNeeded() error { - db := r.options.Database - telemetryEnabled, telemetryEnabledErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryEnabled)) - if telemetryEnabledErr != nil && !errors.Is(telemetryEnabledErr, sql.ErrNoRows) { - r.options.Logger.Debug(r.ctx, "get telemetry enabled", slog.Error(telemetryEnabledErr)) - } - telemetryDisabled, telemetryDisabledErr := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) - if telemetryDisabledErr != nil && !errors.Is(telemetryDisabledErr, sql.ErrNoRows) { - r.options.Logger.Debug(r.ctx, "get telemetry disabled", slog.Error(telemetryDisabledErr)) - } - // There are 2 scenarios in which we want to report the disabled telemetry: - // 1. The telemetry was enabled at some point, and we haven't reported the disabled telemetry yet. - // 2. The telemetry was enabled at some point, we reported the disabled telemetry, the telemetry - // was enabled again, then disabled again, and we haven't reported it yet. - // - // - In both cases, the TelemetryEnabled item will be present. - // - In case 1. the TelemetryDisabled item will not be present. - // - In case 2. the TelemetryDisabled item will be present, and the TelemetryEnabled item will - // be more recent than the TelemetryDisabled item. - shouldReportDisabledTelemetry := telemetryEnabledErr == nil && - (errors.Is(telemetryDisabledErr, sql.ErrNoRows) || - (telemetryDisabledErr == nil && telemetryEnabled.UpdatedAt.After(telemetryDisabled.UpdatedAt))) - if !shouldReportDisabledTelemetry { - return nil - } - - if err := db.UpsertTelemetryItem(r.ctx, database.UpsertTelemetryItemParams{ - Key: string(TelemetryItemKeyTelemetryDisabled), - Value: "", - }); err != nil { - return xerrors.Errorf("upsert telemetry disabled: %w", err) - } - // If any of the following calls fail, we will never report the disabled telemetry. - // Subsequent ReportDisabledIfNeeded calls will see the TelemetryDisabled item - // and quit early. This is okay. We only want to ping the telemetry server once, - // and never again. If that attempt fails, so be it. - item, err := db.GetTelemetryItem(r.ctx, string(TelemetryItemKeyTelemetryDisabled)) - if err != nil { - return xerrors.Errorf("get telemetry disabled: %w", err) - } - - r.reportSync( - &Snapshot{ - TelemetryItems: []TelemetryItem{ - ConvertTelemetryItem(item), - }, - }, - ) - return nil -} - // createSnapshot collects a full snapshot from the database. func (r *remoteReporter) createSnapshot() (*Snapshot, error) { var ( @@ -1645,7 +1637,6 @@ type telemetryItemKey string const ( TelemetryItemKeyHTMLFirstServedAt telemetryItemKey = "html_first_served_at" TelemetryItemKeyTelemetryEnabled telemetryItemKey = "telemetry_enabled" - TelemetryItemKeyTelemetryDisabled telemetryItemKey = "telemetry_disabled" ) type TelemetryItem struct { diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index aea68ab59cf93..915afc624c537 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -362,63 +362,70 @@ func TestTelemetryItem(t *testing.T) { require.Equal(t, item.Value, "new_value") } -func TestReportDisabledIfNeeded(t *testing.T) { +func TestShouldReportTelemetryDisabled(t *testing.T) { t.Parallel() - db, _ := dbtestutil.NewDB(t) - ctx := testutil.Context(t, testutil.WaitMedium) - serverURL, _, snapshotChan := mockTelemetryServer(t) - - options := telemetry.Options{ - Database: db, - Logger: testutil.Logger(t), - URL: serverURL, - DeploymentID: uuid.NewString(), - DisableReportOnClose: true, - } - - reporter, err := telemetry.New(options) - require.NoError(t, err) - t.Cleanup(reporter.Close) - - // No telemetry enabled item, so no report should be sent - require.NoError(t, reporter.ReportDisabledIfNeeded()) - require.Empty(t, snapshotChan) + // Description | telemetryEnabled (db) | telemetryEnabled (is) | Report Telemetry Disabled | + //----------------------------------------|-----------------------|-----------------------|---------------------------| + // New deployment | | true | No | + // New deployment with telemetry disabled | | false | No | + // Telemetry was enabled, and still is | true | true | No | + // Telemetry was enabled but now disabled | true | false | Yes | + // Telemetry was disabled, now is enabled | false | true | No | + // Telemetry was disabled, still disabled | false | false | No | + boolTrue := true + boolFalse := false + require.False(t, telemetry.ShouldReportTelemetryDisabled(nil, true)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(nil, false)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolTrue, true)) + require.True(t, telemetry.ShouldReportTelemetryDisabled(&boolTrue, false)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, true)) + require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, false)) +} - // Telemetry enabled item present, and a telemetry disabled item not present - // Report should be sent - _ = dbgen.TelemetryItem(t, db, database.TelemetryItem{ - Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), - Value: "", - }) - require.NoError(t, reporter.ReportDisabledIfNeeded()) - select { - case snapshot := <-snapshotChan: - require.Len(t, snapshot.TelemetryItems, 1) - require.Equal(t, snapshot.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryDisabled)) - case <-time.After(testutil.WaitShort / 2): - t.Fatal("timeout waiting for snapshot") - } +func TestRecordTelemetryStatusChange(t *testing.T) { + t.Parallel() + for _, testCase := range []struct { + name string + recordedTelemetryEnabled string + telemetryEnabled bool + shouldReport bool + }{ + {name: "New deployment", recordedTelemetryEnabled: "nil", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry disabled", recordedTelemetryEnabled: "nil", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was enabled and still is", recordedTelemetryEnabled: "1", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was enabled but now disabled", recordedTelemetryEnabled: "1", telemetryEnabled: false, shouldReport: true}, + {name: "Telemetry was disabled now is enabled", recordedTelemetryEnabled: "0", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was disabled still disabled", recordedTelemetryEnabled: "0", telemetryEnabled: false, shouldReport: false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + ctx := testutil.Context(t, testutil.WaitMedium) + if testCase.recordedTelemetryEnabled != "nil" { + db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ + Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), + Value: testCase.recordedTelemetryEnabled, + }) + } + snapshot1, err := telemetry.RecordTelemetryStatusChange(ctx, db, testCase.telemetryEnabled) + require.NoError(t, err) + + if testCase.shouldReport { + require.NotNil(t, snapshot1) + require.Equal(t, snapshot1.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryEnabled)) + require.Equal(t, snapshot1.TelemetryItems[0].Value, "0") + } else { + require.Nil(t, snapshot1) + } - // Telemetry enabled item present, and a more recent telemetry disabled item present - // Report should not be sent - require.NoError(t, reporter.ReportDisabledIfNeeded()) - require.Empty(t, snapshotChan) - - // Telemetry enabled item present, and a less recent telemetry disabled item present - // Report should be sent - // Wait a bit to ensure UpdatedAt is greater when we upsert the telemetry enabled item - time.Sleep(100 * time.Millisecond) - require.NoError(t, db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ - Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), - Value: "", - })) - require.NoError(t, reporter.ReportDisabledIfNeeded()) - select { - case snapshot := <-snapshotChan: - require.Len(t, snapshot.TelemetryItems, 1) - require.Equal(t, snapshot.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryDisabled)) - case <-time.After(testutil.WaitShort / 2): - t.Fatal("timeout waiting for snapshot") + for i := 0; i < 3; i++ { + // Whatever happens, subsequent calls should not report if telemetryEnabled didn't change + snapshot2, err := telemetry.RecordTelemetryStatusChange(ctx, db, testCase.telemetryEnabled) + require.NoError(t, err) + require.Nil(t, snapshot2) + } + }) } } @@ -470,7 +477,6 @@ func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts tel reporter, err := telemetry.New(options) require.NoError(t, err) - go reporter.RunSnapshotter() t.Cleanup(reporter.Close) return <-deployment, <-snapshot } From 9a86ec912920a2e1a5fa8cfcdcc1362e767dfbaa Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 21:42:04 +0000 Subject: [PATCH 18/22] rename RecordTelemetryStatusChange to RecordTelemetryStatus --- coderd/telemetry/telemetry.go | 10 +++++----- coderd/telemetry/telemetry_test.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index ee7827bdf61eb..ea93b8464c8e6 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -185,10 +185,10 @@ func ShouldReportTelemetryDisabled(recordedTelemetryEnabled *bool, telemetryEnab return recordedTelemetryEnabled != nil && *recordedTelemetryEnabled && !telemetryEnabled } -// RecordTelemetryStatusChange records the telemetry status change in the database. -// If the change should be reported, meaning that the status changed from enabled to disabled, -// returns a snapshot to be sent to the telemetry server. -func RecordTelemetryStatusChange( //nolint:revive +// RecordTelemetryStatus records the telemetry status in the database. +// If the status changed from enabled to disabled, returns a snapshot to +// be sent to the telemetry server. +func RecordTelemetryStatus( //nolint:revive ctx context.Context, db database.Store, telemetryEnabled bool, @@ -235,7 +235,7 @@ func RecordTelemetryStatusChange( //nolint:revive } func (r *remoteReporter) runSnapshotter() { - telemetryDisabledSnapshot, err := RecordTelemetryStatusChange(r.ctx, r.options.Database, r.Enabled()) + telemetryDisabledSnapshot, err := RecordTelemetryStatus(r.ctx, r.options.Database, r.Enabled()) if err != nil { r.options.Logger.Debug(r.ctx, "record and maybe report telemetry status", slog.Error(err)) } diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 915afc624c537..3e6f95fd6e9bc 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -382,7 +382,7 @@ func TestShouldReportTelemetryDisabled(t *testing.T) { require.False(t, telemetry.ShouldReportTelemetryDisabled(&boolFalse, false)) } -func TestRecordTelemetryStatusChange(t *testing.T) { +func TestRecordTelemetryStatus(t *testing.T) { t.Parallel() for _, testCase := range []struct { name string @@ -408,7 +408,7 @@ func TestRecordTelemetryStatusChange(t *testing.T) { Value: testCase.recordedTelemetryEnabled, }) } - snapshot1, err := telemetry.RecordTelemetryStatusChange(ctx, db, testCase.telemetryEnabled) + snapshot1, err := telemetry.RecordTelemetryStatus(ctx, db, testCase.telemetryEnabled) require.NoError(t, err) if testCase.shouldReport { @@ -421,7 +421,7 @@ func TestRecordTelemetryStatusChange(t *testing.T) { for i := 0; i < 3; i++ { // Whatever happens, subsequent calls should not report if telemetryEnabled didn't change - snapshot2, err := telemetry.RecordTelemetryStatusChange(ctx, db, testCase.telemetryEnabled) + snapshot2, err := telemetry.RecordTelemetryStatus(ctx, db, testCase.telemetryEnabled) require.NoError(t, err) require.Nil(t, snapshot2) } From e032cce077bd69fa41558d148f8598d43c8340ef Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 21:43:54 +0000 Subject: [PATCH 19/22] update comment --- coderd/telemetry/telemetry.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index ea93b8464c8e6..08950691efab4 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -219,10 +219,9 @@ func RecordTelemetryStatus( //nolint:revive if !shouldReport { return nil, nil //nolint:nilnil } - // If any of the following calls fail, we will never report the disabled telemetry. - // Subsequent function calls will see the TelemetryDisabled item - // and quit early. This is okay. We only want to ping the telemetry server once, - // and never again. If that attempt fails, so be it. + // If any of the following calls fail, we will never report that telemetry changed + // from enabled to disabled. This is okay. We only want to ping the telemetry server + // once, and never again. If that attempt fails, so be it. item, err = db.GetTelemetryItem(ctx, string(TelemetryItemKeyTelemetryEnabled)) if err != nil { return nil, xerrors.Errorf("get telemetry enabled after upsert: %w", err) From a7fe67050175b9f640a85de3116328b3af22c759 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 21:54:47 +0000 Subject: [PATCH 20/22] lint --- coderd/telemetry/telemetry_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 3e6f95fd6e9bc..3ff7c8dc75060 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -397,6 +397,7 @@ func TestRecordTelemetryStatus(t *testing.T) { {name: "Telemetry was disabled now is enabled", recordedTelemetryEnabled: "0", telemetryEnabled: true, shouldReport: false}, {name: "Telemetry was disabled still disabled", recordedTelemetryEnabled: "0", telemetryEnabled: false, shouldReport: false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() From b4768fae457546260ca3c1f38c7ddb32777b7625 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 31 Jan 2025 22:16:32 +0000 Subject: [PATCH 21/22] fix test --- cli/server.go | 2 +- coderd/telemetry/telemetry.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/server.go b/cli/server.go index ebcc74156ef86..41a957815fcd7 100644 --- a/cli/server.go +++ b/cli/server.go @@ -786,7 +786,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("remove secrets from deployment values: %w", err) } telemetryReporter, err := telemetry.New(telemetry.Options{ - Enabled: vals.Telemetry.Enable.Value(), + Disabled: !vals.Telemetry.Enable.Value(), BuiltinPostgres: builtinPostgres, DeploymentID: deploymentID, Database: options.Database, diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 08950691efab4..433d55557aaac 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -42,7 +42,7 @@ const ( ) type Options struct { - Enabled bool + Disabled bool Database database.Store Logger slog.Logger // URL is an endpoint to direct telemetry towards! @@ -118,7 +118,7 @@ type remoteReporter struct { } func (r *remoteReporter) Enabled() bool { - return r.options.Enabled + return !r.options.Disabled } func (r *remoteReporter) Report(snapshot *Snapshot) { From a2846d53f11419669ddcbe5c6f202d253c4d9510 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 3 Feb 2025 13:29:58 +0000 Subject: [PATCH 22/22] replace "1" and "0" with "true" and "false" --- cli/server_test.go | 2 +- coderd/telemetry/telemetry.go | 19 +++++++++++-------- coderd/telemetry/telemetry_test.go | 16 +++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/cli/server_test.go b/cli/server_test.go index 952247fca3bc4..988fde808dc5c 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -2096,7 +2096,7 @@ func TestServer_TelemetryDisabled_FinalReport(t *testing.T) { case ss := <-snapshot: require.Len(t, ss.TelemetryItems, 1) require.Equal(t, string(telemetry.TelemetryItemKeyTelemetryEnabled), ss.TelemetryItems[0].Key) - require.Equal(t, "0", ss.TelemetryItems[0].Value) + require.Equal(t, "false", ss.TelemetryItems[0].Value) case <-time.After(testutil.WaitShort / 2): t.Fatalf("timed out waiting for snapshot") } diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go index 433d55557aaac..78819b0c65462 100644 --- a/coderd/telemetry/telemetry.go +++ b/coderd/telemetry/telemetry.go @@ -15,6 +15,7 @@ import ( "regexp" "runtime" "slices" + "strconv" "strings" "sync" "time" @@ -190,6 +191,7 @@ func ShouldReportTelemetryDisabled(recordedTelemetryEnabled *bool, telemetryEnab // be sent to the telemetry server. func RecordTelemetryStatus( //nolint:revive ctx context.Context, + logger slog.Logger, db database.Store, telemetryEnabled bool, ) (*Snapshot, error) { @@ -199,18 +201,19 @@ func RecordTelemetryStatus( //nolint:revive } var recordedTelemetryEnabled *bool if !errors.Is(err, sql.ErrNoRows) { - value := item.Value == "1" + value, err := strconv.ParseBool(item.Value) + if err != nil { + logger.Debug(ctx, "parse telemetry enabled", slog.Error(err)) + } + // If ParseBool fails, value will default to false. + // This may happen if an admin manually edits the telemetry item + // in the database. recordedTelemetryEnabled = &value } - newValue := "0" - if telemetryEnabled { - newValue = "1" - } - if err := db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ Key: string(TelemetryItemKeyTelemetryEnabled), - Value: newValue, + Value: strconv.FormatBool(telemetryEnabled), }); err != nil { return nil, xerrors.Errorf("upsert telemetry enabled: %w", err) } @@ -234,7 +237,7 @@ func RecordTelemetryStatus( //nolint:revive } func (r *remoteReporter) runSnapshotter() { - telemetryDisabledSnapshot, err := RecordTelemetryStatus(r.ctx, r.options.Database, r.Enabled()) + telemetryDisabledSnapshot, err := RecordTelemetryStatus(r.ctx, r.options.Logger, r.options.Database, r.Enabled()) if err != nil { r.options.Logger.Debug(r.ctx, "record and maybe report telemetry status", slog.Error(err)) } diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go index 3ff7c8dc75060..29fcb644fc88f 100644 --- a/coderd/telemetry/telemetry_test.go +++ b/coderd/telemetry/telemetry_test.go @@ -392,10 +392,11 @@ func TestRecordTelemetryStatus(t *testing.T) { }{ {name: "New deployment", recordedTelemetryEnabled: "nil", telemetryEnabled: true, shouldReport: false}, {name: "Telemetry disabled", recordedTelemetryEnabled: "nil", telemetryEnabled: false, shouldReport: false}, - {name: "Telemetry was enabled and still is", recordedTelemetryEnabled: "1", telemetryEnabled: true, shouldReport: false}, - {name: "Telemetry was enabled but now disabled", recordedTelemetryEnabled: "1", telemetryEnabled: false, shouldReport: true}, - {name: "Telemetry was disabled now is enabled", recordedTelemetryEnabled: "0", telemetryEnabled: true, shouldReport: false}, - {name: "Telemetry was disabled still disabled", recordedTelemetryEnabled: "0", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was enabled and still is", recordedTelemetryEnabled: "true", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was enabled but now disabled", recordedTelemetryEnabled: "true", telemetryEnabled: false, shouldReport: true}, + {name: "Telemetry was disabled now is enabled", recordedTelemetryEnabled: "false", telemetryEnabled: true, shouldReport: false}, + {name: "Telemetry was disabled still disabled", recordedTelemetryEnabled: "false", telemetryEnabled: false, shouldReport: false}, + {name: "Telemetry was disabled still disabled, invalid value", recordedTelemetryEnabled: "invalid", telemetryEnabled: false, shouldReport: false}, } { testCase := testCase t.Run(testCase.name, func(t *testing.T) { @@ -403,26 +404,27 @@ func TestRecordTelemetryStatus(t *testing.T) { db, _ := dbtestutil.NewDB(t) ctx := testutil.Context(t, testutil.WaitMedium) + logger := testutil.Logger(t) if testCase.recordedTelemetryEnabled != "nil" { db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{ Key: string(telemetry.TelemetryItemKeyTelemetryEnabled), Value: testCase.recordedTelemetryEnabled, }) } - snapshot1, err := telemetry.RecordTelemetryStatus(ctx, db, testCase.telemetryEnabled) + snapshot1, err := telemetry.RecordTelemetryStatus(ctx, logger, db, testCase.telemetryEnabled) require.NoError(t, err) if testCase.shouldReport { require.NotNil(t, snapshot1) require.Equal(t, snapshot1.TelemetryItems[0].Key, string(telemetry.TelemetryItemKeyTelemetryEnabled)) - require.Equal(t, snapshot1.TelemetryItems[0].Value, "0") + require.Equal(t, snapshot1.TelemetryItems[0].Value, "false") } else { require.Nil(t, snapshot1) } for i := 0; i < 3; i++ { // Whatever happens, subsequent calls should not report if telemetryEnabled didn't change - snapshot2, err := telemetry.RecordTelemetryStatus(ctx, db, testCase.telemetryEnabled) + snapshot2, err := telemetry.RecordTelemetryStatus(ctx, logger, db, testCase.telemetryEnabled) require.NoError(t, err) require.Nil(t, snapshot2) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy