Skip to content

Commit 8fa4173

Browse files
committed
gopls/internal/server: conditionally prompt for telemetry
Previously, VS Code Go extension conditionally asked gopls to consider to prompt, only if * it thinks telemetry data was logged for 7days+, and * the user is selected (based on hash of vscode cliet id) This change implements the condition checking inside gopls, so we can enable prompting for other editor users and simplify vscode-go's code. The prompt file format is changed. old format: <state> <prompt_count> new format: <state> <prompt_count> <creation_unix_time> <token> where - creation_unix_time is the guessed telemetry start time (unix time) - token is a random integer in [1, 1000], which is used in sampling decision. This CL adds environment variables to control the creation_unix_time and token values in integration testing. They are also useful for manual testing, and for VS Code Go prompt logic migration. VS Code Go extension had been used a vscode machine id hash and kept its observed telemetry start time in memento. The env vars can be used to forward the info to gopls. For golang/go#67821 Change-Id: I13d2bf6d43ea1e5ef8ebec7eb2f89fc9af8a8db7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/589517 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent b9a361a commit 8fa4173

File tree

2 files changed

+296
-34
lines changed

2 files changed

+296
-34
lines changed

gopls/internal/server/prompt.go

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ package server
77
import (
88
"context"
99
"fmt"
10+
"math/rand"
1011
"os"
1112
"path/filepath"
13+
"strconv"
1214
"time"
1315

1416
"golang.org/x/telemetry"
@@ -23,15 +25,34 @@ import (
2325
// crash).
2426
const promptTimeout = 24 * time.Hour
2527

28+
// gracePeriod is the amount of time we wait before sufficient telemetry data
29+
// is accumulated in the local directory, so users can have time to review
30+
// what kind of information will be collected and uploaded when prompting starts.
31+
const gracePeriod = 7 * 24 * time.Hour
32+
33+
// samplesPerMille is the prompt probability.
34+
// Token is an integer between [1, 1000] and is assigned when maybePromptForTelemetry
35+
// is called first time. Only the user with a token ∈ [1, samplesPerMille]
36+
// will be considered for prompting.
37+
const samplesPerMille = 10 // 1% sample rate
38+
2639
// The following constants are used for testing telemetry integration.
2740
const (
2841
TelemetryPromptWorkTitle = "Checking telemetry prompt" // progress notification title, for awaiting in tests
2942
GoplsConfigDirEnvvar = "GOPLS_CONFIG_DIR" // overridden for testing
3043
FakeTelemetryModefileEnvvar = "GOPLS_FAKE_TELEMETRY_MODEFILE" // overridden for testing
44+
FakeSamplesPerMille = "GOPLS_FAKE_SAMPLES_PER_MILLE" // overridden for testing
3145
TelemetryYes = "Yes, I'd like to help."
3246
TelemetryNo = "No, thanks."
3347
)
3448

49+
// The following environment variables may be set by the client.
50+
// Exported for testing telemetry integration.
51+
const (
52+
GoTelemetryGoplsClientStartTimeEnvvar = "GOTELEMETRY_GOPLS_CLIENT_START_TIME" // telemetry start time recored in client
53+
GoTelemetryGoplsClientTokenEnvvar = "GOTELEMETRY_GOPLS_CLIENT_TOKEN" // sampling token
54+
)
55+
3556
// getenv returns the effective environment variable value for the provided
3657
// key, looking up the key in the session environment before falling back on
3758
// the process environment.
@@ -119,31 +140,37 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
119140

120141
// prompt states, to be written to the prompt file
121142
const (
122-
pYes = "yes" // user said yes
123-
pNo = "no" // user said no
124-
pPending = "pending" // current prompt is still pending
125-
pFailed = "failed" // prompt was asked but failed
143+
pUnknown = "" // first time
144+
pNotReady = "-" // user is not asked yet (either not sampled or not past the grace period)
145+
pYes = "yes" // user said yes
146+
pNo = "no" // user said no
147+
pPending = "pending" // current prompt is still pending
148+
pFailed = "failed" // prompt was asked but failed
126149
)
127150
validStates := map[string]bool{
128-
pYes: true,
129-
pNo: true,
130-
pPending: true,
131-
pFailed: true,
151+
pNotReady: true,
152+
pYes: true,
153+
pNo: true,
154+
pPending: true,
155+
pFailed: true,
132156
}
133157

134-
// parse the current prompt file
158+
// Parse the current prompt file.
135159
var (
136-
state string
160+
state = pUnknown
137161
attempts = 0 // number of times we've asked already
162+
163+
// the followings are recorded after gopls v0.17+.
164+
token = 0 // valid token is [1, 1000]
165+
creationTime int64 // unix time sec
138166
)
139167
if content, err := os.ReadFile(promptFile); err == nil {
140-
if _, err := fmt.Sscanf(string(content), "%s %d", &state, &attempts); err == nil && validStates[state] {
141-
if state == pYes || state == pNo {
142-
// Prompt has been answered. Nothing to do.
143-
return
144-
}
168+
if n, _ := fmt.Sscanf(string(content), "%s %d %d %d", &state, &attempts, &creationTime, &token); (n == 2 || n == 4) && validStates[state] {
169+
// successfully parsed!
170+
// ~ v0.16: must have only two fields, state and attempts.
171+
// v0.17 ~: must have all four fields.
145172
} else {
146-
state, attempts = "", 0
173+
state, attempts, creationTime, token = pUnknown, 0, 0, 0
147174
errorf("malformed prompt result %q", string(content))
148175
}
149176
} else if !os.IsNotExist(err) {
@@ -153,19 +180,58 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
153180
return
154181
}
155182

156-
if attempts >= 5 {
183+
// Terminal conditions.
184+
if state == pYes || state == pNo {
185+
// Prompt has been answered. Nothing to do.
186+
return
187+
}
188+
if attempts >= 5 { // pPending or pFailed
157189
// We've tried asking enough; give up.
158190
return
159191
}
160-
if attempts == 0 {
161-
// First time asking the prompt; we may need to make the prompt dir.
192+
193+
// Transition: pUnknown -> pNotReady
194+
if state == pUnknown {
195+
// First time; we need to make the prompt dir.
162196
if err := os.MkdirAll(promptDir, 0777); err != nil {
163197
errorf("creating prompt dir: %v", err)
164198
return
165199
}
200+
state = pNotReady
201+
}
202+
203+
// Correct missing values.
204+
if creationTime == 0 {
205+
creationTime = time.Now().Unix()
206+
if v := s.getenv(GoTelemetryGoplsClientStartTimeEnvvar); v != "" {
207+
if sec, err := strconv.ParseInt(v, 10, 64); err == nil && sec > 0 {
208+
creationTime = sec
209+
}
210+
}
211+
}
212+
if token == 0 {
213+
token = rand.Intn(1000) + 1
214+
if v := s.getenv(GoTelemetryGoplsClientTokenEnvvar); v != "" {
215+
if tok, err := strconv.Atoi(v); err == nil && 1 <= tok && tok <= 1000 {
216+
token = tok
217+
}
218+
}
166219
}
167220

168-
// Acquire the lock and write "pending" to the prompt file before actually
221+
// Transition: pNotReady -> pPending if sampled
222+
if state == pNotReady {
223+
threshold := samplesPerMille
224+
if v := s.getenv(FakeSamplesPerMille); v != "" {
225+
if t, err := strconv.Atoi(v); err == nil {
226+
threshold = t
227+
}
228+
}
229+
if token <= threshold && time.Now().Unix()-creationTime > gracePeriod.Milliseconds()/1000 {
230+
state = pPending
231+
}
232+
}
233+
234+
// Acquire the lock and write the updated state to the prompt file before actually
169235
// prompting.
170236
//
171237
// This ensures that the prompt file is writeable, and that we increment the
@@ -178,19 +244,25 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
178244
return
179245
}
180246
if !ok {
181-
// Another prompt is currently pending.
247+
// Another process is making decision.
182248
return
183249
}
184250
defer release()
185251

186-
attempts++
252+
if state != pNotReady { // pPending or pFailed
253+
attempts++
254+
}
187255

188-
pendingContent := []byte(fmt.Sprintf("%s %d", pPending, attempts))
256+
pendingContent := []byte(fmt.Sprintf("%s %d %d %d", state, attempts, creationTime, token))
189257
if err := os.WriteFile(promptFile, pendingContent, 0666); err != nil {
190258
errorf("writing pending state: %v", err)
191259
return
192260
}
193261

262+
if state == pNotReady {
263+
return
264+
}
265+
194266
var prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at https://go.dev/doc/telemetry.
195267
196268
Would you like to enable Go telemetry?
@@ -249,7 +321,7 @@ Would you like to enable Go telemetry?
249321
message(protocol.Error, fmt.Sprintf("Unrecognized response %q", item.Title))
250322
}
251323
}
252-
resultContent := []byte(fmt.Sprintf("%s %d", result, attempts))
324+
resultContent := []byte(fmt.Sprintf("%s %d %d %d", result, attempts, creationTime, token))
253325
if err := os.WriteFile(promptFile, resultContent, 0666); err != nil {
254326
errorf("error writing result state to prompt file: %v", err)
255327
}

0 commit comments

Comments
 (0)