Skip to content

Commit

Permalink
feat(telemetry): add telemetry command
Browse files Browse the repository at this point in the history
  • Loading branch information
Integralist committed Sep 18, 2023
1 parent 85c23e1 commit 3420dad
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 3 deletions.
7 changes: 6 additions & 1 deletion .fastly/config.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
config_version = 3
config_version = 4

[fastly]
api_endpoint = "https://api.fastly.com"

[telemetry]
build_info = "disable"
machine_info = "disable"
package_info = "disable"

[language]
[language.go]
tinygo_constraint = ">= 0.28.1-0" # NOTE -0 indicates to the CLI's semver package that we accept pre-releases (TinyGo users commonly use pre-releases).
Expand Down
3 changes: 3 additions & 0 deletions pkg/app/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
"github.com/fastly/cli/pkg/commands/serviceversion"
"github.com/fastly/cli/pkg/commands/shellcomplete"
"github.com/fastly/cli/pkg/commands/stats"
"github.com/fastly/cli/pkg/commands/telemetry"
tlsConfig "github.com/fastly/cli/pkg/commands/tls/config"
tlsCustom "github.com/fastly/cli/pkg/commands/tls/custom"
tlsCustomActivation "github.com/fastly/cli/pkg/commands/tls/custom/activation"
Expand Down Expand Up @@ -383,6 +384,7 @@ func defineCommands(
statsHistorical := stats.NewHistoricalCommand(statsCmdRoot.CmdClause, g, m)
statsRealtime := stats.NewRealtimeCommand(statsCmdRoot.CmdClause, g, m)
statsRegions := stats.NewRegionsCommand(statsCmdRoot.CmdClause, g)
telemetryCmdRoot := telemetry.NewRootCommand(app, g)
tlsConfigCmdRoot := tlsConfig.NewRootCommand(app, g)
tlsConfigDescribe := tlsConfig.NewDescribeCommand(tlsConfigCmdRoot.CmdClause, g, m)
tlsConfigList := tlsConfig.NewListCommand(tlsConfigCmdRoot.CmdClause, g, m)
Expand Down Expand Up @@ -743,6 +745,7 @@ func defineCommands(
statsHistorical,
statsRealtime,
statsRegions,
telemetryCmdRoot,
tlsConfigCmdRoot,
tlsConfigDescribe,
tlsConfigList,
Expand Down
3 changes: 2 additions & 1 deletion pkg/app/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ service
service-auth
service-version
stats
telemetry
tls-config
tls-custom
tls-platform
Expand Down Expand Up @@ -116,7 +117,7 @@ whoami

go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
_, _ = io.Copy(&buf, r)
outC <- buf.String()
}()

Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/ip/ip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"bytes"
"testing"

"github.com/fastly/go-fastly/v8/fastly"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/mock"
"github.com/fastly/cli/pkg/testutil"
"github.com/fastly/go-fastly/v8/fastly"
)

