Repository: stripe/stripe-cli (go)

Issue:
Connect Webhooks not POSTed to URL after being received
### Issue
Connect webhooks are received by stripe cli but not POSTed to my url when I just set a `--forward-connect-to url`. If I set both `--forward-connect-to` and `--forward-to` then they all get POSTed to my url correctly. [Screenshot of stripe-cli output](https://www.dropbox.com/s/rxxe5n6rhofcpxg/Screenshot%202019-10-17%2015.37.28.png?dl=0) notice the lack of POST lines at the bottom. Same checkout flow for both 

### Expected Behavior
I expect all connect webhooks to be POSTed to the url I specify with `--forward-connect-to` after they have been received. Even if `--forward-to` is not set.

### Steps to reproduce
Run `stripe listen  --forward-connect-to <URL>` (without `--forward-to`) and trigger a connect webhook

### Environment
macOS


Pull request description:
Allow --forward-connect-to without --forward-to
 ### Reviewers
r? @tomer-stripe 
cc @stripe/dev-platform

 ### Summary
Allow `--forward-connect-to` to be used when `--forward-to` is not passed. In that case, only Connect events will be forwarded.

Fixes #242.


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/listen.go b/pkg/cmd/listen.go
index f6a5025..bdc6d1a 100644
--- a/pkg/cmd/listen.go
+++ b/pkg/cmd/listen.go
@@ -132,6 +132,9 @@ func (lc *listenCmd) runListenCmd(cmd *cobra.Command, args []string) error {
 			Connect:        false,
 			EventTypes:     lc.events,
 		})
+	}
+
+	if len(lc.forwardConnectURL) > 0 {
 		endpointRoutes = append(endpointRoutes, proxy.EndpointRoute{
 			URL:            parseURL(lc.forwardConnectURL),
 			ForwardHeaders: lc.forwardConnectHeaders,
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/proxy/proxy.go b/pkg/proxy/proxy.go
index a60d8ea..c0ece43 100644
--- a/pkg/proxy/proxy.go
+++ b/pkg/proxy/proxy.go
@@ -100,11 +100,12 @@ func (p *Proxy) Run() error {
 	signal.Notify(p.interruptCh, os.Interrupt, syscall.SIGTERM)
 
 	go func() {
+		<-p.interruptCh
+
 		log.WithFields(log.Fields{
 			"prefix": "proxy.Proxy.Run",
 		}).Debug("Ctrl+C received, cleaning up...")
 
-		<-p.interruptCh
 		cancel()
 	}()
 
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"))
+}