Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@

vendor/
gh

# Test coverage artifacts
coverage.out
lcov.info
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ completions: bin/gh$(EXE)
bin/gh$(EXE) completion -s fish > ./share/fish/vendor_completions.d/gh.fish
bin/gh$(EXE) completion -s zsh > ./share/zsh/site-functions/_gh

.PHONY: lint
lint:
golangci-lint run ./...

# just convenience tasks around `go test`
.PHONY: test
test:
Expand Down
11 changes: 11 additions & 0 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ func TestWorkflows(t *testing.T) {
testscript.Run(t, testScriptParamsFor(tsEnv, "workflow"))
}

func TestTelemetry(t *testing.T) {
var tsEnv testScriptEnv
if err := tsEnv.fromEnv(); err != nil {
t.Fatal(err)
}

testscript.Run(t, testScriptParamsFor(tsEnv, "telemetry"))
}

func testScriptParamsFor(tsEnv testScriptEnv, command string) testscript.Params {
var files []string
if tsEnv.script != "" {
Expand Down Expand Up @@ -226,6 +235,8 @@ func sharedSetup(tsEnv testScriptEnv) func(ts *testscript.Env) error {

ts.Setenv("RANDOM_STRING", randomString(10))

ts.Setenv("GH_TELEMETRY", "false")

// The sandbox overrides HOME, so git cannot find the user's global
// config. Write a minimal identity so commits inside the sandbox
// don't fail with "Author identity unknown".
Expand Down
9 changes: 9 additions & 0 deletions acceptance/testdata/telemetry/command-invocation.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Telemetry log mode outputs command invocation event to stderr
env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=log
env GH_TELEMETRY_SAMPLE_RATE=100

exec gh version
stderr 'Telemetry payload:'
stderr '"type": "command_invocation"'
stderr '"command": "gh version"'
18 changes: 18 additions & 0 deletions acceptance/testdata/telemetry/no-telemetry-for-alias.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Aliases should not leak their user-defined names via telemetry, but the
# resolved inner command should still record normally — its path is a core
# gh command and conveys no user-authored identifier.

env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=log
env GH_TELEMETRY_SAMPLE_RATE=100

# Create a regular (non-shell) alias that resolves to an existing command.
exec gh alias set secret-project-alias version

# Invoking the alias must not produce any event carrying the alias name.
exec gh secret-project-alias
! stderr 'secret-project-alias'

# The resolved inner command still records telemetry as normal.
stderr 'Telemetry payload:'
stderr '"command": "gh version"'
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The completion command should not generate a telemetry event
env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=log
env GH_TELEMETRY_SAMPLE_RATE=100

exec gh completion -s bash
! stderr 'Telemetry payload:'
27 changes: 27 additions & 0 deletions acceptance/testdata/telemetry/no-telemetry-for-extension.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Extensions should not generate telemetry events
[!exec:bash] skip

env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=log
env GH_TELEMETRY_SAMPLE_RATE=100

# Create a local shell extension repository
exec git init gh-hello
cp gh-hello.sh gh-hello/gh-hello
chmod 755 gh-hello/gh-hello
exec git -C gh-hello add gh-hello
exec git -C gh-hello commit -m 'init'

# Install it locally
cd gh-hello
exec gh ext install .
cd $WORK

# Run the extension and verify no telemetry is logged
exec gh hello
stdout 'hello from extension'
! stderr 'Telemetry payload:'

-- gh-hello.sh --
#!/usr/bin/env bash
echo "hello from extension"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# GHES users should not get telemetry even when telemetry is enabled
env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=log
env GH_TELEMETRY_SAMPLE_RATE=100
env GH_ENTERPRISE_TOKEN=fake-enterprise-token

exec gh version
! stderr 'Telemetry payload:'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# The send-telemetry command should not itself generate a telemetry event
env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=log
env GH_TELEMETRY_SAMPLE_RATE=100
env GH_TELEMETRY_ENDPOINT_URL=http://localhost:1

# Provide a minimal valid payload on stdin so the command can run.
# It will fail to connect but that's fine — we only care about telemetry logging.
stdin payload.json
! exec gh send-telemetry
! stderr 'Telemetry payload:'

-- payload.json --
{"events":[{"type":"test","dimensions":{},"measures":{}}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Command completes successfully even when telemetry endpoint is unreachable
env GH_PRIVATE_ENABLE_TELEMETRY=1
env GH_TELEMETRY=enabled
env GH_TELEMETRY_SAMPLE_RATE=100
env GH_TELEMETRY_ENDPOINT_URL=http://localhost:1

exec gh version
stdout 'gh version'
21 changes: 21 additions & 0 deletions api/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/cli/cli/v2/internal/gh/ghtelemetry"
"github.com/cli/cli/v2/utils"
ghAPI "github.com/cli/go-gh/v2/pkg/api"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
Expand All @@ -26,6 +27,7 @@ type HTTPClientOptions struct {
LogColorize bool
LogVerboseHTTP bool
SkipDefaultHeaders bool
TelemetryDisabler ghtelemetry.Disabler
}

func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
Expand Down Expand Up @@ -74,6 +76,13 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
client.Transport = AddAuthTokenHeader(client.Transport, opts.Config)
}

if opts.TelemetryDisabler != nil {
client.Transport = telemetryDisablerTransport{
wrappedTransport: client.Transport,
telemetryDisabler: opts.TelemetryDisabler,
}
}

return client, nil
}

Expand Down Expand Up @@ -147,3 +156,15 @@ func getHost(r *http.Request) string {
}
return r.URL.Host
}

type telemetryDisablerTransport struct {
wrappedTransport http.RoundTripper
telemetryDisabler ghtelemetry.Disabler
}

func (t telemetryDisablerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if ghauth.IsEnterprise(getHost(req)) {
t.telemetryDisabler.Disable()
}
return t.wrappedTransport.RoundTrip(req)
}
74 changes: 74 additions & 0 deletions api/http_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,80 @@ func TestHTTPClientSanitizeControlCharactersC1(t *testing.T) {
assert.Equal(t, "monalisa¡", issue.Author.Login)
}

func TestNewHTTPClientTelemetryDisabler(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()

tests := []struct {
name string
host string
wantDisabled bool
}{
{
name: "enterprise host triggers disable",
host: "ghes.example.com",
wantDisabled: true,
},
{
name: "github.com does not trigger disable",
host: "github.com",
wantDisabled: false,
},
{
name: "tenancy host does not trigger disable",
host: "my-company.ghe.com",
wantDisabled: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
disabler := &fakeTelemetryDisabler{}
client, err := NewHTTPClient(HTTPClientOptions{
TelemetryDisabler: disabler,
})
require.NoError(t, err)

req, err := http.NewRequest("GET", ts.URL, nil)
require.NoError(t, err)
req.Host = tt.host

res, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 204, res.StatusCode)
assert.Equal(t, tt.wantDisabled, disabler.disabled, "Disable() called")
})
}
}