func TestAllIPs(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/commands/telemetry/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package telemetry contains a command to control what telemetry data is
// recorded.
package telemetry
96 changes: 96 additions & 0 deletions pkg/commands/telemetry/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package telemetry

import (
"fmt"
"io"

"github.com/fastly/cli/pkg/cmd"
"github.com/fastly/cli/pkg/config"
fsterr "github.com/fastly/cli/pkg/errors"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
)

// RootCommand is the parent command for all subcommands in this package.
// It should be installed under the primary root command.
type RootCommand struct {
cmd.Base

disable bool
disableBuild bool
disableMachine bool
disablePackage bool
enable bool
enableBuild bool
enableMachine bool
enablePackage bool
}

// NewRootCommand returns a new command registered in the parent.
func NewRootCommand(parent cmd.Registerer, g *global.Data) *RootCommand {
var c RootCommand
c.Globals = g
c.CmdClause = parent.Command("telemetry", "Control what telemetry data is recorded")
c.CmdClause.Flag("disable", "Disable all telemetry").BoolVar(&c.disable)
c.CmdClause.Flag("disable-build", "Disable telemetry for information regarding the time taken for builds and compilation processes").BoolVar(&c.disableBuild)
c.CmdClause.Flag("disable-machine", "Disable telemetry for general, non-identifying system specifications (CPU, RAM, operating system)").BoolVar(&c.disableMachine)
c.CmdClause.Flag("disable-package", "Disable telemetry for packages and libraries utilized in your source code").BoolVar(&c.disablePackage)
c.CmdClause.Flag("enable", "Enable all telemetry").BoolVar(&c.enable)
c.CmdClause.Flag("enable-build", "Enable telemetry for information regarding the time taken for builds and compilation processes").BoolVar(&c.enableBuild)
c.CmdClause.Flag("enable-machine", "Enable telemetry for general, non-identifying system specifications (CPU, RAM, operating system)").BoolVar(&c.enableMachine)
c.CmdClause.Flag("enable-package", "Enable telemetry for packages and libraries utilized in your source code").BoolVar(&c.enablePackage)
return &c
}

// Exec implements the command interface.
func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error {
if c.disable && c.enable {
return fsterr.ErrInvalidTelemetryEnableDisableCombo
}
if c.disable {
c.Globals.Config.Telemetry = toggleAll("disable")
}
if c.enable {
c.Globals.Config.Telemetry = toggleAll("enable")
}
if c.disable && (c.enableBuild || c.enableMachine || c.enablePackage) {
text.Info(out, "We will disable all telemetry except for the specified `--enable-*` flags")
text.Break(out)
}
if c.enable && (c.disableBuild || c.disableMachine || c.disablePackage) {
text.Info(out, "We will enable all telemetry except for the specified `--disable-*` flags")
text.Break(out)
}
if c.enableBuild {
c.Globals.Config.Telemetry.BuildInfo = "enable"
}
if c.enableMachine {
c.Globals.Config.Telemetry.MachineInfo = "enable"
}
if c.enablePackage {
c.Globals.Config.Telemetry.PackageInfo = "enable"
}
if c.disableBuild {
c.Globals.Config.Telemetry.BuildInfo = "disable"
}
if c.disableMachine {
c.Globals.Config.Telemetry.MachineInfo = "disable"
}
if c.disablePackage {
c.Globals.Config.Telemetry.PackageInfo = "disable"
}
err := c.Globals.Config.Write(c.Globals.ConfigPath)
if err != nil {
return fmt.Errorf("failed to persist telemetry choices to disk: %w", err)
}
text.Success(out, "configuration updated (see: `fastly config`)")
return nil
}

func toggleAll(state string) config.Telemetry {
var t config.Telemetry
t.BuildInfo = state
t.MachineInfo = state
t.PackageInfo = state
return t
}
145 changes: 145 additions & 0 deletions pkg/commands/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package telemetry_test

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

toml "github.com/pelletier/go-toml"

"github.com/fastly/cli/pkg/app"
"github.com/fastly/cli/pkg/config"
"github.com/fastly/cli/pkg/revision"
"github.com/fastly/cli/pkg/testutil"
)

// Scenario is an extension of the base TestScenario.
// It includes manipulating stdin.
type Scenario struct {
testutil.TestScenario

ExpectedConfig config.Telemetry
}

func TestTelemetry(t *testing.T) {
var (
configPath string
data []byte
)

// Create temp environment to run test code within.
{
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}

// Read the test config.toml data
path, err := filepath.Abs(filepath.Join("./", "testdata", "config.toml"))
if err != nil {
t.Fatal(err)
}
data, err = os.ReadFile(path)
if err != nil {
t.Fatal(err)
}

// Create a new test environment along with a test config.toml file.
rootdir := testutil.NewEnv(testutil.EnvOpts{
T: t,
Write: []testutil.FileIO{
{Src: string(data), Dst: "config.toml"},
},
})
configPath = filepath.Join(rootdir, "config.toml")
defer os.RemoveAll(rootdir)

if err := os.Chdir(rootdir); err != nil {
t.Fatal(err)
}
defer os.Chdir(wd)
}

args := testutil.Args
scenarios := []Scenario{
{
TestScenario: testutil.TestScenario{
Args: args("telemetry --enable"),
WantOutput: "SUCCESS: configuration updated (see: `fastly config`)",
},
ExpectedConfig: config.Telemetry{
BuildInfo: "enable",
MachineInfo: "enable",
PackageInfo: "enable",
},
},
{
TestScenario: testutil.TestScenario{
Args: args("telemetry --disable"),
WantOutput: "SUCCESS: configuration updated (see: `fastly config`)",
},
ExpectedConfig: config.Telemetry{
BuildInfo: "disable",
MachineInfo: "disable",
PackageInfo: "disable",
},
},
}

