mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
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
This commit is contained in:
@ -29,6 +29,7 @@ import (
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
||||
"github.com/google/go-github/v43/github"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pion/turn/v2"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@ -53,6 +54,7 @@ import (
|
||||
"github.com/coder/coder/coderd/database/databasefake"
|
||||
"github.com/coder/coder/coderd/devtunnel"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
"github.com/coder/coder/coderd/turnconn"
|
||||
"github.com/coder/coder/codersdk"
|
||||
@ -81,6 +83,7 @@ func server() *cobra.Command {
|
||||
oauth2GithubClientSecret string
|
||||
oauth2GithubAllowedOrganizations []string
|
||||
oauth2GithubAllowSignups bool
|
||||
telemetryURL string
|
||||
tlsCertFile string
|
||||
tlsClientCAFile string
|
||||
tlsClientAuth string
|
||||
@ -134,6 +137,7 @@ func server() *cobra.Command {
|
||||
}
|
||||
|
||||
config := createConfig(cmd)
|
||||
builtinPostgres := false
|
||||
// Only use built-in if PostgreSQL URL isn't specified!
|
||||
if !inMemoryDatabase && postgresURL == "" {
|
||||
var closeFunc func() error
|
||||
@ -142,6 +146,7 @@ func server() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
builtinPostgres = true
|
||||
defer func() {
|
||||
// Gracefully shut PostgreSQL down!
|
||||
_ = closeFunc()
|
||||
@ -253,6 +258,7 @@ func server() *cobra.Command {
|
||||
SSHKeygenAlgorithm: sshKeygenAlgorithm,
|
||||
TURNServer: turnServer,
|
||||
TracerProvider: tracerProvider,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
}
|
||||
|
||||
if oauth2GithubClientSecret != "" {
|
||||
@ -285,6 +291,44 @@ func server() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
deploymentID, err := options.Database.GetDeploymentID(cmd.Context())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get deployment id: %w", err)
|
||||
}
|
||||
if deploymentID == "" {
|
||||
deploymentID = uuid.NewString()
|
||||
err = options.Database.InsertDeploymentID(cmd.Context(), deploymentID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set deployment id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the raw telemetry URL!
|
||||
telemetryURL, err := url.Parse(telemetryURL)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse telemetry url: %w", err)
|
||||
}
|
||||
if !inMemoryDatabase || cmd.Flags().Changed("telemetry-url") {
|
||||
options.Telemetry, err = telemetry.New(telemetry.Options{
|
||||
BuiltinPostgres: builtinPostgres,
|
||||
DeploymentID: deploymentID,
|
||||
Database: options.Database,
|
||||
Logger: logger.Named("telemetry"),
|
||||
URL: telemetryURL,
|
||||
GitHubOAuth: oauth2GithubClientID != "",
|
||||
Prometheus: promEnabled,
|
||||
STUN: len(stunServers) != 0,
|
||||
Tunnel: tunnel,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create telemetry reporter: %w", err)
|
||||
}
|
||||
defer options.Telemetry.Close()
|
||||
}
|
||||
|
||||
coderAPI := coderd.New(options)
|
||||
client := codersdk.New(localURL)
|
||||
if tlsEnable {
|
||||
@ -438,6 +482,8 @@ func server() *cobra.Command {
|
||||
<-devTunnelErrChan
|
||||
}
|
||||
|
||||
// Ensures a last report can be sent before exit!
|
||||
options.Telemetry.Close()
|
||||
cmd.Println("Waiting for WebSocket connections to close...")
|
||||
shutdownConns()
|
||||
coderAPI.Close()
|
||||
@ -485,6 +531,8 @@ func server() *cobra.Command {
|
||||
"Specifies organizations the user must be a member of to authenticate with GitHub.")
|
||||
cliflag.BoolVarP(root.Flags(), &oauth2GithubAllowSignups, "oauth2-github-allow-signups", "", "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS", false,
|
||||
"Specifies whether new users can sign up with GitHub.")
|
||||
cliflag.StringVarP(root.Flags(), &telemetryURL, "telemetry-url", "", "CODER_TELEMETRY_URL", "https://telemetry.coder.com", "Specifies a URL to send telemetry to.")
|
||||
_ = root.Flags().MarkHidden("telemetry-url")
|
||||
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false, "Specifies if TLS will be enabled")
|
||||
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
|
||||
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. "+
|
||||
|
@ -8,10 +8,12 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
@ -19,12 +21,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/coderd/database/postgres"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
@ -233,6 +237,37 @@ func TestServer(t *testing.T) {
|
||||
require.ErrorIs(t, <-errC, context.Canceled)
|
||||
require.Error(t, goleak.Find())
|
||||
})
|
||||
t.Run("Telemetry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
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)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
root, _ := clitest.New(t, "server", "--in-memory", "--address", ":0", "--telemetry-url", server.URL)
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
errC <- root.ExecuteContext(ctx)
|
||||
}()
|
||||
|
||||
<-deployment
|
||||
<-snapshot
|
||||
})
|
||||
}
|
||||
|
||||
func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {
|
||||
|
Reference in New Issue
Block a user