Skip to content

Commit 594cdab

Browse files
committed
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]>
1 parent adb7301 commit 594cdab

File tree

6 files changed

+139
-69
lines changed

6 files changed

+139
-69
lines changed

gopls/internal/server/prompt.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,6 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
113113
defer work.End(ctx, "Done.")
114114
}
115115

116-
if !enabled { // check this after the work progress message for testing.
117-
return // prompt is disabled
118-
}
119-
120-
if s.telemetryMode() == "on" || s.telemetryMode() == "off" {
121-
// Telemetry is already on or explicitly off -- nothing to ask about.
122-
return
123-
}
124-
125116
errorf := func(format string, args ...any) {
126117
err := fmt.Errorf(format, args...)
127118
event.Error(ctx, "telemetry prompt failed", err)
@@ -134,12 +125,14 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
134125
return
135126
}
136127

128+
// Read the current prompt file.
129+
137130
var (
138131
promptDir = filepath.Join(configDir, "prompt") // prompt configuration directory
139132
promptFile = filepath.Join(promptDir, "telemetry") // telemetry prompt file
140133
)
141134

142-
// prompt states, to be written to the prompt file
135+
// prompt states, stored in the prompt file
143136
const (
144137
pUnknown = "" // first time
145138
pNotReady = "-" // user is not asked yet (either not sampled or not past the grace period)
@@ -177,17 +170,55 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
177170
} else if !os.IsNotExist(err) {
178171
errorf("reading prompt file: %v", err)
179172
// Something went wrong. Since we don't know how many times we've asked the
180-
// prompt, err on the side of not spamming.
173+
// prompt, err on the side of not asking.
174+
//
175+
// But record this in telemetry, in case some users enable telemetry by
176+
// other means.
177+
counter.New("gopls/telemetryprompt/corrupted").Inc()
181178
return
182179
}
183180