func TestNewHTTPClientWithoutTelemetryDisabler(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()

client, err := NewHTTPClient(HTTPClientOptions{})
require.NoError(t, err)

req, err := http.NewRequest("GET", ts.URL, nil)
require.NoError(t, err)
req.Host = "ghes.example.com"

res, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 204, res.StatusCode)
}

type fakeTelemetryDisabler struct {
disabled bool
}

func (f *fakeTelemetryDisabler) Disable() {
f.disabled = true
}

type tinyConfig map[string]string

func (c tinyConfig) ActiveToken(host string) (string, string) {
Expand Down
3 changes: 2 additions & 1 deletion cmd/gen-docs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/internal/docs"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/telemetry"
"github.com/cli/cli/v2/pkg/cmd/root"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/extensions"
Expand Down Expand Up @@ -53,7 +54,7 @@ func run(args []string) error {
return config.NewFromString(""), nil
},
ExtensionManager: &em{},
}, "", "")
}, &telemetry.NoOpService{}, "", "")
rootCmd.InitDefaultHelpCmd()

if err := os.MkdirAll(*dir, 0755); err != nil {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.21.4
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-version v1.9.0
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
Expand All @@ -52,6 +53,7 @@ require (
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/theupdateframework/go-tuf/v2 v2.4.1
github.com/twitchtv/twirp v8.1.3+incompatible
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/yuin/goldmark v1.8.2
github.com/zalando/go-keyring v0.2.8
Expand Down Expand Up @@ -129,7 +131,6 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,8 @@ github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
Expand Down
Loading
Loading