for testcaseIdx := range scenarios {
testcase := &scenarios[testcaseIdx]
t.Run(testcase.Name, func(t *testing.T) {
var stdout bytes.Buffer

opts := testutil.NewRunOpts(testcase.Args, &stdout)

// We override the config path so that we don't accidentally write over
// our own configuration file.
opts.ConfigPath = configPath

// We read the static/embedded config so we can get the latest config
// version and so we don't accidentally switch to the UseStatic() version.
var staticConfig config.File
err := toml.Unmarshal(config.Static, &staticConfig)
if err != nil {
t.Error(err)
}

// The read of the config file only happens in the main() function, so for
// the sake of the test environment we need to construct an in-memory
// representation of the config file we want to be using.
opts.ConfigFile = config.File{
ConfigVersion: staticConfig.ConfigVersion,
CLI: config.CLI{
Version: revision.SemVer(revision.AppVersion),
},
}

err = app.Run(opts)

t.Log(stdout.String())

testutil.AssertErrorContains(t, err, testcase.WantError)
testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput)

in := strings.NewReader("")
verboseMode := false
err = opts.ConfigFile.Read(configPath, in, opts.Stdout, opts.ErrLog, verboseMode)
if err != nil {
t.Error(err)
}

if opts.ConfigFile.Telemetry.BuildInfo != testcase.ExpectedConfig.BuildInfo {
t.Errorf("want: %s, got: %s", testcase.ExpectedConfig.BuildInfo, opts.ConfigFile.Telemetry.BuildInfo)
}
if opts.ConfigFile.Telemetry.MachineInfo != testcase.ExpectedConfig.MachineInfo {
t.Errorf("want: %s, got: %s", testcase.ExpectedConfig.MachineInfo, opts.ConfigFile.Telemetry.MachineInfo)
}
if opts.ConfigFile.Telemetry.PackageInfo != testcase.ExpectedConfig.PackageInfo {
t.Errorf("want: %s, got: %s", testcase.ExpectedConfig.PackageInfo, opts.ConfigFile.Telemetry.PackageInfo)
}
})
}
}
4 changes: 4 additions & 0 deletions pkg/commands/telemetry/testdata/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[telemetry]
build_info = "disable"
machine_info = "disable"
package_info = "disable"
17 changes: 17 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ type Fastly struct {
APIEndpoint string `toml:"api_endpoint"`
}

// Telemetry represents what telemetry data will be recorded.
type Telemetry struct {
// BuildInfo represents information regarding the time taken for builds and
// compilation processes, helping us identify bottlenecks and optimize
// performance (enable/disable).
BuildInfo string `toml:"build_info"`
// MachineInfo represents general, non-identifying system specifications (CPU,
// RAM, operating system) to better understand the hardware landscape our CLI
// operates in (enable/disable).
MachineInfo string `toml:"machine_info"`
// PackageInfo represents packages and libraries utilized in your source code,
// enabling us to prioritize support for the most commonly used components
// (enable/disable).
PackageInfo string `toml:"package_info"`
}

// CLI represents CLI specific configuration.
type CLI struct {
Version string `toml:"version"`
Expand Down Expand Up @@ -151,6 +167,7 @@ type File struct {
CLI CLI `toml:"cli"`
ConfigVersion int `toml:"config_version"`
Fastly Fastly `toml:"fastly"`
Telemetry Telemetry `toml:"telemetry"`
Language Language `toml:"language"`
Profiles Profiles `toml:"profile"`
StarterKits StarterKitLanguages `toml:"starter-kits"`
Expand Down
7 changes: 7 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,10 @@ var ErrInvalidStdinFileDirCombo = RemediationError{
Inner: fmt.Errorf("invalid flag combination"),
Remediation: "Use only one of --stdin, --file or --dir.",
}

// ErrInvalidTelemetryEnableDisableCombo means the user provided both a --enable
// and --disable flag which are mutually exclusive behaviours.
var ErrInvalidTelemetryEnableDisableCombo = RemediationError{
Inner: fmt.Errorf("invalid flag combination, --enable and --disable"),
Remediation: "Use either --enable or --disable, not both.",
}

0 comments on commit 3420dad

Please sign in to comment.