184-
// Terminal conditions.
185-
if state == pYes || state == pNo {
186-
// Prompt has been answered. Nothing to do.
181+
counter.New(fmt.Sprintf("gopls/telemetryprompt/attempts:%d", attempts)).Inc()
182+
183+
// Check terminal conditions.
184+
185+
if state == pYes {
186+
// Prompt has been accepted.
187+
//
188+
// We record this counter for every gopls session, rather than when the
189+
// prompt actually accepted below, because if we only recorded it in the
190+
// counter file at the time telemetry is enabled, we'd never upload it,
191+
// because we exclude any counter files that overlap with a time period
192+
// that has telemetry uploading is disabled.
193+
counter.New("gopls/telemetryprompt/accepted").Inc()
194+
return
195+
}
196+
if state == pNo {
197+
// Prompt has been declined. In most cases, this means we'll never see the
198+
// counter below, but it's possible that the user may enable telemetry by
199+
// other means later on. If we see a significant number of users that have
200+
// accepted telemetry but declined the prompt, it may be an indication that
201+
// the prompt is not working well.
202+
counter.New("gopls/telemetryprompt/declined").Inc()
187203
return
188204
}
189205
if attempts >= 5 { // pPending or pFailed
190-
// We've tried asking enough; give up.
206+
// We've tried asking enough; give up. Record that the prompt expired, in
207+
// case the user decides to enable telemetry by other means later on.
208+
// (see also the pNo case).
209+
counter.New("gopls/telemetryprompt/expired").Inc()
210+
return
211+
}
212+
213+
// We only check enabled after (1) the work progress is started, and (2) the
214+
// prompt file has been read. (1) is for testing purposes, and (2) is so that
215+
// we record the "gopls/telemetryprompt/accepted" counter for every session.
216+
if !enabled {
217+
return // prompt is disabled
218+
}
219+
220+
if s.telemetryMode() == "on" || s.telemetryMode() == "off" {
221+
// Telemetry is already on or explicitly off -- nothing to ask about.
191222
return
192223
}
193224

@@ -309,7 +340,6 @@ Would you like to enable Go telemetry?
309340
result = pYes
310341
if err := s.setTelemetryMode("on"); err == nil {
311342
message(protocol.Info, telemetryOnMessage(s.Options().LinkifyShowMessage))
312-
counter.New("gopls/telemetryprompt/accepted").Inc()
313343
} else {
314344
errorf("enabling telemetry failed: %v", err)
315345
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
@@ -13,6 +13,7 @@ import (
1313
"testing"
1414
"time"
1515

16+
"github.com/google/go-cmp/cmp"
1617
"golang.org/x/telemetry/counter"
1718
"golang.org/x/telemetry/counter/countertest"
1819
"golang.org/x/tools/gopls/internal/protocol"
@@ -268,32 +269,63 @@ func main() {
268269
}
269270
`
270271

271-
acceptanceCounterName := "gopls/telemetryprompt/accepted"
272-
acceptanceCounter := counter.New(acceptanceCounterName)
273-
// We must increment the acceptance counter in order for the initial read
274-
// below to succeed.
272+
var (
273+
acceptanceCounter = "gopls/telemetryprompt/accepted"
274+
declinedCounter = "gopls/telemetryprompt/declined"
275+
attempt1Counter = "gopls/telemetryprompt/attempts:1"
276+
allCounters = []string{acceptanceCounter, declinedCounter, attempt1Counter}
277+
)
278+
279+
// We must increment counters in order for the initial reads below to
280+
// succeed.
275281
//
276282
// TODO(rfindley): ReadCounter should simply return 0 for uninitialized
277283
// counters.
278-
acceptanceCounter.Inc()
284+
for _, name := range allCounters {
285+
counter.New(name).Inc()
286+
}
287+
288+
readCounts := func(t *testing.T) map[string]uint64 {
289+
t.Helper()
290+
counts := make(map[string]uint64)
291+
for _, name := range allCounters {
292+
count, err := countertest.ReadCounter(counter.New(name))
293+
if err != nil {
294+
t.Fatalf("ReadCounter(%q) failed: %v", name, err)
295+
}
296+
counts[name] = count
297+
}
298+
return counts
299+
}
279300

280301
tests := []struct {
281-
name string // subtest name
282-
response string // response to choose for the telemetry dialog
283-
wantMode string // resulting telemetry mode
284-
wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected)
285-
wantInc uint64 // expected 'prompt accepted' counter increment
302+
name string // subtest name
303+
response string // response to choose for the telemetry dialog
304+
wantMode string // resulting telemetry mode
305+
wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected)
306+
wantInc uint64 // expected 'prompt accepted' counter increment
307+
wantCounts map[string]uint64
286308
}{
287-
{"yes", server.TelemetryYes, "on", "uploading is now enabled", 1},
288-
{"no", server.TelemetryNo, "", "", 0},
289-
{"empty", "", "", "", 0},
309+
{"yes", server.TelemetryYes, "on", "uploading is now enabled", 1, map[string]uint64{
310+
acceptanceCounter: 1,
311+
declinedCounter: 0,
312+
attempt1Counter: 1,
313+
}},
314+
{"no", server.TelemetryNo, "", "", 0, map[string]uint64{
315+
acceptanceCounter: 0,
316+
declinedCounter: 1,
317+
attempt1Counter: 1,
318+
}},
319+
{"empty", "", "", "", 0, map[string]uint64{
320+
acceptanceCounter: 0,
321+
declinedCounter: 0,
322+
attempt1Counter: 1,
323+
}},
290324
}
325+
291326
for _, test := range tests {
292327
t.Run(test.name, func(t *testing.T) {
293-
initialCount, err := countertest.ReadCounter(acceptanceCounter)
294-
if err != nil {
295-
t.Fatalf("ReadCounter(%q) failed: %v", acceptanceCounterName, err)
296-
}
328+
initialCounts := readCounts(t)
297329
modeFile := filepath.Join(t.TempDir(), "mode")
298330
telemetryStartTime := time.Now().Add(-8 * 24 * time.Hour)
299331
msgRE := regexp.MustCompile(".*Would you like to enable Go telemetry?")
@@ -340,12 +372,22 @@ func main() {
340372
if gotMode != test.wantMode {
341373
t.Errorf("after prompt, mode=%s, want %s", gotMode, test.wantMode)
342374
}
343-
finalCount, err := countertest.ReadCounter(acceptanceCounter)
344-
if err != nil {
345-
t.Fatalf("ReadCounter(%q) failed: %v", acceptanceCounterName, err)
375+
376+
// We increment the acceptance counter when checking the prompt file
377+
// before prompting, so start a second, transient gopls session and
378+
// verify that the acceptance counter is incremented.
379+
env2 := ConnectGoplsEnv(t, env.Ctx, env.Sandbox, env.Editor.Config(), env.Server)
380+
env2.Await(CompletedWork(server.TelemetryPromptWorkTitle, 1, true))
381+
if err := env2.Editor.Close(env2.Ctx); err != nil {
382+
t.Errorf("closing second editor: %v", err)
383+
}
384+
385+
gotCounts := readCounts(t)
386+
for k := range gotCounts {
387+
gotCounts[k] -= initialCounts[k]
346388
}
347-
if gotInc := finalCount - initialCount; gotInc != test.wantInc {
348-
t.Errorf("%q mismatch: got %d, want %d", acceptanceCounterName, gotInc, test.wantInc)
389+
if diff := cmp.Diff(test.wantCounts, gotCounts); diff != "" {
390+
t.Errorf("counter mismatch (-want +got):\n%s", diff)
349391
}
350392
})
351393
})

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)