diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index d0a4c918..ff0168ec 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net" + "net/http" "runtime" "sync/atomic" @@ -19,8 +20,10 @@ import ( ) const ( - AppName = "cli" - telemetryAnalyticsURL = "https://telemetry-proxy.algolia.com/" + AppName = "cli" + // No trailing slash: analytics-go appends "/v1/batch" to the endpoint. + telemetryAnalyticsURL = "https://telemetry-proxy.algolia.com" + envHeader = "X-Algolia-CLI-Env" ) type telemetryMetadataKey struct{} @@ -68,6 +71,10 @@ func NewAnalyticsTelemetryClient(debug bool) (TelemetryClient, error) { Endpoint: telemetryAnalyticsURL, Logger: newTelemetryLogger(debug), Verbose: debug, + Transport: &envHeaderTransport{ + base: http.DefaultTransport, + env: telemetryEnv(), + }, }) if err != nil { return nil, err @@ -75,6 +82,32 @@ func NewAnalyticsTelemetryClient(debug bool) (TelemetryClient, error) { return &AnalyticsTelemetryClient{client: client}, nil } +// envHeaderTransport adds the X-Algolia-CLI-Env header to every telemetry +// request, so the proxy can route release builds to the production Segment +// source and everything else to the development one. Until the proxy routes +// on the header, all events keep going to the development source. +type envHeaderTransport struct { + base http.RoundTripper + env string +} + +func (t *envHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // RoundTrippers must not mutate the caller's request. + req = req.Clone(req.Context()) + req.Header.Set(envHeader, t.env) + return t.base.RoundTrip(req) +} + +// telemetryEnv reports which environment the events belong to: "prod" for +// release builds (goreleaser injects a semver version), "dev" for source +// builds (the version stays "main"). +func telemetryEnv() string { + if version.Version == "main" { + return "dev" + } + return "prod" +} + // anonymousID is a unique identifier for an anonymous user of the CLI (basically the hash of the mac address) func anonymousID() string { addrs, err := net.Interfaces() diff --git a/pkg/telemetry/telemetry_test.go b/pkg/telemetry/telemetry_test.go index d9445ce2..95816218 100644 --- a/pkg/telemetry/telemetry_test.go +++ b/pkg/telemetry/telemetry_test.go @@ -2,6 +2,7 @@ package telemetry import ( "context" + "net/http" "sync" "testing" @@ -9,8 +10,46 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/algolia/cli/pkg/version" ) +// captureTransport records the request it receives without hitting the network. +type captureTransport struct { + req *http.Request +} + +func (c *captureTransport) RoundTrip(req *http.Request) (*http.Response, error) { + c.req = req + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil +} + +func TestEnvHeaderTransport_SetsHeaderWithoutMutatingRequest(t *testing.T) { + capture := &captureTransport{} + transport := &envHeaderTransport{base: capture, env: "prod"} + + req, err := http.NewRequest(http.MethodPost, "https://example.com/v1/batch", nil) + require.NoError(t, err) + + _, err = transport.RoundTrip(req) + require.NoError(t, err) + + assert.Equal(t, "prod", capture.req.Header.Get(envHeader)) + // RoundTrippers must not mutate the caller's request. + assert.Empty(t, req.Header.Get(envHeader)) +} + +func TestTelemetryEnv(t *testing.T) { + orig := version.Version + t.Cleanup(func() { version.Version = orig }) + + version.Version = "main" + assert.Equal(t, "dev", telemetryEnv()) + + version.Version = "1.20.0" + assert.Equal(t, "prod", telemetryEnv()) +} + // Context-related tests. func TestEventMetadataWithGet(t *testing.T) { ctx := context.Background()