Repository: stripe/stripe-cli
Pull request #236 (2019-10-14 18:47:01)

Telemetry: command path
 ### Reviewers
r? @tomer-stripe @brandur-stripe 
cc @stripe/dev-platform

 ### Summary
Add very basic telemetry: send the command path (e.g. `charges create`, `listen`, etc.) in API requests, in a new `Stripe-CLI-Telemetry` header.

The header contains a JSON string, so we can easily add more information in there.

Users can opt out of telemetry by setting `STRIPE_CLI_TELEMETRY_OPTOUT` to `1` or `true`.


ob-stripe: @tomer-stripe Can you take another look? I've changed the way it works to only send the command name, but no arguments or flags.

ptal @tomer-stripe 
tomer-stripe: lgtm!

Patch:
diff --git a/README.md b/README.md
index 1181c52..5680c0d 100644
--- a/README.md
+++ b/README.md
@@ -366,6 +366,10 @@ All of the available shortcuts are listed in the [wiki page](https://github.com/
 
 For dashboard pages, you can also add the `--livemode` flag to open the page directly in live mode.
 
+## Telemetry
+
+The Stripe CLI includes a telemetry feature that collects some usage data. This feature is enabled by default. To opt out of the telemetry feature, set the `STRIPE_CLI_TELEMETRY_OPTOUT` environment variable to `1` or `true`.
+
 ## Developing the Stripe CLI
 
 If you're working on developing the CLI, it's recommended that you alias the go command to run the dev version. Place this in your shell rc file (such as `.bashrc` or `.zshrc`)
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
index a91d734..58a5ada 100644
--- a/pkg/cmd/root.go
+++ b/pkg/cmd/root.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/stripe/stripe-cli/pkg/cmd/resource"
 	"github.com/stripe/stripe-cli/pkg/config"
+	"github.com/stripe/stripe-cli/pkg/stripe"
 	"github.com/stripe/stripe-cli/pkg/version"
 )
 
@@ -49,6 +50,9 @@ get started with pre-built samples (https://stripe.dev/samples).
 		getBanner(),
 		getLogin(&fs, &Config),
 	),
+	PersistentPreRun: func(cmd *cobra.Command, args []string) {
+		stripe.GetTelemetryInstance().SetCommandContext(cmd)
+	},
 }
 
 // Execute adds all child commands to the root command and sets flags appropriately.
diff --git a/pkg/stripe/client.go b/pkg/stripe/client.go
index 4f0744c..39cd2e9 100644
--- a/pkg/stripe/client.go
+++ b/pkg/stripe/client.go
@@ -64,6 +64,13 @@ func (c *Client) PerformRequest(ctx context.Context, method, path string, params
 	req.Header.Set("User-Agent", useragent.GetEncodedUserAgent())
 	req.Header.Set("X-Stripe-Client-User-Agent", useragent.GetEncodedStripeUserAgent())
 
+	if !telemetryOptedOut(os.Getenv("STRIPE_CLI_TELEMETRY_OPTOUT")) {
+		telemetryHdr, err := getTelemetryHeader()
+		if err == nil {
+			req.Header.Set("Stripe-CLI-Telemetry", telemetryHdr)
+		}
+	}
+
 	if c.APIKey != "" {
 		req.Header.Set("Authorization", "Bearer "+c.APIKey)
 	}
diff --git a/pkg/stripe/telemetry.go b/pkg/stripe/telemetry.go
new file mode 100644
index 0000000..feefad6
--- /dev/null
+++ b/pkg/stripe/telemetry.go
@@ -0,0 +1,71 @@
+package stripe
+
+import (
+	"encoding/json"
+	"strings"
+	"sync"
+
+	"github.com/spf13/cobra"
+)
+
+//
+// Public types
+//
+
+// CLITelemetry is the structure that holds telemetry data sent to Stripe in
+// API requests.
+type CLITelemetry struct {
+	CommandPath string `json:"command_path"`
+}
+
+// SetCommandContext sets the telemetry values for the command being executed.
+func (t *CLITelemetry) SetCommandContext(cmd *cobra.Command) {
+	t.CommandPath = cmd.CommandPath()
+}
+
+//
+// Public functions
+//
+
+// GetTelemetryInstance returns the CLITelemetry instance (initializing it
+// first if necessary).
+func GetTelemetryInstance() *CLITelemetry {
+	once.Do(func() {
+		instance = &CLITelemetry{}
+	})
+
+	return instance
+}
+
+//
+// Private variables
+//
+
+var instance *CLITelemetry
+var once sync.Once
+
+//
+// Private functions
+//
+
+func getTelemetryHeader() (string, error) {
+	telemetry := GetTelemetryInstance()
+	b, err := json.Marshal(telemetry)
+
+	if err != nil {
+		return "", err
+	}
+
+	return string(b), nil
+}
+
+// telemetryOptedOut returns true if the user has opted out of telemetry,
+// false otherwise.
+func telemetryOptedOut(optoutVar string) bool {
+	optoutVar = strings.ToLower(optoutVar)
+	if optoutVar == "1" || optoutVar == "true" {
+		return true
+	}
+
+	return false
+}


Test patch:
diff --git a/pkg/stripe/telemetry_test.go b/pkg/stripe/telemetry_test.go
new file mode 100644
index 0000000..aea22eb
--- /dev/null
+++ b/pkg/stripe/telemetry_test.go
@@ -0,0 +1,35 @@
+package stripe
+
+import (
+	"testing"
+
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetTelemetryInstance(t *testing.T) {
+	t1 := GetTelemetryInstance()
+	t2 := GetTelemetryInstance()
+	require.Equal(t, t1, t2)
+}
+
+func TestSetCommandContext(t *testing.T) {
+	tel := GetTelemetryInstance()
+	cmd := &cobra.Command{
+		Use: "foo",
+	}
+	tel.SetCommandContext(cmd)
+	require.Equal(t, "foo", tel.CommandPath)
+}
+
+func TestTelemetryOptedOut(t *testing.T) {
+	require.False(t, telemetryOptedOut(""))
+	require.False(t, telemetryOptedOut("0"))
+	require.False(t, telemetryOptedOut("false"))
+	require.False(t, telemetryOptedOut("False"))
+	require.False(t, telemetryOptedOut("FALSE"))
+	require.True(t, telemetryOptedOut("1"))
+	require.True(t, telemetryOptedOut("true"))
+	require.True(t, telemetryOptedOut("True"))
+	require.True(t, telemetryOptedOut("TRUE"))
+}