Skip to content

Commit dcc114e

Browse files
committed
[gopls-release-branch.0.16] gopls: increment the telemetryprompt acceptance counter for each session
Increment the gopls/telemetryprompt/accepted counter every time we read the prompt file, so that we can actually use the resulting data. We don't upload any counter files that overlap with periods of time when telemetry uploading was not enabled, so with the previous model of only incrementing the counter at the moment the prompt was accepted, we will never upload the counter file into which the acceptance is recorded. With the new model, the counter will give us a sense of what fraction of telemetry participants opted-in as a result of the prompt. For golang/go#68770 Change-Id: I8890c73b5bfa19023bb24fd156bcaa9eb46295ad Reviewed-on: https://go-review.googlesource.com/c/tools/+/607758 Reviewed-by: Hyang-Ah Hana Kim <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> (cherry picked from commit 594cdab) Reviewed-on: https://go-review.googlesource.com/c/tools/+/609135
1 parent 48abc3e commit dcc114e

File tree

6 files changed

+144
-71
lines changed

6 files changed

+144
-71
lines changed

gopls/internal/server/prompt.go

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,6 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
9292
defer work.End(ctx, "Done.")
9393
}
9494

95-
if !enabled { // check this after the work progress message for testing.
96-
return // prompt is disabled
97-
}
98-
99-
if s.telemetryMode() == "on" || s.telemetryMode() == "off" {
100-
// Telemetry is already on or explicitly off -- nothing to ask about.
101-
return
102-
}
103-
10495
errorf := func(format string, args ...any) {
10596
err := fmt.Errorf(format, args...)
10697
event.Error(ctx, "telemetry prompt failed", err)
@@ -113,12 +104,14 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
113104
return
114105
}
115106

107+
// Read the current prompt file.
108+
116109
var (
117110
promptDir = filepath.Join(configDir, "prompt") // prompt configuration directory
118111
promptFile = filepath.Join(promptDir, "telemetry") // telemetry prompt file
119112
)
120113

121-
// prompt states, to be written to the prompt file
114+
// prompt states, stored in the prompt file
122115
const (
123116
pYes = "yes" // user said yes
124117
pNo = "no" // user said no
@@ -139,23 +132,64 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
139132
)
140133
if content, err := os.ReadFile(promptFile); err == nil {
141134
if _, err := fmt.Sscanf(string(content), "%s %d", &state, &attempts); err == nil && validStates[state] {
142-
if state == pYes || state == pNo {
143-
// Prompt has been answered. Nothing to do.
144-
return
145-
}
135+
// successfully parsed!
136+
// ~ v0.16: must have only two fields, state and attempts.
146137
} else {
147138
state, attempts = "", 0
148139
errorf("malformed prompt result %q", string(content))
149140
}
150141
} else if !os.IsNotExist(err) {
151142
errorf("reading prompt file: %v", err)
152143
// Something went wrong. Since we don't know how many times we've asked the
153-
// prompt, err on the side of not spamming.
144+
// prompt, err on the side of not asking.
145+
//
146+
// But record this in telemetry, in case some users enable telemetry by
147+
// other means.
148+
counter.New("gopls/telemetryprompt/corrupted").Inc()
149+
return
150+
}
151+
152+
counter.New(fmt.Sprintf("gopls/telemetryprompt/attempts:%d", attempts)).Inc()
153+
154+
// Check terminal conditions.
155+
156+
if state == pYes {
157+
// Prompt has been accepted.
158+
//
159+
// We record this counter for every gopls session, rather than when the
160+
// prompt actually accepted below, because if we only recorded it in the
161+
// counter file at the time telemetry is enabled, we'd never upload it,
162+
// because we exclude any counter files that overlap with a time period
163+
// that has telemetry uploading is disabled.
164+
counter.New("gopls/telemetryprompt/accepted").Inc()
165+
return
166+
}
167+
if state == pNo {
168+
// Prompt has been declined. In most cases, this means we'll never see the
169+
// counter below, but it's possible that the user may enable telemetry by
170+
// other means later on. If we see a significant number of users that have
171+
// accepted telemetry but declined the prompt, it may be an indication that
172+
// the prompt is not working well.
173+
counter.New("gopls/telemetryprompt/declined").Inc()
174+
return
175+
}
176+
if attempts >= 5 { // pPending or pFailed
177+
// We've tried asking enough; give up. Record that the prompt expired, in
178+
// case the user decides to enable telemetry by other means later on.
179+
// (see also the pNo case).
180+
counter.New("gopls/telemetryprompt/expired").Inc()
154181
return
155182
}
156183

