Skip to content

Commit 3fdc39a

Browse files
Merge master into feature/emr
2 parents 3abafc2 + 83a118e commit 3fdc39a

File tree

8 files changed

+364
-80
lines changed

8 files changed

+364
-80
lines changed

packages/core/src/awsService/cloudformation/commands/cfnCommands.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
shouldImportResources,
3232
getResourcesToImport,
3333
getEnvironmentName,
34-
getChangeSetName,
3534
chooseOptionalFlagSuggestion as chooseOptionalFlagMode,
3635
getTags,
3736
getOnStackFailure,
@@ -111,14 +110,8 @@ export function executeChangeSetCommand(client: LanguageClient, coordinator: Sta
111110
}
112111

113112
export function deleteChangeSetCommand(client: LanguageClient) {
114-
return commands.registerCommand(commandKey('stacks.deleteChangeSet'), async (params?: ChangeSetReference) => {
113+
return commands.registerCommand(commandKey('stacks.deleteChangeSet'), async (params: ChangeSetReference) => {
115114
try {
116-
params = params ?? (await promptForChangeSetReference())
117-
118-
if (!params) {
119-
return
120-
}
121-
122115
const changeSetDeletion = new ChangeSetDeletion(params.stackName, params.changeSetName, client)
123116

124117
await changeSetDeletion.delete()
@@ -129,14 +122,8 @@ export function deleteChangeSetCommand(client: LanguageClient) {
129122
}
130123

131124
export function viewChangeSetCommand(client: LanguageClient, diffProvider: DiffWebviewProvider) {
132-
return commands.registerCommand(commandKey('stacks.viewChangeSet'), async (params?: ChangeSetReference) => {
125+
return commands.registerCommand(commandKey('stacks.viewChangeSet'), async (params: ChangeSetReference) => {
133126
try {
134-
params = params ?? (await promptForChangeSetReference())
135-
136-
if (!params) {
137-
return
138-
}
139-
140127
const describeChangeSetResult = await describeChangeSet(client, {
141128
changeSetName: params.changeSetName,
142129
stackName: params.stackName,
@@ -157,16 +144,6 @@ export function viewChangeSetCommand(client: LanguageClient, diffProvider: DiffW
157144
})
158145
}
159146

160-
async function promptForChangeSetReference(): Promise<ChangeSetReference | undefined> {
161-
const stackName = await getStackName()
162-
const changeSetName = await getChangeSetName()
163-
if (!stackName || !changeSetName) {
164-
return undefined
165-
}
166-
167-
return { stackName: stackName, changeSetName: changeSetName }
168-
}
169-
170147
export function deployTemplateCommand(
171148
client: LanguageClient,
172149
diffProvider: DiffWebviewProvider,

packages/core/src/awsService/cloudformation/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { AwsCredentialsService, encryptionKey } from './auth/credentials'
5252
import { ExtensionId, ExtensionName, Version, CloudFormationTelemetrySettings } from './extensionConfig'
5353
import { commandKey } from './utils'
5454
import { CloudFormationExplorer } from './explorer/explorer'
55-
import { promptTelemetryOptInWithTimeout } from './telemetryOptIn'
55+
import { handleTelemetryOptIn } from './telemetryOptIn'
5656

5757
import { refreshCommand, StacksManager } from './stacks/stacksManager'
5858
import { StackOverviewWebviewProvider } from './ui/stackOverviewWebviewProvider'
@@ -89,7 +89,7 @@ let clientDisposables: Disposable[] = []
8989

9090
async function startClient(context: ExtensionContext) {
9191
const cfnTelemetrySettings = new CloudFormationTelemetrySettings()
92-
const telemetryEnabled = await promptTelemetryOptInWithTimeout(context, cfnTelemetrySettings)
92+
const telemetryEnabled = await handleTelemetryOptIn(context, cfnTelemetrySettings)
9393

9494
const cfnLspConfig = {
9595
...DevSettings.instance.getServiceConfig('cloudformationLsp', {}),

packages/core/src/awsService/cloudformation/stacks/actions/changeSetDeletionWorkflow.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ import {
1515
import { deleteChangeSet, describeChangeSetDeletionStatus, getChangeSetDeletionStatus } from './stackActionApi'
1616
import { createChangeSetDeletionParams } from './stackActionUtil'
1717
import { getLogger } from '../../../../shared/logger/logger'
18-
import { extractErrorMessage } from '../../utils'
18+
import { commandKey, extractErrorMessage } from '../../utils'
19+
import { commands } from 'vscode'
20+
import globals from '../../../../shared/extensionGlobals'
1921

2022
export class ChangeSetDeletion {
2123
private readonly id: string
22-
private readonly stackName: string
23-
private readonly changeSetName: string
24-
private readonly client: LanguageClient
2524
private status: StackActionPhase | undefined
2625

27-
constructor(stackName: string, changeSetName: string, client: LanguageClient) {
26+
constructor(
27+
private readonly stackName: string,
28+
private readonly changeSetName: string,
29+
private readonly client: LanguageClient
30+
) {
2831
this.id = uuidv4()
29-
this.stackName = stackName
30-
this.changeSetName = changeSetName
31-
this.client = client
3232
}
3333

3434
async delete() {
@@ -38,7 +38,7 @@ export class ChangeSetDeletion {
3838
}
3939

4040
private pollForProgress() {
41-
const interval = setInterval(() => {
41+
const interval = globals.clock.setInterval(() => {
4242
getChangeSetDeletionStatus(this.client, { id: this.id })
4343
.then(async (deletionResult) => {
4444
if (deletionResult.phase === this.status) {
@@ -66,7 +66,8 @@ export class ChangeSetDeletion {
6666
describeDeplomentStatusResult.FailureReason ?? 'No failure reason provided'
6767
)
6868
}
69-
clearInterval(interval)
69+
void commands.executeCommand(commandKey('stacks.refresh'))
70+
globals.clock.clearInterval(interval)
7071
break
7172
case StackActionPhase.DELETION_FAILED: {
7273
const describeDeplomentStatusResult = await describeChangeSetDeletionStatus(this.client, {
@@ -77,15 +78,17 @@ export class ChangeSetDeletion {
7778
this.stackName,
7879
describeDeplomentStatusResult.FailureReason ?? 'No failure reason provided'
7980
)
80-
clearInterval(interval)
81+
void commands.executeCommand(commandKey('stacks.refresh'))
82+
globals.clock.clearInterval(interval)
8183
break
8284
}
8385
}
8486
})
8587
.catch(async (error) => {
8688
getLogger().error(`Error polling for deletion status: ${error}`)
8789
showErrorMessage(`Error polling for deletion status: ${extractErrorMessage(error)}`)
88-
clearInterval(interval)
90+
void commands.executeCommand(commandKey('stacks.refresh'))
91+
globals.clock.clearInterval(interval)
8992
})
9093
}, 1000)
9194
}

packages/core/src/awsService/cloudformation/telemetryOptIn.ts

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,74 +7,165 @@ import { ExtensionContext, env, Uri, window } from 'vscode'
77
import { CloudFormationTelemetrySettings } from './extensionConfig'
88
import { commandKey } from './utils'
99
import { 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

Comments
 (0)