Skip to content

Commit

Permalink
Telemetry (turn off with DO_NOT_TRACK env var) (#671)
Browse files Browse the repository at this point in the history
Introducing anonymous telemetry that respects common ENV var
`DO_NOT_TRACK=true` or `DO_NOT_TRACK=1`.
  • Loading branch information
sourishkrout authored Sep 19, 2024
1 parent 54acee3 commit ca6efae
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
3 changes: 3 additions & 0 deletions internal/cmd/beta/server/server_start_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stateful/runme/v3/internal/config"
"github.com/stateful/runme/v3/internal/config/autoconfig"
"github.com/stateful/runme/v3/internal/server"
"github.com/stateful/runme/v3/internal/telemetry"
)

func serverStartCmd() *cobra.Command {
Expand All @@ -33,6 +34,8 @@ func serverStartCmd() *cobra.Command {
TLSEnabled: cfg.ServerTLSEnabled,
}

_ = telemetry.ReportUnlessNoTracking(logger)

logger.Debug("server config", zap.Any("config", serverCfg))

s, err := server.New(serverCfg, cmdFactory, logger)
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/stateful/runme/v3/internal/project/projectservice"
"github.com/stateful/runme/v3/internal/runner"
runnerv2service "github.com/stateful/runme/v3/internal/runnerv2service"
"github.com/stateful/runme/v3/internal/telemetry"
runmetls "github.com/stateful/runme/v3/internal/tls"
parserv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/parser/v1"
projectv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/project/v1"
Expand Down Expand Up @@ -95,6 +96,8 @@ The kernel is used to run long running processes like shells and interacting wit
return err
}

_ = telemetry.ReportUnlessNoTracking(logger)

logger.Info("started listening", zap.String("addr", lis.Addr().String()))

const maxMsgSize = 100 * 1024 * 1024 // 100 MiB
Expand Down
110 changes: 110 additions & 0 deletions internal/telemetry/scarf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package telemetry

import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/pkg/errors"
"go.uber.org/zap"
)

const (
base = "https://home.runme.dev/"
client = "Kernel"
)

type LookupEnv func(key string) (string, bool)

// Returns true if telemetry reporting is enabled, false otherwise.
func ReportUnlessNoTracking(logger *zap.Logger) bool {
if v := os.Getenv("DO_NOT_TRACK"); v != "" && v != "0" && v != "false" {
logger.Info("Telemetry reporting is disabled with DO_NOT_TRACK")
return false
}

if v := os.Getenv("SCARF_NO_ANALYTICS"); v != "" && v != "0" && v != "false" {
logger.Info("Telemetry reporting is disabled with SCARF_NO_ANALYTICS")
return false
}

logger.Info("Telemetry reporting is enabled")

go func() {
err := report()
if err != nil {
logger.Warn("Error reporting telemetry", zap.Error(err))
}
}()

return true
}

func report() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

encodedURL, err := buildURL(os.LookupEnv, client)
if err != nil {
return errors.Wrapf(err, "Error building telemtry URL")
}

req, err := http.NewRequestWithContext(ctx, "GET", encodedURL.String(), nil)
if err != nil {
return errors.Wrapf(err, "Error creating telemetry request")
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return errors.Wrapf(err, "Error sending telemetry request")
}
defer resp.Body.Close()

if resp.StatusCode >= 400 {
return fmt.Errorf("error sending telemetry request: status_code=%d, status=%s", resp.StatusCode, resp.Status)
}

return nil
}

func buildURL(lookup LookupEnv, client string) (*url.URL, error) {
baseAndClient := base + client

props := []string{
"extname",
"extversion",
"remotename",
"appname",
"product",
"platform",
"uikind",
}

params := url.Values{}
for _, p := range props {
addValue(lookup, &params, p)
}

// until we have a non-extension-bundled reporting strategy, lets error
if len(params) == 0 {
return nil, fmt.Errorf("no telemetry properties provided")
}

dst, err := url.Parse(baseAndClient)
if err != nil {
return nil, err
}
dst.RawQuery = params.Encode()

return dst, nil
}

func addValue(lookup LookupEnv, params *url.Values, prop string) {
if v, ok := lookup(fmt.Sprintf("TELEMETRY_%s", strings.ToUpper(prop))); ok {
params.Add(strings.ToLower(prop), v)
}
}
71 changes: 71 additions & 0 deletions internal/telemetry/scarf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package telemetry

import (
"os"
"testing"

"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestReportUnlessNoTracking(t *testing.T) {
t.Run("Track", func(t *testing.T) {
logger := zap.NewNop()
require.True(t, ReportUnlessNoTracking(logger))
})

t.Run("DO_NOT_TRACK", func(t *testing.T) {
logger := zap.NewNop()
t.Setenv("DO_NOT_TRACK", "true")
require.False(t, ReportUnlessNoTracking(logger))
})

t.Run("SCARF_NO_ANALYTICS", func(t *testing.T) {
logger := zap.NewNop()
t.Setenv("SCARF_NO_ANALYTICS", "true")
defer os.Unsetenv("SCARF_NO_ANALYTICS")
require.False(t, ReportUnlessNoTracking(logger))
})
}

func TestUrlBuilder(t *testing.T) {
t.Parallel()

t.Run("Full", func(t *testing.T) {
lookupEnv := createLookup(map[string]string{
"TELEMETRY_EXTNAME": "stateful.runme",
"TELEMETRY_EXTVERSION": "3.7.7-dev.10",
"TELEMETRY_REMOTENAME": "none",
"TELEMETRY_APPNAME": "Visual Studio Code",
"TELEMETRY_PRODUCT": "desktop",
"TELEMETRY_PLATFORM": "darwin_arm64",
"TELEMETRY_UIKIND": "desktop",
})
dst, err := buildURL(lookupEnv, "Kernel")
require.NoError(t, err)
require.Equal(t, "https://home.runme.dev/Kernel?appname=Visual+Studio+Code&extname=stateful.runme&extversion=3.7.7-dev.10&platform=darwin_arm64&product=desktop&remotename=none&uikind=desktop", dst.String())
})

t.Run("Partial", func(t *testing.T) {
lookupEnv := createLookup(map[string]string{
"TELEMETRY_EXTNAME": "stateful.runme",
"TELEMETRY_PLATFORM": "linux_x64",
})
dst, err := buildURL(lookupEnv, "Kernel")
require.NoError(t, err)
require.Equal(t, "https://home.runme.dev/Kernel?extname=stateful.runme&platform=linux_x64", dst.String())
})

t.Run("Empty", func(t *testing.T) {
lookupEnv := createLookup(map[string]string{})
_, err := buildURL(lookupEnv, "Kernel")
require.Error(t, err, "no telemetry properties provided")
})
}

func createLookup(fixture map[string]string) func(string) (string, bool) {
return func(key string) (string, bool) {
value, ok := fixture[key]
return value, ok
}
}

0 comments on commit ca6efae

Please sign in to comment.