157-
if attempts >= 5 {
158-
// We've tried asking enough; give up.
184+
// We only check enabled after (1) the work progress is started, and (2) the
185+
// prompt file has been read. (1) is for testing purposes, and (2) is so that
186+
// we record the "gopls/telemetryprompt/accepted" counter for every session.
187+
if !enabled {
188+
return // prompt is disabled
189+
}
190+
191+
if s.telemetryMode() == "on" || s.telemetryMode() == "off" {
192+
// Telemetry is already on or explicitly off -- nothing to ask about.
159193
return
160194
}
161195
if attempts == 0 {
@@ -237,7 +271,6 @@ Would you like to enable Go telemetry?
237271
result = pYes
238272
if err := s.setTelemetryMode("on"); err == nil {
239273
message(protocol.Info, telemetryOnMessage(s.Options().LinkifyShowMessage))
240-
counter.New("gopls/telemetryprompt/accepted").Inc()
241274
} else {
242275
errorf("enabling telemetry failed: %v", err)
243276
msg := fmt.Sprintf("Failed to enable Go telemetry: %v\nTo enable telemetry manually, please run `go run golang.org/x/telemetry/cmd/gotelemetry@latest on`", err)

gopls/internal/test/integration/fake/editor.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ func (e *Editor) Exit(ctx context.Context) error {
200200
return nil
201201
}
202202

203-
// Close issues the shutdown and exit sequence an editor should.
203+
// Close disconnects the LSP client session.
204+
// TODO(rfindley): rename to 'Disconnect'.
204205
func (e *Editor) Close(ctx context.Context) error {
205206
if err := e.Shutdown(ctx); err != nil {
206207
return err

gopls/internal/test/integration/fake/workdir.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"time"
2020

2121
"golang.org/x/tools/gopls/internal/protocol"
22+
"golang.org/x/tools/gopls/internal/util/slices"
2223
"golang.org/x/tools/internal/robustio"
2324
)
2425

@@ -333,8 +334,7 @@ func (w *Workdir) CheckForFileChanges(ctx context.Context) error {
333334
return nil
334335
}
335336
w.watcherMu.Lock()
336-
watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers))
337-
copy(watchers, w.watchers)
337+
watchers := slices.Clone(w.watchers)
338338
w.watcherMu.Unlock()
339339
for _, w := range watchers {
340340
w(ctx, evts)

gopls/internal/test/integration/misc/prompt_test.go

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"regexp"
1212
"testing"
1313

14+
"github.com/google/go-cmp/cmp"
1415
"golang.org/x/telemetry/counter"
1516
"golang.org/x/telemetry/counter/countertest"
1617
"golang.org/x/tools/gopls/internal/protocol"
@@ -87,32 +88,63 @@ func main() {
8788
}
8889
`
8990

90-
acceptanceCounterName := "gopls/telemetryprompt/accepted"
91-
acceptanceCounter := counter.New(acceptanceCounterName)
92-
// We must increment the acceptance counter in order for the initial read
93-
// below to succeed.
91+
var (
92+
acceptanceCounter = "gopls/telemetryprompt/accepted"
93+
declinedCounter = "gopls/telemetryprompt/declined"
94+
attempt1Counter = "gopls/telemetryprompt/attempts:1"
95+
allCounters = []string{acceptanceCounter, declinedCounter, attempt1Counter}
96+
)
97+
98+
// We must increment counters in order for the initial reads below to
99+
// succeed.
94100
//
95101
// TODO(rfindley): ReadCounter should simply return 0 for uninitialized
96102
// counters.
97-
acceptanceCounter.Inc()
103+
for _, name := range allCounters {
104+
counter.New(name).Inc()
105+
}
106+
107+
readCounts := func(t *testing.T) map[string]uint64 {
108+
t.Helper()
109+
counts := make(map[string]uint64)
110+
for _, name := range allCounters {
111+
count, err := countertest.ReadCounter(counter.New(name))
112+
if err != nil {
113+
t.Fatalf("ReadCounter(%q) failed: %v", name, err)
114+
}
115+
counts[name] = count
116+
}
117+
return counts
118+
}
98119

99120
tests := []struct {
100-
name string // subtest name
101-
response string // response to choose for the telemetry dialog
102-
wantMode string // resulting telemetry mode
103-
wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected)
104-
wantInc uint64 // expected 'prompt accepted' counter increment
121+
name string // subtest name
122+
response string // response to choose for the telemetry dialog
123+
wantMode string // resulting telemetry mode
124+
wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected)
125+
wantInc uint64 // expected 'prompt accepted' counter increment
126+
wantCounts map[string]uint64
105127
}{
106-
{"yes", server.TelemetryYes, "on", "uploading is now enabled", 1},
107-
{"no", server.TelemetryNo, "", "", 0},
108-
{"empty", "", "", "", 0},
128+
{"yes", server.TelemetryYes, "on", "uploading is now enabled", 1, map[string]uint64{
129+
acceptanceCounter: 1,
130+
declinedCounter: 0,
131+
attempt1Counter: 1,
132+
}},
133+
{"no", server.TelemetryNo, "", "", 0, map[string]uint64{
134+
acceptanceCounter: 0,
135+
declinedCounter: 1,
136+
attempt1Counter: 1,
137+
}},
138+
{"empty", "", "", "", 0, map[string]uint64{
139+
acceptanceCounter: 0,
140+
declinedCounter: 0,
141+
attempt1Counter: 1,
142+
}},
109143
}
144+
110145
for _, test := range tests {
111146
t.Run(test.name, func(t *testing.T) {
112-
initialCount, err := countertest.ReadCounter(acceptanceCounter)
113-
if err != nil {
114-
t.Fatalf("ReadCounter(%q) failed: %v", acceptanceCounterName, err)
115-
}
147+
initialCounts := readCounts(t)
116148
modeFile := filepath.Join(t.TempDir(), "mode")
117149
msgRE := regexp.MustCompile(".*Would you like to enable Go telemetry?")
118150
respond := func(m *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
@@ -156,12 +188,22 @@ func main() {
156188
if gotMode != test.wantMode {
157189
t.Errorf("after prompt, mode=%s, want %s", gotMode, test.wantMode)
158190
}
159-
finalCount, err := countertest.ReadCounter(acceptanceCounter)
160-
if err != nil {
161-
t.Fatalf("ReadCounter(%q) failed: %v", acceptanceCounterName, err)
191+
192+
// We increment the acceptance counter when checking the prompt file
193+
// before prompting, so start a second, transient gopls session and
194+
// verify that the acceptance counter is incremented.
195+
env2 := ConnectGoplsEnv(t, env.Ctx, env.Sandbox, env.Editor.Config(), env.Server)
196+
env2.Await(CompletedWork(server.TelemetryPromptWorkTitle, 1, true))
197+
if err := env2.Editor.Close(env2.Ctx); err != nil {
198+
t.Errorf("closing second editor: %v", err)
199+
}
200+
201+
gotCounts := readCounts(t)
202+
for k := range gotCounts {
203+
gotCounts[k] -= initialCounts[k]
162204
}
163-
if gotInc := finalCount - initialCount; gotInc != test.wantInc {
164-
t.Errorf("%q mismatch: got %d, want %d", acceptanceCounterName, gotInc, test.wantInc)
205+
if diff := cmp.Diff(test.wantCounts, gotCounts); diff != "" {
206+
t.Errorf("counter mismatch (-want +got):\n%s", diff)
165207
}
166208
})
167209
})

gopls/internal/test/integration/misc/shared_test.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"testing"
99

1010
. "golang.org/x/tools/gopls/internal/test/integration"
11-
"golang.org/x/tools/gopls/internal/test/integration/fake"
1211
)
1312

1413
// Smoke test that simultaneous editing sessions in the same workspace works.
@@ -32,19 +31,7 @@ func main() {
3231
).Run(t, sharedProgram, func(t *testing.T, env1 *Env) {
3332
// Create a second test session connected to the same workspace and server
3433
// as the first.
35-
awaiter := NewAwaiter(env1.Sandbox.Workdir)
36-
editor, err := fake.NewEditor(env1.Sandbox, env1.Editor.Config()).Connect(env1.Ctx, env1.Server, awaiter.Hooks())
37-
if err != nil {
38-
t.Fatal(err)
39-
}
40-
env2 := &Env{
41-
T: t,
42-
Ctx: env1.Ctx,
43-
Sandbox: env1.Sandbox,
44-
Server: env1.Server,
45-
Editor: editor,
46-
Awaiter: awaiter,
47-
}
34+
env2 := ConnectGoplsEnv(t, env1.Ctx, env1.Sandbox, env1.Editor.Config(), env1.Server)
4835
env2.Await(InitialWorkspaceLoad)
4936
// In editor #1, break fmt.Println as before.
5037
env1.OpenFile("main.go")

gopls/internal/test/integration/runner.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -215,19 +215,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
215215
framer = ls.framer(jsonrpc2.NewRawStream)
216216
ts := servertest.NewPipeServer(ss, framer)
217217

218-
awaiter := NewAwaiter(sandbox.Workdir)
219-
editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks())
220-
if err != nil {
221-
t.Fatal(err)
222-
}
223-
env := &Env{
224-
T: t,
225-
Ctx: ctx,
226-
Sandbox: sandbox,
227-
Editor: editor,
228-
Server: ts,
229-
Awaiter: awaiter,
230-
}
218+
env := ConnectGoplsEnv(t, ctx, sandbox, config.editor, ts)
231219
defer func() {
232220
if t.Failed() && r.PrintGoroutinesOnFailure {
233221
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
@@ -242,7 +230,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
242230
// the editor: in general we want to clean up before proceeding to the
243231
// next test, and if there is a deadlock preventing closing it will
244232
// eventually be handled by the `go test` timeout.
245-
if err := editor.Close(xcontext.Detach(ctx)); err != nil {
233+
if err := env.Editor.Close(xcontext.Detach(ctx)); err != nil {
246234
t.Errorf("closing editor: %v", err)
247235
}
248236
}()
@@ -253,6 +241,28 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
253241
}
254242
}
255243

244+
// ConnectGoplsEnv creates a new Gopls environment for the given sandbox,
245+
// editor config, and server connector.
246+
//
247+
// TODO(rfindley): significantly refactor the way testing environments are
248+
// constructed.
249+
func ConnectGoplsEnv(t testing.TB, ctx context.Context, sandbox *fake.Sandbox, config fake.EditorConfig, connector servertest.Connector) *Env {
250+
awaiter := NewAwaiter(sandbox.Workdir)
251+
editor, err := fake.NewEditor(sandbox, config).Connect(ctx, connector, awaiter.Hooks())
252+
if err != nil {
253+
t.Fatal(err)
254+
}
255+
env := &Env{
256+
T: t,
257+
Ctx: ctx,
258+
Sandbox: sandbox,
259+
Server: connector,
260+
Editor: editor,
261+
Awaiter: awaiter,
262+
}
263+
return env
264+
}
265+
256266
// longBuilders maps builders that are skipped when -short is set to a
257267
// (possibly empty) justification.
258268
var longBuilders = map[string]string{

0 commit comments

Comments
 (0)