@@ -7,74 +7,165 @@ import { ExtensionContext, env, Uri, window } from 'vscode'
77import { CloudFormationTelemetrySettings } from './extensionConfig'
88import { commandKey } from './utils'
99import { isAutomation } from '../../shared/vscode/env'
10+ import { getLogger } from '../../shared/logger/logger'
11+ import globals from '../../shared/extensionGlobals'
1012
11- export async function promptTelemetryOptInWithTimeout (
12- context : ExtensionContext ,
13- cfnTelemetrySettings : CloudFormationTelemetrySettings
14- ) : Promise < boolean > {
15- const promptPromise = promptTelemetryOptIn ( context , cfnTelemetrySettings )
16- const timeoutPromise = new Promise < false > ( ( resolve ) => setTimeout ( ( ) => resolve ( false ) , 2500 ) )
13+ enum TelemetryChoice {
14+ Allow = 'Yes, Allow' ,
15+ Later = 'Not Now' ,
16+ Never = 'Never' ,
17+ LearnMore = 'Learn More' ,
18+ }
1719
18- const result = await Promise . race ( [ promptPromise , timeoutPromise ] )
20+ const telemetryKeys = {
21+ hasResponded : commandKey ( 'telemetry.hasResponded' ) ,
22+ lastPromptDate : commandKey ( 'telemetry.lastPromptDate' ) ,
23+ unpersistedResponse : commandKey ( 'telemetry.unpersistedResponse' ) ,
24+ } as const
1925
20- // Keep prompt alive in background
21- void promptPromise
26+ const telemetrySettings = {
27+ enabled : 'enabled' ,
28+ } as const
2229
23- return result
24- }
30+ const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000
31+ const promptTimeoutMs = 2500
32+ const telemetryDocsUrl = 'https://github.com/aws-cloudformation/cloudformation-languageserver/tree/main/src/telemetry'
2533
2634/* eslint-disable aws-toolkits/no-banned-usages */
27- async function promptTelemetryOptIn (
35+ export async function handleTelemetryOptIn (
2836 context : ExtensionContext ,
2937 cfnTelemetrySettings : CloudFormationTelemetrySettings
3038) : Promise < boolean > {
31- const telemetryEnabled = cfnTelemetrySettings . get ( 'enabled' , false )
32- if ( isAutomation ( ) ) {
33- return telemetryEnabled
39+ // If previous choice failed to persist, persist it now and return
40+ const unpersistedResponse = ( await context . globalState . get ( telemetryKeys . unpersistedResponse ) ) as string
41+ const hasResponded = context . globalState . get < boolean > ( telemetryKeys . hasResponded )
42+ const lastPromptDate = context . globalState . get < number > ( telemetryKeys . lastPromptDate )
43+ if ( unpersistedResponse ) {
44+ // May still raise popup if user lacks permission or file is corrupted
45+ const didSave = await saveTelemetryResponse ( unpersistedResponse , cfnTelemetrySettings )
46+ await context . globalState . update ( telemetryKeys . unpersistedResponse , undefined )
47+ // If we still couldn't save, clear everything so they get asked again until the file/perms is fixed
48+ if ( ! didSave ) {
49+ getLogger ( ) . warn (
50+ 'CloudFormation telemetry choice was not saved successfully after restart. Clearing related globalState keys for next restart'
51+ )
52+ await context . globalState . update ( telemetryKeys . hasResponded , undefined )
53+ await context . globalState . update ( telemetryKeys . lastPromptDate , undefined )
54+ }
55+ return logAndReturnTelemetryChoice (
56+ unpersistedResponse === TelemetryChoice . Allow . toString ( ) ,
57+ hasResponded ,
58+ lastPromptDate
59+ )
3460 }
3561
36- const hasResponded = context . globalState . get < boolean > ( commandKey ( 'telemetry.hasResponded' ) , false )
37- const lastPromptDate = context . globalState . get < number > ( commandKey ( 'telemetry.lastPromptDate' ) , 0 )
38- const now = Date . now ( )
39- const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000
62+ // Never throws because we provide a default
63+ const telemetryEnabled = cfnTelemetrySettings . get ( telemetrySettings . enabled , false )
64+
65+ if ( isAutomation ( ) ) {
66+ return logAndReturnTelemetryChoice ( telemetryEnabled )
67+ }
4068
4169 // If user has permanently responded, use their choice
4270 if ( hasResponded ) {
43- return telemetryEnabled
71+ return logAndReturnTelemetryChoice ( telemetryEnabled , hasResponded )
4472 }
4573
4674 // Check if we should show reminder (30 days since last prompt)
47- const shouldPrompt = lastPromptDate === 0 || now - lastPromptDate >= thirtyDaysMs
75+ const shouldPrompt = lastPromptDate === undefined || globals . clock . Date . now ( ) - lastPromptDate >= thirtyDaysMs
4876 if ( ! shouldPrompt ) {
49- return telemetryEnabled
77+ return logAndReturnTelemetryChoice ( telemetryEnabled , hasResponded , lastPromptDate )
78+ }
79+
80+ // Show prompt but set false if timeout
81+ const promptPromise = promptTelemetryOptIn ( context , cfnTelemetrySettings )
82+ const timeoutPromise = new Promise < false > ( ( resolve ) =>
83+ globals . clock . setTimeout ( ( ) => resolve ( false ) , promptTimeoutMs )
84+ )
85+ const result = await Promise . race ( [ promptPromise , timeoutPromise ] )
86+
87+ // Keep prompt alive in background
88+ void promptPromise
89+
90+ return logAndReturnTelemetryChoice ( result )
91+ }
92+ /**
93+ * Updates the telemetry setting. In case of error, the update calls do not throw.
94+ * They instead raise a popup and return false.
95+ *
96+ * @returns boolean whether the save/update was successful
97+ */
98+ /* eslint-disable aws-toolkits/no-banned-usages */
99+ async function saveTelemetryResponse (
100+ response : string | undefined ,
101+ cfnTelemetrySettings : CloudFormationTelemetrySettings
102+ ) : Promise < boolean > {
103+ if ( response === TelemetryChoice . Allow ) {
104+ return await cfnTelemetrySettings . update ( telemetrySettings . enabled , true )
105+ } else if ( response === TelemetryChoice . Never ) {
106+ return await cfnTelemetrySettings . update ( telemetrySettings . enabled , false )
107+ } else if ( response === TelemetryChoice . Later ) {
108+ return await cfnTelemetrySettings . update ( telemetrySettings . enabled , false )
50109 }
110+ return false
111+ }
51112
113+ function logAndReturnTelemetryChoice ( choice : boolean , hasResponded ?: boolean , lastPromptDate ?: number ) : boolean {
114+ getLogger ( ) . info (
115+ 'CloudFormation telemetry: choice=%s, hasResponded=%s, lastPromptDate=%s' ,
116+ choice ,
117+ hasResponded ,
118+ lastPromptDate
119+ )
120+ return choice
121+ }
122+
123+ /* eslint-disable aws-toolkits/no-banned-usages */
124+ async function promptTelemetryOptIn (
125+ context : ExtensionContext ,
126+ cfnTelemetrySettings : CloudFormationTelemetrySettings
127+ ) : Promise < boolean > {
52128 const message =
53129 'Help us improve the AWS CloudFormation Language Server by sharing anonymous telemetry data with AWS. You can change this preference at any time in aws.cloudformation Settings.'
54130
55- const allow = 'Yes, Allow'
56- const later = 'Not Now'
57- const never = 'Never'
58- const learnMore = 'Learn More'
59- const response = await window . showInformationMessage ( message , allow , later , never , learnMore )
131+ const response = await window . showInformationMessage (
132+ message ,
133+ TelemetryChoice . Allow ,
134+ TelemetryChoice . Later ,
135+ TelemetryChoice . Never ,
136+ TelemetryChoice . LearnMore
137+ )
60138
61- if ( response === learnMore ) {
62- await env . openExternal (
63- Uri . parse ( 'https://github.com/aws-cloudformation/cloudformation-languageserver/tree/main/src/telemetry' )
64- )
139+ if ( response === TelemetryChoice . LearnMore ) {
140+ await env . openExternal ( Uri . parse ( telemetryDocsUrl ) )
65141 return promptTelemetryOptIn ( context , cfnTelemetrySettings )
66142 }
67143
68- if ( response === allow ) {
69- await cfnTelemetrySettings . update ( 'enabled' , true )
70- await context . globalState . update ( commandKey ( 'telemetry.hasResponded' ) , true )
71- } else if ( response === never ) {
72- await cfnTelemetrySettings . update ( 'enabled' , false )
73- await context . globalState . update ( commandKey ( 'telemetry.hasResponded' ) , true )
74- } else if ( response === later ) {
75- await cfnTelemetrySettings . update ( 'enabled' , false )
76- await context . globalState . update ( commandKey ( 'telemetry.lastPromptDate' ) , now )
144+ const now = globals . clock . Date . now ( )
145+ await context . globalState . update ( telemetryKeys . lastPromptDate , now )
146+
147+ // There's a chance our settings aren't registered yet from package.json, so we
148+ // see if we can persist to settings first
149+ try {
150+ // Throws (with no popup) if setting is not registered
151+ cfnTelemetrySettings . get ( telemetrySettings . enabled )
152+ } catch ( err ) {
153+ getLogger ( ) . warn ( err as Error )
154+ // Save the choice in globalState and save to settings next time handleTelemetryOptIn is called
155+ await context . globalState . update ( telemetryKeys . unpersistedResponse , response )
156+ if ( response === TelemetryChoice . Allow ) {
157+ await context . globalState . update ( telemetryKeys . hasResponded , true )
158+ return true
159+ } else if ( response === TelemetryChoice . Never ) {
160+ await context . globalState . update ( telemetryKeys . hasResponded , true )
161+ return false
162+ } else if ( response === TelemetryChoice . Later ) {
163+ return false
164+ }
77165 }
78166
79- return cfnTelemetrySettings . get ( 'enabled' , false )
167+ // At this point should be able to save and get successfully
168+ await saveTelemetryResponse ( response , cfnTelemetrySettings )
169+ await context . globalState . update ( telemetryKeys . hasResponded , response !== TelemetryChoice . Later )
170+ return cfnTelemetrySettings . get ( telemetrySettings . enabled , false )
80171}
0 commit comments