Skip to content

Commit 4cce969

Browse files
authored
feat: Add anonymized telemetry to report product usage (#2273)
* feat: Add anonymized telemetry to report product usage This adds a background service to report telemetry to a Coder server for usage data. There will be realtime event data sent in the future, but for now usage will report on a CRON. * Fix flake and requested changes * Add reporting options for setup * Add reporting for workspaces * Add resources as they are reported * Track API key usage * Ensure telemetry is tracked prior to exit
1 parent af8a1e3 commit 4cce969

33 files changed

+1674
-95
lines changed

cli/server.go

+48
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/coreos/go-systemd/daemon"
3030
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
3131
"github.com/google/go-github/v43/github"
32+
"github.com/google/uuid"
3233
"github.com/pion/turn/v2"
3334
"github.com/pion/webrtc/v3"
3435
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -53,6 +54,7 @@ import (
5354
"github.com/coder/coder/coderd/database/databasefake"
5455
"github.com/coder/coder/coderd/devtunnel"
5556
"github.com/coder/coder/coderd/gitsshkey"
57+
"github.com/coder/coder/coderd/telemetry"
5658
"github.com/coder/coder/coderd/tracing"
5759
"github.com/coder/coder/coderd/turnconn"
5860
"github.com/coder/coder/codersdk"
@@ -81,6 +83,7 @@ func server() *cobra.Command {
8183
oauth2GithubClientSecret string
8284
oauth2GithubAllowedOrganizations []string
8385
oauth2GithubAllowSignups bool
86+
telemetryURL string
8487
tlsCertFile string
8588
tlsClientCAFile string
8689
tlsClientAuth string
@@ -134,6 +137,7 @@ func server() *cobra.Command {
134137
}
135138

136139
config := createConfig(cmd)
140+
builtinPostgres := false
137141
// Only use built-in if PostgreSQL URL isn't specified!
138142
if !inMemoryDatabase && postgresURL == "" {
139143
var closeFunc func() error
@@ -142,6 +146,7 @@ func server() *cobra.Command {
142146
if err != nil {
143147
return err
144148
}
149+
builtinPostgres = true
145150
defer func() {
146151
// Gracefully shut PostgreSQL down!
147152
_ = closeFunc()
@@ -253,6 +258,7 @@ func server() *cobra.Command {
253258
SSHKeygenAlgorithm: sshKeygenAlgorithm,
254259
TURNServer: turnServer,
255260
TracerProvider: tracerProvider,
261+
Telemetry: telemetry.NewNoop(),
256262
}
257263

258264
if oauth2GithubClientSecret != "" {
@@ -285,6 +291,44 @@ func server() *cobra.Command {
285291
}
286292
}
287293

294+
deploymentID, err := options.Database.GetDeploymentID(cmd.Context())
295+
if errors.Is(err, sql.ErrNoRows) {
296+
err = nil
297+
}
298+
if err != nil {
299+
return xerrors.Errorf("get deployment id: %w", err)
300+
}
301+
if deploymentID == "" {
302+
deploymentID = uuid.NewString()
303+
err = options.Database.InsertDeploymentID(cmd.Context(), deploymentID)
304+
if err != nil {
305+
return xerrors.Errorf("set deployment id: %w", err)
306+
}
307+
}
308+
309+
// Parse the raw telemetry URL!
310+
telemetryURL, err := url.Parse(telemetryURL)
311+
if err != nil {
312+
return xerrors.Errorf("parse telemetry url: %w", err)
313+
}
314+
if !inMemoryDatabase || cmd.Flags().Changed("telemetry-url") {
315+
options.Telemetry, err = telemetry.New(telemetry.Options{
316+
BuiltinPostgres: builtinPostgres,
317+
DeploymentID: deploymentID,
318+
Database: options.Database,
319+
Logger: logger.Named("telemetry"),
320+
URL: telemetryURL,
321+
GitHubOAuth: oauth2GithubClientID != "",
322+
Prometheus: promEnabled,
323+
STUN: len(stunServers) != 0,
324+
Tunnel: tunnel,
325+
})
326+
if err != nil {
327+
return xerrors.Errorf("create telemetry reporter: %w", err)
328+
}
329+
defer options.Telemetry.Close()
330+
}
331+
288332
coderAPI := coderd.New(options)
289333
client := codersdk.New(localURL)
290334
if tlsEnable {
@@ -438,6 +482,8 @@ func server() *cobra.Command {
438482
<-devTunnelErrChan
439483
}
440484

485+
// Ensures a last report can be sent before exit!
486+
options.Telemetry.Close()
441487
cmd.Println("Waiting for WebSocket connections to close...")
442488
shutdownConns()
443489
coderAPI.Close()
@@ -485,6 +531,8 @@ func server() *cobra.Command {
485531
"Specifies organizations the user must be a member of to authenticate with GitHub.")
486532
cliflag.BoolVarP(root.Flags(), &oauth2GithubAllowSignups, "oauth2-github-allow-signups", "", "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS", false,
487533
"Specifies whether new users can sign up with GitHub.")
534+
cliflag.StringVarP(root.Flags(), &telemetryURL, "telemetry-url", "", "CODER_TELEMETRY_URL", "https://telemetry.coder.com", "Specifies a URL to send telemetry to.")
535+
_ = root.Flags().MarkHidden("telemetry-url")
488536
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false, "Specifies if TLS will be enabled")
489537
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
490538
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. "+

cli/server_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,27 @@ import (
88
"crypto/tls"
99
"crypto/x509"
1010
"crypto/x509/pkix"
11+
"encoding/json"
1112
"encoding/pem"
1213
"math/big"
1314
"net"
1415
"net/http"
16+
"net/http/httptest"
1517
"net/url"
1618
"os"
1719
"runtime"
1820
"strings"
1921
"testing"
2022
"time"
2123

24+
"github.com/go-chi/chi"
2225
"github.com/stretchr/testify/assert"
2326
"github.com/stretchr/testify/require"
2427
"go.uber.org/goleak"
2528

2629
"github.com/coder/coder/cli/clitest"
2730
"github.com/coder/coder/coderd/database/postgres"
31+
"github.com/coder/coder/coderd/telemetry"
2832
"github.com/coder/coder/codersdk"
2933
)
3034

@@ -233,6 +237,37 @@ func TestServer(t *testing.T) {
233237
require.ErrorIs(t, <-errC, context.Canceled)
234238
require.Error(t, goleak.Find())
235239
})
240+
t.Run("Telemetry", func(t *testing.T) {
241+
t.Parallel()
242+
ctx, cancelFunc := context.WithCancel(context.Background())
243+
defer cancelFunc()
244+
245+
deployment := make(chan struct{}, 64)
246+
snapshot := make(chan *telemetry.Snapshot, 64)
247+
r := chi.NewRouter()
248+
r.Post("/deployment", func(w http.ResponseWriter, r *http.Request) {
249+
w.WriteHeader(http.StatusAccepted)
250+
deployment <- struct{}{}
251+
})
252+
r.Post("/snapshot", func(w http.ResponseWriter, r *http.Request) {
253+
w.WriteHeader(http.StatusAccepted)
254+
ss := &telemetry.Snapshot{}
255+
err := json.NewDecoder(r.Body).Decode(ss)
256+
require.NoError(t, err)
257+
snapshot <- ss
258+
})
259+
server := httptest.NewServer(r)
260+
t.Cleanup(server.Close)
261+
262+
root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--telemetry-url", server.URL)
263+
errC := make(chan error)
264+
go func() {
265+
errC <- root.ExecuteContext(ctx)
266+
}()
267+
268+
<-deployment
269+
<-snapshot
270+
})
236271
}
237272

238273
func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {

coderd/coderd.go

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/coder/coder/coderd/httpapi"
2828
"github.com/coder/coder/coderd/httpmw"
2929
"github.com/coder/coder/coderd/rbac"
30+
"github.com/coder/coder/coderd/telemetry"
3031
"github.com/coder/coder/coderd/tracing"
3132
"github.com/coder/coder/coderd/turnconn"
3233
"github.com/coder/coder/coderd/wsconncache"
@@ -54,6 +55,7 @@ type Options struct {
5455
ICEServers []webrtc.ICEServer
5556
SecureAuthCookie bool
5657
SSHKeygenAlgorithm gitsshkey.Algorithm
58+
Telemetry telemetry.Reporter
5759
TURNServer *turnconn.Server
5860
TracerProvider *sdktrace.TracerProvider
5961
}

coderd/coderdtest/coderdtest.go

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/spf13/afero"
2929

3030
"github.com/coder/coder/coderd/rbac"
31+
"github.com/coder/coder/coderd/telemetry"
3132
"github.com/coder/coder/coderd/util/ptr"
3233

3334
"cloud.google.com/go/compute/metadata"
@@ -166,6 +167,7 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, *coderd.API)
166167
TURNServer: turnServer,
167168
APIRateLimit: options.APIRateLimit,
168169
Authorizer: options.Authorizer,
170+
Telemetry: telemetry.NewNoop(),
169171
})
170172
srv.Config.Handler = coderAPI.Handler
171173
if options.IncludeProvisionerD {

0 commit comments

Comments
 (0)