@@ -7,8 +7,10 @@ package server
7
7
import (
8
8
"context"
9
9
"fmt"
10
+ "math/rand"
10
11
"os"
11
12
"path/filepath"
13
+ "strconv"
12
14
"time"
13
15
14
16
"golang.org/x/telemetry"
@@ -23,15 +25,34 @@ import (
23
25
// crash).
24
26
const promptTimeout = 24 * time .Hour
25
27
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
+
26
39
// The following constants are used for testing telemetry integration.
27
40
const (
28
41
TelemetryPromptWorkTitle = "Checking telemetry prompt" // progress notification title, for awaiting in tests
29
42
GoplsConfigDirEnvvar = "GOPLS_CONFIG_DIR" // overridden for testing
30
43
FakeTelemetryModefileEnvvar = "GOPLS_FAKE_TELEMETRY_MODEFILE" // overridden for testing
44
+ FakeSamplesPerMille = "GOPLS_FAKE_SAMPLES_PER_MILLE" // overridden for testing
31
45
TelemetryYes = "Yes, I'd like to help."
32
46
TelemetryNo = "No, thanks."
33
47
)
34
48
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
+
35
56
// getenv returns the effective environment variable value for the provided
36
57
// key, looking up the key in the session environment before falling back on
37
58
// the process environment.
@@ -119,31 +140,37 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
119
140
120
141
// prompt states, to be written to the prompt file
121
142
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
126
149
)
127
150
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 ,
132
156
}
133
157
134
- // parse the current prompt file
158
+ // Parse the current prompt file.
135
159
var (
136
- state string
160
+ state = pUnknown
137
161
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
138
166
)
139
167
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.
145
172
} else {
146
- state , attempts = "" , 0
173
+ state , attempts , creationTime , token = pUnknown , 0 , 0 , 0
147
174
errorf ("malformed prompt result %q" , string (content ))
148
175
}
149
176
} else if ! os .IsNotExist (err ) {
@@ -153,19 +180,58 @@ func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) {
153
180
return
154
181
}
155
182
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
157
189
// We've tried asking enough; give up.
158
190
return
159
191
}
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.
162
196
if err := os .MkdirAll (promptDir , 0777 ); err != nil {
163
197
errorf ("creating prompt dir: %v" , err )
164
198
return
165
199
}
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
+ }
166
219
}
167
220
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
169
235
// prompting.
170
236
//
171
237
// 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) {
178
244
return
179
245
}
180
246
if ! ok {
181
- // Another prompt is currently pending .
247
+ // Another process is making decision .
182
248
return
183
249
}
184
250
defer release ()
185
251
186
- attempts ++
252
+ if state != pNotReady { // pPending or pFailed
253
+ attempts ++
254
+ }
187
255
188
- pendingContent := []byte (fmt .Sprintf ("%s %d" , pPending , attempts ))
256
+ pendingContent := []byte (fmt .Sprintf ("%s %d %d %d " , state , attempts , creationTime , token ))
189
257
if err := os .WriteFile (promptFile , pendingContent , 0666 ); err != nil {
190
258
errorf ("writing pending state: %v" , err )
191
259
return
192
260
}
193
261
262
+ if state == pNotReady {
263
+ return
264
+ }
265
+
194
266
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.
195
267
196
268
Would you like to enable Go telemetry?
@@ -249,7 +321,7 @@ Would you like to enable Go telemetry?
249
321
message (protocol .Error , fmt .Sprintf ("Unrecognized response %q" , item .Title ))
250
322
}
251
323
}
252
- resultContent := []byte (fmt .Sprintf ("%s %d" , result , attempts ))
324
+ resultContent := []byte (fmt .Sprintf ("%s %d %d %d " , result , attempts , creationTime , token ))
253
325
if err := os .WriteFile (promptFile , resultContent , 0666 ); err != nil {
254
326
errorf ("error writing result state to prompt file: %v" , err )
255
327
}
0 commit comments